mlrun 1.7.2rc3__py3-none-any.whl → 1.8.0__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 +26 -22
- mlrun/__main__.py +15 -16
- mlrun/alerts/alert.py +150 -15
- mlrun/api/schemas/__init__.py +1 -9
- mlrun/artifacts/__init__.py +2 -3
- mlrun/artifacts/base.py +62 -19
- mlrun/artifacts/dataset.py +17 -17
- mlrun/artifacts/document.py +454 -0
- mlrun/artifacts/manager.py +28 -18
- mlrun/artifacts/model.py +91 -59
- mlrun/artifacts/plots.py +2 -2
- mlrun/common/constants.py +8 -0
- mlrun/common/formatters/__init__.py +1 -0
- mlrun/common/formatters/artifact.py +1 -1
- mlrun/common/formatters/feature_set.py +2 -0
- mlrun/common/formatters/function.py +1 -0
- mlrun/{model_monitoring/db/stores/v3io_kv/__init__.py → common/formatters/model_endpoint.py} +17 -0
- mlrun/common/formatters/pipeline.py +1 -2
- mlrun/common/formatters/project.py +9 -0
- mlrun/common/model_monitoring/__init__.py +0 -5
- mlrun/common/model_monitoring/helpers.py +12 -62
- mlrun/common/runtimes/constants.py +25 -4
- mlrun/common/schemas/__init__.py +9 -5
- mlrun/common/schemas/alert.py +114 -19
- mlrun/common/schemas/api_gateway.py +3 -3
- mlrun/common/schemas/artifact.py +22 -9
- mlrun/common/schemas/auth.py +8 -4
- mlrun/common/schemas/background_task.py +7 -7
- mlrun/common/schemas/client_spec.py +4 -4
- mlrun/common/schemas/clusterization_spec.py +2 -2
- mlrun/common/schemas/common.py +53 -3
- mlrun/common/schemas/constants.py +15 -0
- mlrun/common/schemas/datastore_profile.py +1 -1
- mlrun/common/schemas/feature_store.py +9 -9
- mlrun/common/schemas/frontend_spec.py +4 -4
- mlrun/common/schemas/function.py +10 -10
- mlrun/common/schemas/hub.py +1 -1
- mlrun/common/schemas/k8s.py +3 -3
- mlrun/common/schemas/memory_reports.py +3 -3
- mlrun/common/schemas/model_monitoring/__init__.py +4 -8
- mlrun/common/schemas/model_monitoring/constants.py +127 -46
- mlrun/common/schemas/model_monitoring/grafana.py +18 -12
- mlrun/common/schemas/model_monitoring/model_endpoints.py +154 -160
- mlrun/common/schemas/notification.py +24 -3
- mlrun/common/schemas/object.py +1 -1
- mlrun/common/schemas/pagination.py +4 -4
- mlrun/common/schemas/partition.py +142 -0
- mlrun/common/schemas/pipeline.py +3 -3
- mlrun/common/schemas/project.py +26 -18
- mlrun/common/schemas/runs.py +3 -3
- mlrun/common/schemas/runtime_resource.py +5 -5
- mlrun/common/schemas/schedule.py +1 -1
- mlrun/common/schemas/secret.py +1 -1
- mlrun/{model_monitoring/db/stores/sqldb/__init__.py → common/schemas/serving.py} +10 -1
- mlrun/common/schemas/tag.py +3 -3
- mlrun/common/schemas/workflow.py +6 -5
- mlrun/common/types.py +1 -0
- mlrun/config.py +157 -89
- mlrun/data_types/__init__.py +5 -3
- mlrun/data_types/infer.py +13 -3
- mlrun/data_types/spark.py +2 -1
- mlrun/datastore/__init__.py +59 -18
- mlrun/datastore/alibaba_oss.py +4 -1
- mlrun/datastore/azure_blob.py +4 -1
- mlrun/datastore/base.py +19 -24
- mlrun/datastore/datastore.py +10 -4
- mlrun/datastore/datastore_profile.py +178 -45
- mlrun/datastore/dbfs_store.py +4 -1
- mlrun/datastore/filestore.py +4 -1
- mlrun/datastore/google_cloud_storage.py +4 -1
- mlrun/datastore/hdfs.py +4 -1
- mlrun/datastore/inmem.py +4 -1
- mlrun/datastore/redis.py +4 -1
- mlrun/datastore/s3.py +14 -3
- mlrun/datastore/sources.py +89 -92
- mlrun/datastore/store_resources.py +7 -4
- mlrun/datastore/storeytargets.py +51 -16
- mlrun/datastore/targets.py +38 -31
- mlrun/datastore/utils.py +87 -4
- mlrun/datastore/v3io.py +4 -1
- mlrun/datastore/vectorstore.py +291 -0
- mlrun/datastore/wasbfs/fs.py +13 -12
- mlrun/db/base.py +286 -100
- mlrun/db/httpdb.py +1562 -490
- mlrun/db/nopdb.py +250 -83
- mlrun/errors.py +6 -2
- mlrun/execution.py +194 -50
- mlrun/feature_store/__init__.py +2 -10
- mlrun/feature_store/api.py +20 -458
- mlrun/feature_store/common.py +9 -9
- mlrun/feature_store/feature_set.py +20 -18
- mlrun/feature_store/feature_vector.py +105 -479
- mlrun/feature_store/feature_vector_utils.py +466 -0
- mlrun/feature_store/retrieval/base.py +15 -11
- mlrun/feature_store/retrieval/job.py +2 -1
- mlrun/feature_store/retrieval/storey_merger.py +1 -1
- mlrun/feature_store/steps.py +3 -3
- mlrun/features.py +30 -13
- mlrun/frameworks/__init__.py +1 -2
- mlrun/frameworks/_common/__init__.py +1 -2
- mlrun/frameworks/_common/artifacts_library.py +2 -2
- mlrun/frameworks/_common/mlrun_interface.py +10 -6
- mlrun/frameworks/_common/model_handler.py +31 -31
- mlrun/frameworks/_common/producer.py +3 -1
- mlrun/frameworks/_dl_common/__init__.py +1 -2
- mlrun/frameworks/_dl_common/loggers/__init__.py +1 -2
- mlrun/frameworks/_dl_common/loggers/mlrun_logger.py +4 -4
- mlrun/frameworks/_dl_common/loggers/tensorboard_logger.py +3 -3
- mlrun/frameworks/_ml_common/__init__.py +1 -2
- mlrun/frameworks/_ml_common/loggers/__init__.py +1 -2
- mlrun/frameworks/_ml_common/model_handler.py +21 -21
- mlrun/frameworks/_ml_common/plans/__init__.py +1 -2
- mlrun/frameworks/_ml_common/plans/confusion_matrix_plan.py +3 -1
- mlrun/frameworks/_ml_common/plans/dataset_plan.py +3 -3
- mlrun/frameworks/_ml_common/plans/roc_curve_plan.py +4 -4
- mlrun/frameworks/auto_mlrun/__init__.py +1 -2
- mlrun/frameworks/auto_mlrun/auto_mlrun.py +22 -15
- mlrun/frameworks/huggingface/__init__.py +1 -2
- mlrun/frameworks/huggingface/model_server.py +9 -9
- mlrun/frameworks/lgbm/__init__.py +47 -44
- mlrun/frameworks/lgbm/callbacks/__init__.py +1 -2
- mlrun/frameworks/lgbm/callbacks/logging_callback.py +4 -2
- mlrun/frameworks/lgbm/callbacks/mlrun_logging_callback.py +4 -2
- mlrun/frameworks/lgbm/mlrun_interfaces/__init__.py +1 -2
- mlrun/frameworks/lgbm/mlrun_interfaces/mlrun_interface.py +5 -5
- mlrun/frameworks/lgbm/model_handler.py +15 -11
- mlrun/frameworks/lgbm/model_server.py +11 -7
- mlrun/frameworks/lgbm/utils.py +2 -2
- mlrun/frameworks/onnx/__init__.py +1 -2
- mlrun/frameworks/onnx/dataset.py +3 -3
- mlrun/frameworks/onnx/mlrun_interface.py +2 -2
- mlrun/frameworks/onnx/model_handler.py +7 -5
- mlrun/frameworks/onnx/model_server.py +8 -6
- mlrun/frameworks/parallel_coordinates.py +11 -11
- mlrun/frameworks/pytorch/__init__.py +22 -23
- mlrun/frameworks/pytorch/callbacks/__init__.py +1 -2
- mlrun/frameworks/pytorch/callbacks/callback.py +2 -1
- mlrun/frameworks/pytorch/callbacks/logging_callback.py +15 -8
- mlrun/frameworks/pytorch/callbacks/mlrun_logging_callback.py +19 -12
- mlrun/frameworks/pytorch/callbacks/tensorboard_logging_callback.py +22 -15
- mlrun/frameworks/pytorch/callbacks_handler.py +36 -30
- mlrun/frameworks/pytorch/mlrun_interface.py +17 -17
- mlrun/frameworks/pytorch/model_handler.py +21 -17
- mlrun/frameworks/pytorch/model_server.py +13 -9
- mlrun/frameworks/sklearn/__init__.py +19 -18
- mlrun/frameworks/sklearn/estimator.py +2 -2
- mlrun/frameworks/sklearn/metric.py +3 -3
- mlrun/frameworks/sklearn/metrics_library.py +8 -6
- mlrun/frameworks/sklearn/mlrun_interface.py +3 -2
- mlrun/frameworks/sklearn/model_handler.py +4 -3
- mlrun/frameworks/tf_keras/__init__.py +11 -12
- mlrun/frameworks/tf_keras/callbacks/__init__.py +1 -2
- mlrun/frameworks/tf_keras/callbacks/logging_callback.py +17 -14
- mlrun/frameworks/tf_keras/callbacks/mlrun_logging_callback.py +15 -12
- mlrun/frameworks/tf_keras/callbacks/tensorboard_logging_callback.py +21 -18
- mlrun/frameworks/tf_keras/model_handler.py +17 -13
- mlrun/frameworks/tf_keras/model_server.py +12 -8
- mlrun/frameworks/xgboost/__init__.py +19 -18
- mlrun/frameworks/xgboost/model_handler.py +13 -9
- mlrun/k8s_utils.py +2 -5
- mlrun/launcher/base.py +3 -4
- mlrun/launcher/client.py +2 -2
- mlrun/launcher/local.py +6 -2
- mlrun/launcher/remote.py +1 -1
- mlrun/lists.py +8 -4
- mlrun/model.py +132 -46
- mlrun/model_monitoring/__init__.py +3 -5
- mlrun/model_monitoring/api.py +113 -98
- mlrun/model_monitoring/applications/__init__.py +0 -5
- mlrun/model_monitoring/applications/_application_steps.py +81 -50
- mlrun/model_monitoring/applications/base.py +467 -14
- mlrun/model_monitoring/applications/context.py +212 -134
- mlrun/model_monitoring/{db/stores/base → applications/evidently}/__init__.py +6 -2
- mlrun/model_monitoring/applications/evidently/base.py +146 -0
- mlrun/model_monitoring/applications/histogram_data_drift.py +89 -56
- mlrun/model_monitoring/applications/results.py +67 -15
- mlrun/model_monitoring/controller.py +701 -315
- mlrun/model_monitoring/db/__init__.py +0 -2
- mlrun/model_monitoring/db/_schedules.py +242 -0
- mlrun/model_monitoring/db/_stats.py +189 -0
- mlrun/model_monitoring/db/tsdb/__init__.py +33 -22
- mlrun/model_monitoring/db/tsdb/base.py +243 -49
- mlrun/model_monitoring/db/tsdb/tdengine/schemas.py +76 -36
- mlrun/model_monitoring/db/tsdb/tdengine/stream_graph_steps.py +33 -0
- mlrun/model_monitoring/db/tsdb/tdengine/tdengine_connection.py +213 -0
- mlrun/model_monitoring/db/tsdb/tdengine/tdengine_connector.py +534 -88
- mlrun/model_monitoring/db/tsdb/v3io/stream_graph_steps.py +1 -0
- mlrun/model_monitoring/db/tsdb/v3io/v3io_connector.py +436 -106
- mlrun/model_monitoring/helpers.py +356 -114
- mlrun/model_monitoring/stream_processing.py +190 -345
- mlrun/model_monitoring/tracking_policy.py +11 -4
- mlrun/model_monitoring/writer.py +49 -90
- mlrun/package/__init__.py +3 -6
- mlrun/package/context_handler.py +2 -2
- mlrun/package/packager.py +12 -9
- mlrun/package/packagers/__init__.py +0 -2
- mlrun/package/packagers/default_packager.py +14 -11
- mlrun/package/packagers/numpy_packagers.py +16 -7
- mlrun/package/packagers/pandas_packagers.py +18 -18
- mlrun/package/packagers/python_standard_library_packagers.py +25 -11
- mlrun/package/packagers_manager.py +35 -32
- mlrun/package/utils/__init__.py +0 -3
- mlrun/package/utils/_pickler.py +6 -6
- mlrun/platforms/__init__.py +47 -16
- mlrun/platforms/iguazio.py +4 -1
- mlrun/projects/operations.py +30 -30
- mlrun/projects/pipelines.py +116 -47
- mlrun/projects/project.py +1292 -329
- mlrun/render.py +5 -9
- mlrun/run.py +57 -14
- mlrun/runtimes/__init__.py +1 -3
- mlrun/runtimes/base.py +30 -22
- mlrun/runtimes/daskjob.py +9 -9
- mlrun/runtimes/databricks_job/databricks_runtime.py +6 -5
- mlrun/runtimes/function_reference.py +5 -2
- mlrun/runtimes/generators.py +3 -2
- mlrun/runtimes/kubejob.py +6 -7
- mlrun/runtimes/mounts.py +574 -0
- mlrun/runtimes/mpijob/__init__.py +0 -2
- mlrun/runtimes/mpijob/abstract.py +7 -6
- mlrun/runtimes/nuclio/api_gateway.py +7 -7
- mlrun/runtimes/nuclio/application/application.py +11 -13
- mlrun/runtimes/nuclio/application/reverse_proxy.go +66 -64
- mlrun/runtimes/nuclio/function.py +127 -70
- mlrun/runtimes/nuclio/serving.py +105 -37
- mlrun/runtimes/pod.py +159 -54
- mlrun/runtimes/remotesparkjob.py +3 -2
- mlrun/runtimes/sparkjob/__init__.py +0 -2
- mlrun/runtimes/sparkjob/spark3job.py +22 -12
- mlrun/runtimes/utils.py +7 -6
- mlrun/secrets.py +2 -2
- mlrun/serving/__init__.py +8 -0
- mlrun/serving/merger.py +7 -5
- mlrun/serving/remote.py +35 -22
- mlrun/serving/routers.py +186 -240
- mlrun/serving/server.py +41 -10
- mlrun/serving/states.py +432 -118
- mlrun/serving/utils.py +13 -2
- mlrun/serving/v1_serving.py +3 -2
- mlrun/serving/v2_serving.py +161 -203
- mlrun/track/__init__.py +1 -1
- mlrun/track/tracker.py +2 -2
- mlrun/track/trackers/mlflow_tracker.py +6 -5
- mlrun/utils/async_http.py +35 -22
- mlrun/utils/clones.py +7 -4
- mlrun/utils/helpers.py +511 -58
- mlrun/utils/logger.py +119 -13
- mlrun/utils/notifications/notification/__init__.py +22 -19
- mlrun/utils/notifications/notification/base.py +39 -15
- mlrun/utils/notifications/notification/console.py +6 -6
- mlrun/utils/notifications/notification/git.py +11 -11
- mlrun/utils/notifications/notification/ipython.py +10 -9
- mlrun/utils/notifications/notification/mail.py +176 -0
- mlrun/utils/notifications/notification/slack.py +16 -8
- mlrun/utils/notifications/notification/webhook.py +24 -8
- mlrun/utils/notifications/notification_pusher.py +191 -200
- mlrun/utils/regex.py +12 -2
- mlrun/utils/version/version.json +2 -2
- {mlrun-1.7.2rc3.dist-info → mlrun-1.8.0.dist-info}/METADATA +81 -54
- mlrun-1.8.0.dist-info/RECORD +351 -0
- {mlrun-1.7.2rc3.dist-info → mlrun-1.8.0.dist-info}/WHEEL +1 -1
- mlrun/model_monitoring/applications/evidently_base.py +0 -137
- mlrun/model_monitoring/db/stores/__init__.py +0 -136
- mlrun/model_monitoring/db/stores/base/store.py +0 -213
- mlrun/model_monitoring/db/stores/sqldb/models/__init__.py +0 -71
- mlrun/model_monitoring/db/stores/sqldb/models/base.py +0 -190
- mlrun/model_monitoring/db/stores/sqldb/models/mysql.py +0 -103
- mlrun/model_monitoring/db/stores/sqldb/models/sqlite.py +0 -40
- mlrun/model_monitoring/db/stores/sqldb/sql_store.py +0 -659
- mlrun/model_monitoring/db/stores/v3io_kv/kv_store.py +0 -726
- mlrun/model_monitoring/model_endpoint.py +0 -118
- mlrun-1.7.2rc3.dist-info/RECORD +0 -351
- {mlrun-1.7.2rc3.dist-info → mlrun-1.8.0.dist-info}/entry_points.txt +0 -0
- {mlrun-1.7.2rc3.dist-info → mlrun-1.8.0.dist-info/licenses}/LICENSE +0 -0
- {mlrun-1.7.2rc3.dist-info → mlrun-1.8.0.dist-info}/top_level.txt +0 -0
mlrun/utils/helpers.py
CHANGED
|
@@ -13,8 +13,10 @@
|
|
|
13
13
|
# limitations under the License.
|
|
14
14
|
|
|
15
15
|
import asyncio
|
|
16
|
+
import base64
|
|
16
17
|
import enum
|
|
17
18
|
import functools
|
|
19
|
+
import gzip
|
|
18
20
|
import hashlib
|
|
19
21
|
import inspect
|
|
20
22
|
import itertools
|
|
@@ -23,35 +25,43 @@ import os
|
|
|
23
25
|
import re
|
|
24
26
|
import string
|
|
25
27
|
import sys
|
|
28
|
+
import traceback
|
|
26
29
|
import typing
|
|
27
30
|
import uuid
|
|
28
31
|
import warnings
|
|
29
|
-
from datetime import datetime, timezone
|
|
32
|
+
from datetime import datetime, timedelta, timezone
|
|
30
33
|
from importlib import import_module, reload
|
|
31
34
|
from os import path
|
|
32
35
|
from types import ModuleType
|
|
33
36
|
from typing import Any, Optional
|
|
37
|
+
from urllib.parse import urlparse
|
|
34
38
|
|
|
35
39
|
import git
|
|
36
40
|
import inflection
|
|
37
41
|
import numpy as np
|
|
38
42
|
import packaging.version
|
|
39
43
|
import pandas
|
|
44
|
+
import pytz
|
|
40
45
|
import semver
|
|
41
46
|
import yaml
|
|
42
47
|
from dateutil import parser
|
|
43
|
-
from mlrun_pipelines.models import PipelineRun
|
|
44
48
|
from pandas import Timedelta, Timestamp
|
|
45
49
|
from yaml.representer import RepresenterError
|
|
46
50
|
|
|
47
51
|
import mlrun
|
|
52
|
+
import mlrun.common.constants as mlrun_constants
|
|
48
53
|
import mlrun.common.helpers
|
|
54
|
+
import mlrun.common.runtimes.constants as runtimes_constants
|
|
49
55
|
import mlrun.common.schemas
|
|
50
56
|
import mlrun.errors
|
|
51
57
|
import mlrun.utils.regex
|
|
52
58
|
import mlrun.utils.version.version
|
|
59
|
+
import mlrun_pipelines.common.constants
|
|
60
|
+
import mlrun_pipelines.models
|
|
61
|
+
import mlrun_pipelines.utils
|
|
53
62
|
from mlrun.common.constants import MYSQL_MEDIUMBLOB_SIZE_BYTES
|
|
54
63
|
from mlrun.config import config
|
|
64
|
+
from mlrun_pipelines.models import PipelineRun
|
|
55
65
|
|
|
56
66
|
from .logger import create_logger
|
|
57
67
|
from .retryer import ( # noqa: F401
|
|
@@ -85,14 +95,19 @@ class StorePrefix:
|
|
|
85
95
|
Artifact = "artifacts"
|
|
86
96
|
Model = "models"
|
|
87
97
|
Dataset = "datasets"
|
|
98
|
+
Document = "documents"
|
|
88
99
|
|
|
89
100
|
@classmethod
|
|
90
101
|
def is_artifact(cls, prefix):
|
|
91
|
-
return prefix in [cls.Artifact, cls.Model, cls.Dataset]
|
|
102
|
+
return prefix in [cls.Artifact, cls.Model, cls.Dataset, cls.Document]
|
|
92
103
|
|
|
93
104
|
@classmethod
|
|
94
105
|
def kind_to_prefix(cls, kind):
|
|
95
|
-
kind_map = {
|
|
106
|
+
kind_map = {
|
|
107
|
+
"model": cls.Model,
|
|
108
|
+
"dataset": cls.Dataset,
|
|
109
|
+
"document": cls.Document,
|
|
110
|
+
}
|
|
96
111
|
return kind_map.get(kind, cls.Artifact)
|
|
97
112
|
|
|
98
113
|
@classmethod
|
|
@@ -103,6 +118,7 @@ class StorePrefix:
|
|
|
103
118
|
cls.Dataset,
|
|
104
119
|
cls.FeatureSet,
|
|
105
120
|
cls.FeatureVector,
|
|
121
|
+
cls.Document,
|
|
106
122
|
]
|
|
107
123
|
|
|
108
124
|
|
|
@@ -111,21 +127,27 @@ def get_artifact_target(item: dict, project=None):
|
|
|
111
127
|
project_str = project or item["metadata"].get("project")
|
|
112
128
|
tree = item["metadata"].get("tree")
|
|
113
129
|
tag = item["metadata"].get("tag")
|
|
130
|
+
iter = item["metadata"].get("iter")
|
|
114
131
|
kind = item.get("kind")
|
|
132
|
+
uid = item["metadata"].get("uid")
|
|
115
133
|
|
|
116
134
|
if kind in {"dataset", "model", "artifact"} and db_key:
|
|
117
135
|
target = (
|
|
118
136
|
f"{DB_SCHEMA}://{StorePrefix.kind_to_prefix(kind)}/{project_str}/{db_key}"
|
|
119
137
|
)
|
|
138
|
+
if iter:
|
|
139
|
+
target = f"{target}#{iter}"
|
|
120
140
|
target += f":{tag}" if tag else ":latest"
|
|
121
141
|
if tree:
|
|
122
142
|
target += f"@{tree}"
|
|
143
|
+
if uid:
|
|
144
|
+
target += f"^{uid}"
|
|
123
145
|
return target
|
|
124
146
|
|
|
125
147
|
return item["spec"].get("target_path")
|
|
126
148
|
|
|
127
149
|
|
|
128
|
-
# TODO:
|
|
150
|
+
# TODO: Remove once data migration v5 is obsolete
|
|
129
151
|
def is_legacy_artifact(artifact):
|
|
130
152
|
if isinstance(artifact, dict):
|
|
131
153
|
return "metadata" not in artifact
|
|
@@ -167,6 +189,7 @@ class RunKeys:
|
|
|
167
189
|
inputs = "inputs"
|
|
168
190
|
returns = "returns"
|
|
169
191
|
artifacts = "artifacts"
|
|
192
|
+
artifact_uris = "artifact_uris"
|
|
170
193
|
outputs = "outputs"
|
|
171
194
|
data_stores = "data_stores"
|
|
172
195
|
secrets = "secret_sources"
|
|
@@ -220,7 +243,7 @@ def verify_field_regex(
|
|
|
220
243
|
|
|
221
244
|
|
|
222
245
|
def validate_builder_source(
|
|
223
|
-
source: str, pull_at_runtime: bool = False, workdir: str = None
|
|
246
|
+
source: str, pull_at_runtime: bool = False, workdir: Optional[str] = None
|
|
224
247
|
):
|
|
225
248
|
if pull_at_runtime or not source:
|
|
226
249
|
return
|
|
@@ -268,12 +291,14 @@ def validate_tag_name(
|
|
|
268
291
|
def validate_artifact_key_name(
|
|
269
292
|
artifact_key: str, field_name: str, raise_on_failure: bool = True
|
|
270
293
|
) -> bool:
|
|
294
|
+
field_type = "key" if field_name == "artifact.key" else "db_key"
|
|
271
295
|
return mlrun.utils.helpers.verify_field_regex(
|
|
272
296
|
field_name,
|
|
273
297
|
artifact_key,
|
|
274
298
|
mlrun.utils.regex.artifact_key,
|
|
275
299
|
raise_on_failure=raise_on_failure,
|
|
276
|
-
log_message="
|
|
300
|
+
log_message=f"Artifact {field_type} must start and end with an alphanumeric character, and may only contain "
|
|
301
|
+
"letters, numbers, hyphens, underscores, and dots.",
|
|
277
302
|
)
|
|
278
303
|
|
|
279
304
|
|
|
@@ -354,8 +379,8 @@ def verify_field_list_of_type(
|
|
|
354
379
|
def verify_dict_items_type(
|
|
355
380
|
name: str,
|
|
356
381
|
dictionary: dict,
|
|
357
|
-
expected_keys_types: list = None,
|
|
358
|
-
expected_values_types: list = None,
|
|
382
|
+
expected_keys_types: Optional[list] = None,
|
|
383
|
+
expected_values_types: Optional[list] = None,
|
|
359
384
|
):
|
|
360
385
|
if dictionary:
|
|
361
386
|
if not isinstance(dictionary, dict):
|
|
@@ -372,7 +397,7 @@ def verify_dict_items_type(
|
|
|
372
397
|
) from exc
|
|
373
398
|
|
|
374
399
|
|
|
375
|
-
def verify_list_items_type(list_, expected_types: list = None):
|
|
400
|
+
def verify_list_items_type(list_, expected_types: Optional[list] = None):
|
|
376
401
|
if list_ and expected_types:
|
|
377
402
|
list_items_types = set(map(type, list_))
|
|
378
403
|
expected_types = set(expected_types)
|
|
@@ -396,6 +421,32 @@ def now_date(tz: timezone = timezone.utc) -> datetime:
|
|
|
396
421
|
return datetime.now(tz=tz)
|
|
397
422
|
|
|
398
423
|
|
|
424
|
+
def datetime_to_mysql_ts(datetime_object: datetime) -> datetime:
|
|
425
|
+
"""
|
|
426
|
+
Convert a Python datetime object to a MySQL-compatible timestamp string,
|
|
427
|
+
rounded to the nearest millisecond.
|
|
428
|
+
Example: 2024-12-18T16:36:05.235687+00:00 -> 2024-12-18T16:36:05.236000
|
|
429
|
+
|
|
430
|
+
:param datetime_object: A Python datetime object.
|
|
431
|
+
|
|
432
|
+
:return: A MySQL-compatible timestamp string with millisecond precision.
|
|
433
|
+
"""
|
|
434
|
+
if not datetime_object.tzinfo:
|
|
435
|
+
datetime_object = datetime_object.replace(tzinfo=timezone.utc)
|
|
436
|
+
|
|
437
|
+
# Round to the nearest millisecond
|
|
438
|
+
ms = round(datetime_object.microsecond / 1000) * 1000
|
|
439
|
+
if ms == 1000000:
|
|
440
|
+
datetime_object += timedelta(seconds=1)
|
|
441
|
+
ms = 0
|
|
442
|
+
|
|
443
|
+
return datetime_object.replace(microsecond=ms)
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
def datetime_min(tz: timezone = timezone.utc) -> datetime:
|
|
447
|
+
return datetime(1970, 1, 1, tzinfo=tz)
|
|
448
|
+
|
|
449
|
+
|
|
399
450
|
datetime_now = now_date
|
|
400
451
|
|
|
401
452
|
|
|
@@ -448,7 +499,6 @@ def get_in(obj, keys, default=None):
|
|
|
448
499
|
"""
|
|
449
500
|
if isinstance(keys, str):
|
|
450
501
|
keys = keys.split(".")
|
|
451
|
-
|
|
452
502
|
for key in keys:
|
|
453
503
|
if not obj or key not in obj:
|
|
454
504
|
return default
|
|
@@ -663,8 +713,8 @@ def dict_to_json(struct):
|
|
|
663
713
|
|
|
664
714
|
def parse_artifact_uri(uri, default_project=""):
|
|
665
715
|
"""
|
|
666
|
-
Parse artifact URI into project, key, tag, iter, tree
|
|
667
|
-
URI format: [<project>/]<key>[#<iter>][:<tag>][@<tree>]
|
|
716
|
+
Parse artifact URI into project, key, tag, iter, tree, uid
|
|
717
|
+
URI format: [<project>/]<key>[#<iter>][:<tag>][@<tree>][^<uid>]
|
|
668
718
|
|
|
669
719
|
:param uri: uri to parse
|
|
670
720
|
:param default_project: default project name if not in URI
|
|
@@ -674,6 +724,7 @@ def parse_artifact_uri(uri, default_project=""):
|
|
|
674
724
|
[2] = iteration
|
|
675
725
|
[3] = tag
|
|
676
726
|
[4] = tree
|
|
727
|
+
[5] = uid
|
|
677
728
|
"""
|
|
678
729
|
uri_pattern = mlrun.utils.regex.artifact_uri_pattern
|
|
679
730
|
match = re.match(uri_pattern, uri)
|
|
@@ -698,6 +749,7 @@ def parse_artifact_uri(uri, default_project=""):
|
|
|
698
749
|
iteration,
|
|
699
750
|
group_dict["tag"],
|
|
700
751
|
group_dict["tree"],
|
|
752
|
+
group_dict["uid"],
|
|
701
753
|
)
|
|
702
754
|
|
|
703
755
|
|
|
@@ -712,7 +764,9 @@ def generate_object_uri(project, name, tag=None, hash_key=None):
|
|
|
712
764
|
return uri
|
|
713
765
|
|
|
714
766
|
|
|
715
|
-
def generate_artifact_uri(
|
|
767
|
+
def generate_artifact_uri(
|
|
768
|
+
project, key, tag=None, iter=None, tree=None, uid=None
|
|
769
|
+
) -> str:
|
|
716
770
|
artifact_uri = f"{project}/{key}"
|
|
717
771
|
if iter is not None:
|
|
718
772
|
artifact_uri = f"{artifact_uri}#{iter}"
|
|
@@ -720,6 +774,8 @@ def generate_artifact_uri(project, key, tag=None, iter=None, tree=None):
|
|
|
720
774
|
artifact_uri = f"{artifact_uri}:{tag}"
|
|
721
775
|
if tree is not None:
|
|
722
776
|
artifact_uri = f"{artifact_uri}@{tree}"
|
|
777
|
+
if uid is not None:
|
|
778
|
+
artifact_uri = f"{artifact_uri}^{uid}"
|
|
723
779
|
return artifact_uri
|
|
724
780
|
|
|
725
781
|
|
|
@@ -816,7 +872,9 @@ def _convert_python_package_version_to_image_tag(version: typing.Optional[str]):
|
|
|
816
872
|
|
|
817
873
|
|
|
818
874
|
def enrich_image_url(
|
|
819
|
-
image_url: str,
|
|
875
|
+
image_url: str,
|
|
876
|
+
client_version: Optional[str] = None,
|
|
877
|
+
client_python_version: Optional[str] = None,
|
|
820
878
|
) -> str:
|
|
821
879
|
client_version = _convert_python_package_version_to_image_tag(client_version)
|
|
822
880
|
server_version = _convert_python_package_version_to_image_tag(
|
|
@@ -856,7 +914,7 @@ def enrich_image_url(
|
|
|
856
914
|
|
|
857
915
|
|
|
858
916
|
def resolve_image_tag_suffix(
|
|
859
|
-
mlrun_version: str = None, python_version: str = None
|
|
917
|
+
mlrun_version: Optional[str] = None, python_version: Optional[str] = None
|
|
860
918
|
) -> str:
|
|
861
919
|
"""
|
|
862
920
|
resolves what suffix should be appended to the image tag
|
|
@@ -989,49 +1047,165 @@ async def retry_until_successful_async(
|
|
|
989
1047
|
).run()
|
|
990
1048
|
|
|
991
1049
|
|
|
992
|
-
def
|
|
993
|
-
|
|
1050
|
+
def get_project_url(project: str) -> str:
|
|
1051
|
+
"""
|
|
1052
|
+
Generate the base URL for a given project.
|
|
1053
|
+
|
|
1054
|
+
:param project: The project name.
|
|
1055
|
+
:return: The base URL for the project, or an empty string if the base URL is not resolved.
|
|
1056
|
+
"""
|
|
994
1057
|
if mlrun.mlconf.resolve_ui_url():
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
1058
|
+
return f"{mlrun.mlconf.resolve_ui_url()}/{mlrun.mlconf.ui.projects_prefix}/{project}"
|
|
1059
|
+
return ""
|
|
1060
|
+
|
|
1061
|
+
|
|
1062
|
+
def get_run_url(project: str, uid: str, name: str) -> str:
|
|
1063
|
+
"""
|
|
1064
|
+
Generate the URL for a specific run.
|
|
1065
|
+
|
|
1066
|
+
:param project: The project name.
|
|
1067
|
+
:param uid: The run UID.
|
|
1068
|
+
:param name: The run name.
|
|
1069
|
+
:return: The URL for the run, or an empty string if the base URL is not resolved.
|
|
1070
|
+
"""
|
|
1071
|
+
runs_url = get_runs_url(project)
|
|
1072
|
+
if not runs_url:
|
|
1073
|
+
return ""
|
|
1074
|
+
return f"{runs_url}/monitor-jobs/{name}/{uid}/overview"
|
|
1075
|
+
|
|
1076
|
+
|
|
1077
|
+
def get_runs_url(project: str) -> str:
|
|
1078
|
+
"""
|
|
1079
|
+
Generate the URL for the runs of a given project.
|
|
1080
|
+
|
|
1081
|
+
:param project: The project name.
|
|
1082
|
+
:return: The URL for the runs, or an empty string if the base URL is not resolved.
|
|
1083
|
+
"""
|
|
1084
|
+
base_url = get_project_url(project)
|
|
1085
|
+
if not base_url:
|
|
1086
|
+
return ""
|
|
1087
|
+
return f"{base_url}/jobs"
|
|
1088
|
+
|
|
1089
|
+
|
|
1090
|
+
def get_model_endpoint_url(
|
|
1091
|
+
project: str,
|
|
1092
|
+
model_name: Optional[str] = None,
|
|
1093
|
+
model_endpoint_id: Optional[str] = None,
|
|
1094
|
+
) -> str:
|
|
1095
|
+
"""
|
|
1096
|
+
Generate the URL for a specific model endpoint.
|
|
1097
|
+
|
|
1098
|
+
:param project: The project name.
|
|
1099
|
+
:param model_name: The model name.
|
|
1100
|
+
:param model_endpoint_id: The model endpoint ID.
|
|
1101
|
+
:return: The URL for the model endpoint, or an empty string if the base URL is not resolved.
|
|
1102
|
+
"""
|
|
1103
|
+
base_url = get_project_url(project)
|
|
1104
|
+
if not base_url:
|
|
1105
|
+
return ""
|
|
1106
|
+
url = f"{base_url}/models"
|
|
1107
|
+
if model_name and model_endpoint_id:
|
|
1108
|
+
url += f"/model-endpoints/{model_name}/{model_endpoint_id}/overview"
|
|
998
1109
|
return url
|
|
999
1110
|
|
|
1000
1111
|
|
|
1001
|
-
def
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1112
|
+
def get_workflow_url(
|
|
1113
|
+
project: str,
|
|
1114
|
+
id: Optional[str] = None,
|
|
1115
|
+
) -> str:
|
|
1116
|
+
"""
|
|
1117
|
+
Generate the URL for a specific workflow.
|
|
1118
|
+
|
|
1119
|
+
:param project: The project name.
|
|
1120
|
+
:param id: The workflow ID.
|
|
1121
|
+
:return: The URL for the workflow, or an empty string if the base URL is not resolved.
|
|
1122
|
+
"""
|
|
1123
|
+
base_url = get_project_url(project)
|
|
1124
|
+
if not base_url:
|
|
1125
|
+
return ""
|
|
1126
|
+
url = f"{base_url}/jobs/monitor-workflows/workflow"
|
|
1127
|
+
if id:
|
|
1128
|
+
url += f"/{id}"
|
|
1007
1129
|
return url
|
|
1008
1130
|
|
|
1009
1131
|
|
|
1010
|
-
def
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1132
|
+
def get_kfp_list_runs_filter(
|
|
1133
|
+
project_name: Optional[str] = None,
|
|
1134
|
+
end_date: Optional[str] = None,
|
|
1135
|
+
start_date: Optional[str] = None,
|
|
1136
|
+
) -> str:
|
|
1137
|
+
"""
|
|
1138
|
+
Generates a filter for listing Kubeflow Pipelines (KFP) runs.
|
|
1139
|
+
|
|
1140
|
+
:param project_name: The name of the project. If "*", it won't filter by project.
|
|
1141
|
+
:param end_date: The latest creation date for filtering runs (ISO 8601 format).
|
|
1142
|
+
:param start_date: The earliest creation date for filtering runs (ISO 8601 format).
|
|
1143
|
+
:return: A JSON-formatted filter string for KFP.
|
|
1144
|
+
"""
|
|
1145
|
+
|
|
1146
|
+
# KFP filter operation codes
|
|
1147
|
+
kfp_less_than_or_equal_op = 7 # '<='
|
|
1148
|
+
kfp_greater_than_or_equal_op = 5 # '>='
|
|
1149
|
+
kfp_substring_op = 9 # Substring match
|
|
1150
|
+
|
|
1151
|
+
filters = {"predicates": []}
|
|
1152
|
+
|
|
1153
|
+
if end_date:
|
|
1154
|
+
filters["predicates"].append(
|
|
1155
|
+
{
|
|
1156
|
+
"key": "created_at",
|
|
1157
|
+
"op": kfp_less_than_or_equal_op,
|
|
1158
|
+
"timestamp_value": end_date,
|
|
1159
|
+
}
|
|
1016
1160
|
)
|
|
1017
|
-
|
|
1161
|
+
|
|
1162
|
+
if project_name and project_name != "*":
|
|
1163
|
+
filters["predicates"].append(
|
|
1164
|
+
{
|
|
1165
|
+
"key": "name",
|
|
1166
|
+
"op": kfp_substring_op,
|
|
1167
|
+
"string_value": project_name,
|
|
1168
|
+
}
|
|
1169
|
+
)
|
|
1170
|
+
if start_date:
|
|
1171
|
+
filters["predicates"].append(
|
|
1172
|
+
{
|
|
1173
|
+
"key": "created_at",
|
|
1174
|
+
"op": kfp_greater_than_or_equal_op,
|
|
1175
|
+
"timestamp_value": start_date,
|
|
1176
|
+
}
|
|
1177
|
+
)
|
|
1178
|
+
return json.dumps(filters)
|
|
1018
1179
|
|
|
1019
1180
|
|
|
1020
|
-
def
|
|
1181
|
+
def validate_and_convert_date(date_input: str) -> str:
|
|
1021
1182
|
"""
|
|
1022
|
-
|
|
1023
|
-
|
|
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.
|
|
1183
|
+
Converts any recognizable date string into a standardized RFC 3339 format.
|
|
1184
|
+
:param date_input: A date string in a recognizable format.
|
|
1027
1185
|
"""
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1186
|
+
try:
|
|
1187
|
+
dt_object = parser.parse(date_input)
|
|
1188
|
+
if dt_object.tzinfo is not None:
|
|
1189
|
+
# Convert to UTC if it's in a different timezone
|
|
1190
|
+
dt_object = dt_object.astimezone(pytz.utc)
|
|
1191
|
+
else:
|
|
1192
|
+
# If no timezone info is present, assume it's in local time
|
|
1193
|
+
local_tz = pytz.timezone("UTC")
|
|
1194
|
+
dt_object = local_tz.localize(dt_object)
|
|
1195
|
+
|
|
1196
|
+
# Convert the datetime object to an RFC 3339-compliant string.
|
|
1197
|
+
# RFC 3339 requires timestamps to be in ISO 8601 format with a 'Z' suffix for UTC time.
|
|
1198
|
+
# The isoformat() method adds a "+00:00" suffix for UTC by default,
|
|
1199
|
+
# so we replace it with "Z" to ensure compliance.
|
|
1200
|
+
formatted_date = dt_object.isoformat().replace("+00:00", "Z")
|
|
1201
|
+
formatted_date = formatted_date.rstrip("Z") + "Z"
|
|
1202
|
+
|
|
1203
|
+
return formatted_date
|
|
1204
|
+
except (ValueError, OverflowError) as e:
|
|
1205
|
+
raise ValueError(
|
|
1206
|
+
f"Invalid date format: {date_input}."
|
|
1207
|
+
f" Date format must adhere to the RFC 3339 standard (e.g., 'YYYY-MM-DDTHH:MM:SSZ' for UTC)."
|
|
1208
|
+
) from e
|
|
1035
1209
|
|
|
1036
1210
|
|
|
1037
1211
|
def are_strings_in_exception_chain_messages(
|
|
@@ -1175,7 +1349,7 @@ def get_function(function, namespaces, reload_modules: bool = False):
|
|
|
1175
1349
|
def get_handler_extended(
|
|
1176
1350
|
handler_path: str,
|
|
1177
1351
|
context=None,
|
|
1178
|
-
class_args: dict = None,
|
|
1352
|
+
class_args: Optional[dict] = None,
|
|
1179
1353
|
namespaces=None,
|
|
1180
1354
|
reload_modules: bool = False,
|
|
1181
1355
|
):
|
|
@@ -1217,7 +1391,11 @@ def get_handler_extended(
|
|
|
1217
1391
|
def datetime_from_iso(time_str: str) -> Optional[datetime]:
|
|
1218
1392
|
if not time_str:
|
|
1219
1393
|
return
|
|
1220
|
-
|
|
1394
|
+
dt = parser.isoparse(time_str)
|
|
1395
|
+
if dt.tzinfo is None:
|
|
1396
|
+
dt = dt.replace(tzinfo=timezone.utc)
|
|
1397
|
+
# ensure the datetime is in UTC, converting if necessary
|
|
1398
|
+
return dt.astimezone(timezone.utc)
|
|
1221
1399
|
|
|
1222
1400
|
|
|
1223
1401
|
def datetime_to_iso(time_obj: Optional[datetime]) -> Optional[str]:
|
|
@@ -1256,6 +1434,21 @@ def has_timezone(timestamp):
|
|
|
1256
1434
|
return False
|
|
1257
1435
|
|
|
1258
1436
|
|
|
1437
|
+
def format_datetime(dt: datetime, fmt: Optional[str] = None) -> str:
|
|
1438
|
+
if dt is None:
|
|
1439
|
+
return ""
|
|
1440
|
+
|
|
1441
|
+
# If the datetime is naive
|
|
1442
|
+
if dt.tzinfo is None:
|
|
1443
|
+
dt = dt.replace(tzinfo=timezone.utc)
|
|
1444
|
+
|
|
1445
|
+
# TODO: Once Python 3.12 is the minimal version, use %:z to format the timezone offset with a colon
|
|
1446
|
+
formatted_time = dt.strftime(fmt or "%Y-%m-%d %H:%M:%S.%f%z")
|
|
1447
|
+
|
|
1448
|
+
# For versions earlier than Python 3.12, we manually insert the colon in the timezone offset
|
|
1449
|
+
return formatted_time[:-2] + ":" + formatted_time[-2:]
|
|
1450
|
+
|
|
1451
|
+
|
|
1259
1452
|
def as_list(element: Any) -> list[Any]:
|
|
1260
1453
|
return element if isinstance(element, list) else [element]
|
|
1261
1454
|
|
|
@@ -1309,6 +1502,17 @@ def to_non_empty_values_dict(input_dict: dict) -> dict:
|
|
|
1309
1502
|
return {key: value for key, value in input_dict.items() if value}
|
|
1310
1503
|
|
|
1311
1504
|
|
|
1505
|
+
def get_enriched_gpu_limits(function_limits: dict) -> dict[str, int]:
|
|
1506
|
+
"""
|
|
1507
|
+
Creates new limits containing the GPU-related limits from the function's limits,
|
|
1508
|
+
mapping each to zero. This is used for pods like Kaniko and Argo pods, which inherit
|
|
1509
|
+
GPU-related selectors but do not require GPU resources. By setting these
|
|
1510
|
+
limits to zero, the pods receive the necessary tolerations from the cloud provider for scheduling,
|
|
1511
|
+
without actually consuming GPU resources.
|
|
1512
|
+
"""
|
|
1513
|
+
return {resource: 0 for resource in function_limits if "/gpu" in resource.lower()}
|
|
1514
|
+
|
|
1515
|
+
|
|
1312
1516
|
def str_to_timestamp(time_str: str, now_time: Timestamp = None):
|
|
1313
1517
|
"""convert fixed/relative time string to Pandas Timestamp
|
|
1314
1518
|
|
|
@@ -1347,6 +1551,16 @@ def str_to_timestamp(time_str: str, now_time: Timestamp = None):
|
|
|
1347
1551
|
return Timestamp(time_str)
|
|
1348
1552
|
|
|
1349
1553
|
|
|
1554
|
+
def str_to_bool(value: str) -> bool:
|
|
1555
|
+
"""Convert a string to a boolean value."""
|
|
1556
|
+
value = value.lower()
|
|
1557
|
+
if value in ("true", "1", "t", "y", "yes", "on"):
|
|
1558
|
+
return True
|
|
1559
|
+
if value in ("false", "0", "f", "n", "no", "off"):
|
|
1560
|
+
return False
|
|
1561
|
+
raise ValueError(f"invalid boolean value: {value}")
|
|
1562
|
+
|
|
1563
|
+
|
|
1350
1564
|
def is_link_artifact(artifact):
|
|
1351
1565
|
if isinstance(artifact, dict):
|
|
1352
1566
|
return (
|
|
@@ -1625,7 +1839,9 @@ setting partitioned=False"""
|
|
|
1625
1839
|
|
|
1626
1840
|
def is_ecr_url(registry: str) -> bool:
|
|
1627
1841
|
# example URL: <aws_account_id>.dkr.ecr.<region>.amazonaws.com
|
|
1628
|
-
|
|
1842
|
+
parsed_url = urlparse(f"https://{registry}")
|
|
1843
|
+
hostname = parsed_url.hostname
|
|
1844
|
+
return hostname and ".ecr." in hostname and hostname.endswith(".amazonaws.com")
|
|
1629
1845
|
|
|
1630
1846
|
|
|
1631
1847
|
def get_local_file_schema() -> list:
|
|
@@ -1660,7 +1876,14 @@ def get_serving_spec():
|
|
|
1660
1876
|
raise mlrun.errors.MLRunInvalidArgumentError(
|
|
1661
1877
|
"Failed to find serving spec in env var or config file"
|
|
1662
1878
|
)
|
|
1663
|
-
|
|
1879
|
+
# Attempt to decode and decompress, or use as-is for backward compatibility
|
|
1880
|
+
try:
|
|
1881
|
+
decoded_data = base64.b64decode(data)
|
|
1882
|
+
decompressed_data = gzip.decompress(decoded_data)
|
|
1883
|
+
spec = json.loads(decompressed_data.decode("utf-8"))
|
|
1884
|
+
except (OSError, gzip.BadGzipFile, base64.binascii.Error, json.JSONDecodeError):
|
|
1885
|
+
spec = json.loads(data)
|
|
1886
|
+
|
|
1664
1887
|
return spec
|
|
1665
1888
|
|
|
1666
1889
|
|
|
@@ -1694,17 +1917,22 @@ def merge_dicts_with_precedence(*dicts: dict) -> dict:
|
|
|
1694
1917
|
|
|
1695
1918
|
|
|
1696
1919
|
def validate_component_version_compatibility(
|
|
1697
|
-
component_name: typing.Literal["iguazio", "nuclio"],
|
|
1920
|
+
component_name: typing.Literal["iguazio", "nuclio", "mlrun-client"],
|
|
1921
|
+
*min_versions: str,
|
|
1922
|
+
mlrun_client_version: Optional[str] = None,
|
|
1698
1923
|
):
|
|
1699
1924
|
"""
|
|
1700
1925
|
:param component_name: Name of the component to validate compatibility for.
|
|
1701
1926
|
:param min_versions: Valid minimum version(s) required, assuming no 2 versions has equal major and minor.
|
|
1927
|
+
:param mlrun_client_version: Client version to validate when component_name is "mlrun-client".
|
|
1702
1928
|
"""
|
|
1703
1929
|
parsed_min_versions = [
|
|
1704
1930
|
semver.VersionInfo.parse(min_version) for min_version in min_versions
|
|
1705
1931
|
]
|
|
1706
1932
|
parsed_current_version = None
|
|
1707
1933
|
component_current_version = None
|
|
1934
|
+
# For mlrun client we don't assume compatability if we fail to parse the client version
|
|
1935
|
+
assume_compatible = component_name not in ["mlrun-client"]
|
|
1708
1936
|
try:
|
|
1709
1937
|
if component_name == "iguazio":
|
|
1710
1938
|
component_current_version = mlrun.mlconf.igz_version
|
|
@@ -1721,18 +1949,29 @@ def validate_component_version_compatibility(
|
|
|
1721
1949
|
parsed_current_version = semver.VersionInfo.parse(
|
|
1722
1950
|
mlrun.mlconf.nuclio_version
|
|
1723
1951
|
)
|
|
1952
|
+
if component_name == "mlrun-client":
|
|
1953
|
+
# dev version, assume compatible
|
|
1954
|
+
if mlrun_client_version and (
|
|
1955
|
+
mlrun_client_version.startswith("0.0.0+")
|
|
1956
|
+
or "unstable" in mlrun_client_version
|
|
1957
|
+
):
|
|
1958
|
+
return True
|
|
1959
|
+
|
|
1960
|
+
component_current_version = mlrun_client_version
|
|
1961
|
+
parsed_current_version = semver.Version.parse(mlrun_client_version)
|
|
1724
1962
|
if not parsed_current_version:
|
|
1725
|
-
return
|
|
1963
|
+
return assume_compatible
|
|
1726
1964
|
except ValueError:
|
|
1727
1965
|
# only log when version is set but invalid
|
|
1728
1966
|
if component_current_version:
|
|
1729
1967
|
logger.warning(
|
|
1730
|
-
"Unable to parse current version
|
|
1968
|
+
"Unable to parse current version",
|
|
1731
1969
|
component_name=component_name,
|
|
1732
1970
|
current_version=component_current_version,
|
|
1733
1971
|
min_versions=min_versions,
|
|
1972
|
+
assume_compatible=assume_compatible,
|
|
1734
1973
|
)
|
|
1735
|
-
return
|
|
1974
|
+
return assume_compatible
|
|
1736
1975
|
|
|
1737
1976
|
# Feature might have been back-ported e.g. nuclio node selection is supported from
|
|
1738
1977
|
# 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
|
|
@@ -1797,9 +2036,8 @@ def _reload(module, max_recursion_depth):
|
|
|
1797
2036
|
def run_with_retry(
|
|
1798
2037
|
retry_count: int,
|
|
1799
2038
|
func: typing.Callable,
|
|
1800
|
-
retry_on_exceptions:
|
|
1801
|
-
type[Exception],
|
|
1802
|
-
tuple[type[Exception]],
|
|
2039
|
+
retry_on_exceptions: Optional[
|
|
2040
|
+
typing.Union[type[Exception], tuple[type[Exception]]]
|
|
1803
2041
|
] = None,
|
|
1804
2042
|
*args,
|
|
1805
2043
|
**kwargs,
|
|
@@ -1832,3 +2070,218 @@ def run_with_retry(
|
|
|
1832
2070
|
if attempt == retry_count:
|
|
1833
2071
|
raise
|
|
1834
2072
|
raise last_exception
|
|
2073
|
+
|
|
2074
|
+
|
|
2075
|
+
def join_urls(base_url: Optional[str], path: Optional[str]) -> str:
|
|
2076
|
+
"""
|
|
2077
|
+
Joins a base URL with a path, ensuring proper handling of slashes.
|
|
2078
|
+
|
|
2079
|
+
:param base_url: The base URL (e.g., "http://example.com").
|
|
2080
|
+
:param path: The path to append to the base URL (e.g., "/path/to/resource").
|
|
2081
|
+
|
|
2082
|
+
:return: A unified URL with exactly one slash between base_url and path.
|
|
2083
|
+
"""
|
|
2084
|
+
if base_url is None:
|
|
2085
|
+
base_url = ""
|
|
2086
|
+
return f"{base_url.rstrip('/')}/{path.lstrip('/')}" if path else base_url
|
|
2087
|
+
|
|
2088
|
+
|
|
2089
|
+
class Workflow:
|
|
2090
|
+
@staticmethod
|
|
2091
|
+
def get_workflow_steps(
|
|
2092
|
+
db: "mlrun.db.RunDBInterface", workflow_id: str, project: str
|
|
2093
|
+
) -> list:
|
|
2094
|
+
steps = []
|
|
2095
|
+
|
|
2096
|
+
def _add_run_step(_step: mlrun_pipelines.models.PipelineStep):
|
|
2097
|
+
# on kfp 1.8 argo sets the pod hostname differently than what we have with kfp 2.5
|
|
2098
|
+
# therefore, the heuristic needs to change. what we do here is first trying against 1.8 conventions
|
|
2099
|
+
# and if we can't find it then falling back to 2.5
|
|
2100
|
+
try:
|
|
2101
|
+
# runner_pod = x-y-N
|
|
2102
|
+
_runs = db.list_runs(
|
|
2103
|
+
project=project,
|
|
2104
|
+
labels=f"{mlrun_constants.MLRunInternalLabels.runner_pod}={_step.node_name}",
|
|
2105
|
+
)
|
|
2106
|
+
if not _runs:
|
|
2107
|
+
try:
|
|
2108
|
+
# x-y-N -> x-y, N
|
|
2109
|
+
node_name_initials, node_name_generated_id = (
|
|
2110
|
+
_step.node_name.rsplit("-", 1)
|
|
2111
|
+
)
|
|
2112
|
+
|
|
2113
|
+
except ValueError:
|
|
2114
|
+
# defensive programming, if the node name is not in the expected format
|
|
2115
|
+
node_name_initials = _step.node_name
|
|
2116
|
+
node_name_generated_id = ""
|
|
2117
|
+
|
|
2118
|
+
# compile the expected runner pod hostname as per kfp >= 2.4
|
|
2119
|
+
# x-y, Z, N -> runner_pod = x-y-Z-N
|
|
2120
|
+
runner_pod_value = "-".join(
|
|
2121
|
+
[
|
|
2122
|
+
node_name_initials,
|
|
2123
|
+
_step.display_name,
|
|
2124
|
+
node_name_generated_id,
|
|
2125
|
+
]
|
|
2126
|
+
).rstrip("-")
|
|
2127
|
+
logger.debug(
|
|
2128
|
+
"No run found for step, trying with different node name",
|
|
2129
|
+
step_node_name=runner_pod_value,
|
|
2130
|
+
)
|
|
2131
|
+
_runs = db.list_runs(
|
|
2132
|
+
project=project,
|
|
2133
|
+
labels=f"{mlrun_constants.MLRunInternalLabels.runner_pod}={runner_pod_value}",
|
|
2134
|
+
)
|
|
2135
|
+
|
|
2136
|
+
_run = _runs[0]
|
|
2137
|
+
except IndexError:
|
|
2138
|
+
logger.warning("No run found for step", step=_step.to_dict())
|
|
2139
|
+
_run = {
|
|
2140
|
+
"metadata": {
|
|
2141
|
+
"name": _step.display_name,
|
|
2142
|
+
"project": project,
|
|
2143
|
+
},
|
|
2144
|
+
"status": {},
|
|
2145
|
+
}
|
|
2146
|
+
_run["step_kind"] = _step.step_type
|
|
2147
|
+
if _step.skipped:
|
|
2148
|
+
_run.setdefault("status", {})["state"] = (
|
|
2149
|
+
runtimes_constants.RunStates.skipped
|
|
2150
|
+
)
|
|
2151
|
+
steps.append(_run)
|
|
2152
|
+
|
|
2153
|
+
def _add_deploy_function_step(_step: mlrun_pipelines.models.PipelineStep):
|
|
2154
|
+
project, name, hash_key = Workflow._extract_function_uri(
|
|
2155
|
+
_step.get_annotation("mlrun/function-uri")
|
|
2156
|
+
)
|
|
2157
|
+
if name:
|
|
2158
|
+
try:
|
|
2159
|
+
function = db.get_function(
|
|
2160
|
+
project=project, name=name, hash_key=hash_key
|
|
2161
|
+
)
|
|
2162
|
+
except mlrun.errors.MLRunNotFoundError:
|
|
2163
|
+
# If the function is not found (if build failed for example), we will create a dummy
|
|
2164
|
+
# function object for the notification to display the function name
|
|
2165
|
+
function = {
|
|
2166
|
+
"metadata": {
|
|
2167
|
+
"name": name,
|
|
2168
|
+
"project": project,
|
|
2169
|
+
"hash_key": hash_key,
|
|
2170
|
+
},
|
|
2171
|
+
}
|
|
2172
|
+
pod_phase = _step.phase
|
|
2173
|
+
if _step.skipped:
|
|
2174
|
+
state = mlrun.common.schemas.FunctionState.skipped
|
|
2175
|
+
else:
|
|
2176
|
+
state = runtimes_constants.PodPhases.pod_phase_to_run_state(
|
|
2177
|
+
pod_phase
|
|
2178
|
+
)
|
|
2179
|
+
function["status"] = {"state": state}
|
|
2180
|
+
if isinstance(function["metadata"].get("updated"), datetime):
|
|
2181
|
+
function["metadata"]["updated"] = function["metadata"][
|
|
2182
|
+
"updated"
|
|
2183
|
+
].isoformat()
|
|
2184
|
+
function["step_kind"] = _step.step_type
|
|
2185
|
+
steps.append(function)
|
|
2186
|
+
|
|
2187
|
+
step_methods = {
|
|
2188
|
+
mlrun_pipelines.common.constants.PipelineRunType.run: _add_run_step,
|
|
2189
|
+
mlrun_pipelines.common.constants.PipelineRunType.build: _add_deploy_function_step,
|
|
2190
|
+
mlrun_pipelines.common.constants.PipelineRunType.deploy: _add_deploy_function_step,
|
|
2191
|
+
}
|
|
2192
|
+
|
|
2193
|
+
if not workflow_id:
|
|
2194
|
+
return steps
|
|
2195
|
+
|
|
2196
|
+
try:
|
|
2197
|
+
workflow_manifest = Workflow._get_workflow_manifest(workflow_id)
|
|
2198
|
+
except Exception:
|
|
2199
|
+
logger.warning(
|
|
2200
|
+
"Failed to extract workflow steps from workflow manifest, "
|
|
2201
|
+
"returning all runs with the workflow id label",
|
|
2202
|
+
workflow_id=workflow_id,
|
|
2203
|
+
traceback=traceback.format_exc(),
|
|
2204
|
+
)
|
|
2205
|
+
return db.list_runs(
|
|
2206
|
+
project=project,
|
|
2207
|
+
labels=f"workflow={workflow_id}",
|
|
2208
|
+
)
|
|
2209
|
+
|
|
2210
|
+
if not workflow_manifest:
|
|
2211
|
+
return steps
|
|
2212
|
+
|
|
2213
|
+
try:
|
|
2214
|
+
for step in workflow_manifest.get_steps():
|
|
2215
|
+
step_method = step_methods.get(step.step_type)
|
|
2216
|
+
if step_method:
|
|
2217
|
+
step_method(step)
|
|
2218
|
+
return steps
|
|
2219
|
+
except Exception:
|
|
2220
|
+
# If we fail to read the pipeline steps, we will return the list of runs that have the same workflow id
|
|
2221
|
+
logger.warning(
|
|
2222
|
+
"Failed to extract workflow steps from workflow manifest, "
|
|
2223
|
+
"returning all runs with the workflow id label",
|
|
2224
|
+
workflow_id=workflow_id,
|
|
2225
|
+
traceback=traceback.format_exc(),
|
|
2226
|
+
)
|
|
2227
|
+
return db.list_runs(
|
|
2228
|
+
project=project,
|
|
2229
|
+
labels=f"workflow={workflow_id}",
|
|
2230
|
+
)
|
|
2231
|
+
|
|
2232
|
+
@staticmethod
|
|
2233
|
+
def _extract_function_uri(function_uri: str) -> tuple[str, str, str]:
|
|
2234
|
+
"""
|
|
2235
|
+
Extract the project, name, and hash key from a function uri.
|
|
2236
|
+
Examples:
|
|
2237
|
+
- "project/name@hash_key" returns project, name, hash_key
|
|
2238
|
+
- "project/name returns" project, name, ""
|
|
2239
|
+
"""
|
|
2240
|
+
project, name, hash_key = None, None, None
|
|
2241
|
+
hashed_pattern = r"^(.+)/(.+)@(.+)$"
|
|
2242
|
+
pattern = r"^(.+)/(.+)$"
|
|
2243
|
+
match = re.match(hashed_pattern, function_uri)
|
|
2244
|
+
if match:
|
|
2245
|
+
project, name, hash_key = match.groups()
|
|
2246
|
+
else:
|
|
2247
|
+
match = re.match(pattern, function_uri)
|
|
2248
|
+
if match:
|
|
2249
|
+
project, name = match.groups()
|
|
2250
|
+
hash_key = ""
|
|
2251
|
+
return project, name, hash_key
|
|
2252
|
+
|
|
2253
|
+
@staticmethod
|
|
2254
|
+
def _get_workflow_manifest(
|
|
2255
|
+
workflow_id: str,
|
|
2256
|
+
) -> typing.Optional[mlrun_pipelines.models.PipelineManifest]:
|
|
2257
|
+
kfp_client = mlrun_pipelines.utils.get_client(mlrun.mlconf.kfp_url)
|
|
2258
|
+
|
|
2259
|
+
# arbitrary timeout of 30 seconds, the workflow should be done by now, however sometimes kfp takes a few
|
|
2260
|
+
# seconds to update the workflow status
|
|
2261
|
+
kfp_run = kfp_client.wait_for_run_completion(workflow_id, 30)
|
|
2262
|
+
if not kfp_run:
|
|
2263
|
+
return None
|
|
2264
|
+
|
|
2265
|
+
kfp_run = mlrun_pipelines.models.PipelineRun(kfp_run)
|
|
2266
|
+
return kfp_run.workflow_manifest()
|
|
2267
|
+
|
|
2268
|
+
|
|
2269
|
+
def as_dict(data: typing.Union[dict, str]) -> dict:
|
|
2270
|
+
if isinstance(data, str):
|
|
2271
|
+
return json.loads(data)
|
|
2272
|
+
return data
|
|
2273
|
+
|
|
2274
|
+
|
|
2275
|
+
def encode_user_code(
|
|
2276
|
+
user_code: typing.Union[str, bytes], max_len_warning: typing.Optional[int] = None
|
|
2277
|
+
) -> str:
|
|
2278
|
+
max_len_warning = max_len_warning or config.function.spec.source_code_max_bytes
|
|
2279
|
+
if isinstance(user_code, str):
|
|
2280
|
+
user_code = user_code.encode("utf-8")
|
|
2281
|
+
encoded = base64.b64encode(user_code).decode("utf-8")
|
|
2282
|
+
if len(encoded) > max_len_warning:
|
|
2283
|
+
logger.warning(
|
|
2284
|
+
f"User code exceeds the maximum allowed size of {max_len_warning} bytes for non remote source. "
|
|
2285
|
+
"Consider using `with_source_archive` to add user code as a remote source to the function."
|
|
2286
|
+
)
|
|
2287
|
+
return encoded
|