mlrun 1.6.4rc8__py3-none-any.whl → 1.7.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 +11 -1
- mlrun/__main__.py +40 -122
- mlrun/alerts/__init__.py +15 -0
- mlrun/alerts/alert.py +248 -0
- mlrun/api/schemas/__init__.py +5 -4
- mlrun/artifacts/__init__.py +8 -3
- mlrun/artifacts/base.py +47 -257
- mlrun/artifacts/dataset.py +11 -192
- mlrun/artifacts/manager.py +79 -47
- mlrun/artifacts/model.py +31 -159
- mlrun/artifacts/plots.py +23 -380
- mlrun/common/constants.py +74 -1
- mlrun/common/db/sql_session.py +5 -5
- mlrun/common/formatters/__init__.py +21 -0
- mlrun/common/formatters/artifact.py +45 -0
- mlrun/common/formatters/base.py +113 -0
- mlrun/common/formatters/feature_set.py +33 -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 +12 -3
- mlrun/common/model_monitoring/helpers.py +9 -5
- mlrun/{runtimes → common/runtimes}/constants.py +37 -9
- mlrun/common/schemas/__init__.py +31 -5
- mlrun/common/schemas/alert.py +202 -0
- mlrun/common/schemas/api_gateway.py +196 -0
- mlrun/common/schemas/artifact.py +25 -4
- mlrun/common/schemas/auth.py +16 -5
- mlrun/common/schemas/background_task.py +1 -1
- mlrun/common/schemas/client_spec.py +4 -2
- mlrun/common/schemas/common.py +7 -4
- mlrun/common/schemas/constants.py +3 -0
- mlrun/common/schemas/feature_store.py +74 -44
- mlrun/common/schemas/frontend_spec.py +15 -7
- mlrun/common/schemas/function.py +12 -1
- mlrun/common/schemas/hub.py +11 -18
- mlrun/common/schemas/memory_reports.py +2 -2
- mlrun/common/schemas/model_monitoring/__init__.py +20 -4
- mlrun/common/schemas/model_monitoring/constants.py +123 -42
- mlrun/common/schemas/model_monitoring/grafana.py +13 -9
- mlrun/common/schemas/model_monitoring/model_endpoints.py +101 -54
- mlrun/common/schemas/notification.py +71 -14
- mlrun/common/schemas/object.py +2 -2
- mlrun/{model_monitoring/controller_handler.py → common/schemas/pagination.py} +9 -12
- mlrun/common/schemas/pipeline.py +8 -1
- mlrun/common/schemas/project.py +69 -18
- mlrun/common/schemas/runs.py +7 -1
- mlrun/common/schemas/runtime_resource.py +8 -12
- mlrun/common/schemas/schedule.py +4 -4
- mlrun/common/schemas/tag.py +1 -2
- mlrun/common/schemas/workflow.py +12 -4
- mlrun/common/types.py +14 -1
- mlrun/config.py +154 -69
- mlrun/data_types/data_types.py +6 -1
- mlrun/data_types/spark.py +2 -2
- mlrun/data_types/to_pandas.py +67 -37
- mlrun/datastore/__init__.py +6 -8
- mlrun/datastore/alibaba_oss.py +131 -0
- mlrun/datastore/azure_blob.py +143 -42
- mlrun/datastore/base.py +102 -58
- mlrun/datastore/datastore.py +34 -13
- mlrun/datastore/datastore_profile.py +146 -20
- mlrun/datastore/dbfs_store.py +3 -7
- mlrun/datastore/filestore.py +1 -4
- mlrun/datastore/google_cloud_storage.py +97 -33
- mlrun/datastore/hdfs.py +56 -0
- mlrun/datastore/inmem.py +6 -3
- mlrun/datastore/redis.py +7 -2
- mlrun/datastore/s3.py +34 -12
- mlrun/datastore/snowflake_utils.py +45 -0
- mlrun/datastore/sources.py +303 -111
- mlrun/datastore/spark_utils.py +31 -2
- mlrun/datastore/store_resources.py +9 -7
- mlrun/datastore/storeytargets.py +151 -0
- mlrun/datastore/targets.py +453 -176
- mlrun/datastore/utils.py +72 -58
- mlrun/datastore/v3io.py +6 -1
- mlrun/db/base.py +274 -41
- mlrun/db/factory.py +1 -1
- mlrun/db/httpdb.py +893 -225
- mlrun/db/nopdb.py +291 -33
- mlrun/errors.py +36 -6
- mlrun/execution.py +115 -42
- mlrun/feature_store/__init__.py +0 -2
- mlrun/feature_store/api.py +65 -73
- mlrun/feature_store/common.py +7 -12
- mlrun/feature_store/feature_set.py +76 -55
- mlrun/feature_store/feature_vector.py +39 -31
- mlrun/feature_store/ingestion.py +7 -6
- mlrun/feature_store/retrieval/base.py +16 -11
- 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 +45 -34
- mlrun/features.py +11 -21
- mlrun/frameworks/_common/artifacts_library.py +9 -9
- mlrun/frameworks/_common/mlrun_interface.py +5 -5
- mlrun/frameworks/_common/model_handler.py +48 -48
- mlrun/frameworks/_common/plan.py +5 -6
- mlrun/frameworks/_common/producer.py +3 -4
- mlrun/frameworks/_common/utils.py +5 -5
- mlrun/frameworks/_dl_common/loggers/logger.py +6 -7
- mlrun/frameworks/_dl_common/loggers/mlrun_logger.py +9 -9
- mlrun/frameworks/_dl_common/loggers/tensorboard_logger.py +23 -47
- mlrun/frameworks/_ml_common/artifacts_library.py +1 -2
- mlrun/frameworks/_ml_common/loggers/logger.py +3 -4
- mlrun/frameworks/_ml_common/loggers/mlrun_logger.py +4 -5
- mlrun/frameworks/_ml_common/model_handler.py +24 -24
- mlrun/frameworks/_ml_common/pkl_model_server.py +2 -2
- mlrun/frameworks/_ml_common/plan.py +2 -2
- mlrun/frameworks/_ml_common/plans/calibration_curve_plan.py +2 -3
- mlrun/frameworks/_ml_common/plans/confusion_matrix_plan.py +2 -3
- mlrun/frameworks/_ml_common/plans/dataset_plan.py +3 -3
- mlrun/frameworks/_ml_common/plans/feature_importance_plan.py +3 -3
- mlrun/frameworks/_ml_common/plans/roc_curve_plan.py +4 -4
- mlrun/frameworks/_ml_common/utils.py +4 -4
- mlrun/frameworks/auto_mlrun/auto_mlrun.py +9 -9
- mlrun/frameworks/huggingface/model_server.py +4 -4
- mlrun/frameworks/lgbm/__init__.py +33 -33
- mlrun/frameworks/lgbm/callbacks/callback.py +2 -4
- mlrun/frameworks/lgbm/callbacks/logging_callback.py +4 -5
- mlrun/frameworks/lgbm/callbacks/mlrun_logging_callback.py +4 -5
- mlrun/frameworks/lgbm/mlrun_interfaces/booster_mlrun_interface.py +1 -3
- mlrun/frameworks/lgbm/mlrun_interfaces/mlrun_interface.py +6 -6
- mlrun/frameworks/lgbm/model_handler.py +10 -10
- mlrun/frameworks/lgbm/model_server.py +6 -6
- mlrun/frameworks/lgbm/utils.py +5 -5
- mlrun/frameworks/onnx/dataset.py +8 -8
- mlrun/frameworks/onnx/mlrun_interface.py +3 -3
- mlrun/frameworks/onnx/model_handler.py +6 -6
- mlrun/frameworks/onnx/model_server.py +7 -7
- mlrun/frameworks/parallel_coordinates.py +6 -6
- mlrun/frameworks/pytorch/__init__.py +18 -18
- mlrun/frameworks/pytorch/callbacks/callback.py +4 -5
- mlrun/frameworks/pytorch/callbacks/logging_callback.py +17 -17
- mlrun/frameworks/pytorch/callbacks/mlrun_logging_callback.py +11 -11
- mlrun/frameworks/pytorch/callbacks/tensorboard_logging_callback.py +23 -29
- mlrun/frameworks/pytorch/callbacks_handler.py +38 -38
- mlrun/frameworks/pytorch/mlrun_interface.py +20 -20
- mlrun/frameworks/pytorch/model_handler.py +17 -17
- mlrun/frameworks/pytorch/model_server.py +7 -7
- mlrun/frameworks/sklearn/__init__.py +13 -13
- mlrun/frameworks/sklearn/estimator.py +4 -4
- mlrun/frameworks/sklearn/metrics_library.py +14 -14
- mlrun/frameworks/sklearn/mlrun_interface.py +16 -9
- mlrun/frameworks/sklearn/model_handler.py +2 -2
- mlrun/frameworks/tf_keras/__init__.py +10 -7
- mlrun/frameworks/tf_keras/callbacks/logging_callback.py +15 -15
- mlrun/frameworks/tf_keras/callbacks/mlrun_logging_callback.py +11 -11
- mlrun/frameworks/tf_keras/callbacks/tensorboard_logging_callback.py +19 -23
- mlrun/frameworks/tf_keras/mlrun_interface.py +9 -11
- mlrun/frameworks/tf_keras/model_handler.py +14 -14
- mlrun/frameworks/tf_keras/model_server.py +6 -6
- mlrun/frameworks/xgboost/__init__.py +13 -13
- mlrun/frameworks/xgboost/model_handler.py +6 -6
- mlrun/k8s_utils.py +61 -17
- mlrun/launcher/__init__.py +1 -1
- mlrun/launcher/base.py +16 -15
- mlrun/launcher/client.py +13 -11
- mlrun/launcher/factory.py +1 -1
- mlrun/launcher/local.py +23 -13
- mlrun/launcher/remote.py +17 -10
- mlrun/lists.py +7 -6
- mlrun/model.py +478 -103
- mlrun/model_monitoring/__init__.py +1 -1
- mlrun/model_monitoring/api.py +163 -371
- mlrun/{runtimes/mpijob/v1alpha1.py → model_monitoring/applications/__init__.py} +9 -15
- mlrun/model_monitoring/applications/_application_steps.py +188 -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 +354 -0
- mlrun/model_monitoring/applications/results.py +99 -0
- mlrun/model_monitoring/controller.py +131 -278
- mlrun/model_monitoring/db/__init__.py +18 -0
- 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 +279 -0
- mlrun/model_monitoring/db/tsdb/tdengine/stream_graph_steps.py +42 -0
- mlrun/model_monitoring/db/tsdb/tdengine/tdengine_connector.py +507 -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 +134 -106
- mlrun/model_monitoring/helpers.py +199 -55
- mlrun/model_monitoring/metrics/__init__.py +13 -0
- mlrun/model_monitoring/metrics/histogram_distance.py +127 -0
- mlrun/model_monitoring/model_endpoint.py +3 -2
- mlrun/model_monitoring/stream_processing.py +134 -398
- mlrun/model_monitoring/tracking_policy.py +9 -2
- mlrun/model_monitoring/writer.py +161 -125
- mlrun/package/__init__.py +6 -6
- mlrun/package/context_handler.py +5 -5
- mlrun/package/packager.py +7 -7
- mlrun/package/packagers/default_packager.py +8 -8
- mlrun/package/packagers/numpy_packagers.py +15 -15
- mlrun/package/packagers/pandas_packagers.py +5 -5
- mlrun/package/packagers/python_standard_library_packagers.py +10 -10
- mlrun/package/packagers_manager.py +19 -23
- mlrun/package/utils/_formatter.py +6 -6
- mlrun/package/utils/_pickler.py +2 -2
- mlrun/package/utils/_supported_format.py +4 -4
- mlrun/package/utils/log_hint_utils.py +2 -2
- mlrun/package/utils/type_hint_utils.py +4 -9
- mlrun/platforms/__init__.py +11 -10
- mlrun/platforms/iguazio.py +24 -203
- mlrun/projects/operations.py +52 -25
- mlrun/projects/pipelines.py +191 -197
- mlrun/projects/project.py +1227 -400
- mlrun/render.py +16 -19
- mlrun/run.py +209 -184
- mlrun/runtimes/__init__.py +83 -15
- mlrun/runtimes/base.py +51 -35
- mlrun/runtimes/daskjob.py +17 -10
- mlrun/runtimes/databricks_job/databricks_cancel_task.py +1 -1
- mlrun/runtimes/databricks_job/databricks_runtime.py +8 -7
- mlrun/runtimes/databricks_job/databricks_wrapper.py +1 -1
- mlrun/runtimes/funcdoc.py +1 -29
- mlrun/runtimes/function_reference.py +1 -1
- mlrun/runtimes/kubejob.py +34 -128
- mlrun/runtimes/local.py +40 -11
- mlrun/runtimes/mpijob/__init__.py +0 -20
- mlrun/runtimes/mpijob/abstract.py +9 -10
- mlrun/runtimes/mpijob/v1.py +1 -1
- mlrun/{model_monitoring/stores/models/sqlite.py → runtimes/nuclio/__init__.py} +7 -9
- 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/{function.py → nuclio/function.py} +200 -83
- mlrun/runtimes/{nuclio.py → nuclio/nuclio.py} +6 -6
- mlrun/runtimes/{serving.py → nuclio/serving.py} +65 -68
- mlrun/runtimes/pod.py +281 -101
- mlrun/runtimes/remotesparkjob.py +12 -9
- mlrun/runtimes/sparkjob/spark3job.py +67 -51
- mlrun/runtimes/utils.py +41 -75
- mlrun/secrets.py +9 -5
- mlrun/serving/__init__.py +8 -1
- mlrun/serving/remote.py +2 -7
- mlrun/serving/routers.py +85 -69
- mlrun/serving/server.py +69 -44
- mlrun/serving/states.py +209 -36
- mlrun/serving/utils.py +22 -14
- mlrun/serving/v1_serving.py +6 -7
- mlrun/serving/v2_serving.py +133 -54
- mlrun/track/tracker.py +2 -1
- mlrun/track/tracker_manager.py +3 -3
- mlrun/track/trackers/mlflow_tracker.py +6 -2
- mlrun/utils/async_http.py +6 -8
- mlrun/utils/azure_vault.py +1 -1
- mlrun/utils/clones.py +1 -2
- mlrun/utils/condition_evaluator.py +3 -3
- mlrun/utils/db.py +21 -3
- mlrun/utils/helpers.py +405 -225
- mlrun/utils/http.py +3 -6
- mlrun/utils/logger.py +112 -16
- mlrun/utils/notifications/notification/__init__.py +17 -13
- mlrun/utils/notifications/notification/base.py +50 -2
- mlrun/utils/notifications/notification/console.py +2 -0
- mlrun/utils/notifications/notification/git.py +24 -1
- mlrun/utils/notifications/notification/ipython.py +3 -1
- mlrun/utils/notifications/notification/slack.py +96 -21
- mlrun/utils/notifications/notification/webhook.py +59 -2
- mlrun/utils/notifications/notification_pusher.py +149 -30
- mlrun/utils/regex.py +9 -0
- mlrun/utils/retryer.py +208 -0
- mlrun/utils/singleton.py +1 -1
- mlrun/utils/v3io_clients.py +4 -6
- mlrun/utils/version/version.json +2 -2
- mlrun/utils/version/version.py +2 -6
- mlrun-1.7.0.dist-info/METADATA +378 -0
- mlrun-1.7.0.dist-info/RECORD +351 -0
- {mlrun-1.6.4rc8.dist-info → mlrun-1.7.0.dist-info}/WHEEL +1 -1
- mlrun/feature_store/retrieval/conversion.py +0 -273
- mlrun/kfpops.py +0 -868
- mlrun/model_monitoring/application.py +0 -310
- mlrun/model_monitoring/batch.py +0 -1095
- mlrun/model_monitoring/prometheus.py +0 -219
- mlrun/model_monitoring/stores/__init__.py +0 -111
- mlrun/model_monitoring/stores/kv_model_endpoint_store.py +0 -576
- mlrun/model_monitoring/stores/model_endpoint_store.py +0 -147
- 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 -384
- mlrun/platforms/other.py +0 -306
- mlrun-1.6.4rc8.dist-info/METADATA +0 -272
- mlrun-1.6.4rc8.dist-info/RECORD +0 -314
- {mlrun-1.6.4rc8.dist-info → mlrun-1.7.0.dist-info}/LICENSE +0 -0
- {mlrun-1.6.4rc8.dist-info → mlrun-1.7.0.dist-info}/entry_points.txt +0 -0
- {mlrun-1.6.4rc8.dist-info → mlrun-1.7.0.dist-info}/top_level.txt +0 -0
|
@@ -28,6 +28,12 @@ class WebhookNotification(NotificationBase):
|
|
|
28
28
|
API/Client notification for sending run statuses in a http request
|
|
29
29
|
"""
|
|
30
30
|
|
|
31
|
+
@classmethod
|
|
32
|
+
def validate_params(cls, params):
|
|
33
|
+
url = params.get("url", None)
|
|
34
|
+
if not url:
|
|
35
|
+
raise ValueError("Parameter 'url' is required for WebhookNotification")
|
|
36
|
+
|
|
31
37
|
async def push(
|
|
32
38
|
self,
|
|
33
39
|
message: str,
|
|
@@ -36,6 +42,8 @@ class WebhookNotification(NotificationBase):
|
|
|
36
42
|
] = mlrun.common.schemas.NotificationSeverity.INFO,
|
|
37
43
|
runs: typing.Union[mlrun.lists.RunList, list] = None,
|
|
38
44
|
custom_html: str = None,
|
|
45
|
+
alert: mlrun.common.schemas.AlertConfig = None,
|
|
46
|
+
event_data: mlrun.common.schemas.Event = None,
|
|
39
47
|
):
|
|
40
48
|
url = self.params.get("url", None)
|
|
41
49
|
method = self.params.get("method", "post").lower()
|
|
@@ -46,14 +54,29 @@ class WebhookNotification(NotificationBase):
|
|
|
46
54
|
request_body = {
|
|
47
55
|
"message": message,
|
|
48
56
|
"severity": severity,
|
|
49
|
-
"runs": runs,
|
|
50
57
|
}
|
|
51
58
|
|
|
59
|
+
if runs:
|
|
60
|
+
request_body["runs"] = runs
|
|
61
|
+
|
|
62
|
+
if alert:
|
|
63
|
+
request_body["name"] = alert.name
|
|
64
|
+
request_body["project"] = alert.project
|
|
65
|
+
request_body["severity"] = alert.severity
|
|
66
|
+
if alert.summary:
|
|
67
|
+
request_body["summary"] = mlrun.utils.helpers.format_alert_summary(
|
|
68
|
+
alert, event_data
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
if event_data:
|
|
72
|
+
request_body["value"] = event_data.value_dict
|
|
73
|
+
request_body["id"] = event_data.entity.ids[0]
|
|
74
|
+
|
|
52
75
|
if custom_html:
|
|
53
76
|
request_body["custom_html"] = custom_html
|
|
54
77
|
|
|
55
78
|
if override_body:
|
|
56
|
-
request_body = override_body
|
|
79
|
+
request_body = self._serialize_runs_in_request_body(override_body, runs)
|
|
57
80
|
|
|
58
81
|
# Specify the `verify_ssl` parameter value only for HTTPS urls.
|
|
59
82
|
# The `ClientSession` allows using `ssl=None` for the default SSL check,
|
|
@@ -67,3 +90,37 @@ class WebhookNotification(NotificationBase):
|
|
|
67
90
|
url, headers=headers, json=request_body, ssl=verify_ssl
|
|
68
91
|
)
|
|
69
92
|
response.raise_for_status()
|
|
93
|
+
|
|
94
|
+
@staticmethod
|
|
95
|
+
def _serialize_runs_in_request_body(override_body, runs):
|
|
96
|
+
str_parsed_runs = ""
|
|
97
|
+
runs = runs or []
|
|
98
|
+
|
|
99
|
+
def parse_runs():
|
|
100
|
+
parsed_runs = []
|
|
101
|
+
for run in runs:
|
|
102
|
+
if hasattr(run, "to_dict"):
|
|
103
|
+
run = run.to_dict()
|
|
104
|
+
if isinstance(run, dict):
|
|
105
|
+
parsed_run = {
|
|
106
|
+
"project": run["metadata"]["project"],
|
|
107
|
+
"name": run["metadata"]["name"],
|
|
108
|
+
"host": run["metadata"]["labels"]["host"],
|
|
109
|
+
"status": {"state": run["status"]["state"]},
|
|
110
|
+
}
|
|
111
|
+
if run["status"].get("error", None):
|
|
112
|
+
parsed_run["status"]["error"] = run["status"]["error"]
|
|
113
|
+
elif run["status"].get("results", None):
|
|
114
|
+
parsed_run["status"]["results"] = run["status"]["results"]
|
|
115
|
+
parsed_runs.append(parsed_run)
|
|
116
|
+
return str(parsed_runs)
|
|
117
|
+
|
|
118
|
+
if isinstance(override_body, dict):
|
|
119
|
+
for key, value in override_body.items():
|
|
120
|
+
if "{{ runs }}" or "{{runs}}" in value:
|
|
121
|
+
if not str_parsed_runs:
|
|
122
|
+
str_parsed_runs = parse_runs()
|
|
123
|
+
override_body[key] = value.replace(
|
|
124
|
+
"{{ runs }}", str_parsed_runs
|
|
125
|
+
).replace("{{runs}}", str_parsed_runs)
|
|
126
|
+
return override_body
|
|
@@ -15,10 +15,17 @@
|
|
|
15
15
|
import asyncio
|
|
16
16
|
import datetime
|
|
17
17
|
import os
|
|
18
|
+
import re
|
|
18
19
|
import traceback
|
|
19
20
|
import typing
|
|
20
21
|
from concurrent.futures import ThreadPoolExecutor
|
|
21
22
|
|
|
23
|
+
import mlrun_pipelines.common.ops
|
|
24
|
+
import mlrun_pipelines.models
|
|
25
|
+
import mlrun_pipelines.utils
|
|
26
|
+
|
|
27
|
+
import mlrun.common.constants as mlrun_constants
|
|
28
|
+
import mlrun.common.runtimes.constants
|
|
22
29
|
import mlrun.common.schemas
|
|
23
30
|
import mlrun.config
|
|
24
31
|
import mlrun.db.base
|
|
@@ -32,7 +39,7 @@ from mlrun.utils.condition_evaluator import evaluate_condition_in_separate_proce
|
|
|
32
39
|
from .notification import NotificationBase, NotificationTypes
|
|
33
40
|
|
|
34
41
|
|
|
35
|
-
class _NotificationPusherBase
|
|
42
|
+
class _NotificationPusherBase:
|
|
36
43
|
def _push(
|
|
37
44
|
self, sync_push_callback: typing.Callable, async_push_callback: typing.Callable
|
|
38
45
|
):
|
|
@@ -95,15 +102,11 @@ class NotificationPusher(_NotificationPusherBase):
|
|
|
95
102
|
|
|
96
103
|
def __init__(self, runs: typing.Union[mlrun.lists.RunList, list]):
|
|
97
104
|
self._runs = runs
|
|
98
|
-
self._sync_notifications:
|
|
99
|
-
|
|
100
|
-
NotificationBase, mlrun.model.RunObject, mlrun.model.Notification
|
|
101
|
-
]
|
|
105
|
+
self._sync_notifications: list[
|
|
106
|
+
tuple[NotificationBase, mlrun.model.RunObject, mlrun.model.Notification]
|
|
102
107
|
] = []
|
|
103
|
-
self._async_notifications:
|
|
104
|
-
|
|
105
|
-
NotificationBase, mlrun.model.RunObject, mlrun.model.Notification
|
|
106
|
-
]
|
|
108
|
+
self._async_notifications: list[
|
|
109
|
+
tuple[NotificationBase, mlrun.model.RunObject, mlrun.model.Notification]
|
|
107
110
|
] = []
|
|
108
111
|
|
|
109
112
|
for run in self._runs:
|
|
@@ -237,25 +240,12 @@ class NotificationPusher(_NotificationPusherBase):
|
|
|
237
240
|
resource = "Run"
|
|
238
241
|
runs = [run.to_dict()]
|
|
239
242
|
|
|
240
|
-
if
|
|
241
|
-
resource =
|
|
243
|
+
if mlrun_constants.MLRunInternalLabels.workflow in run.metadata.labels:
|
|
244
|
+
resource = mlrun_constants.MLRunInternalLabels.workflow
|
|
242
245
|
custom_message = (
|
|
243
246
|
f" (workflow: {run.metadata.labels['workflow']}){custom_message}"
|
|
244
247
|
)
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
workflow_id = run.status.results.get("workflow_id", None)
|
|
248
|
-
if workflow_id:
|
|
249
|
-
workflow_runs = db.list_runs(
|
|
250
|
-
project=run.metadata.project,
|
|
251
|
-
labels=f"workflow={workflow_id}",
|
|
252
|
-
)
|
|
253
|
-
logger.debug(
|
|
254
|
-
"Found workflow runs, extending notification runs",
|
|
255
|
-
workflow_id=workflow_id,
|
|
256
|
-
workflow_runs_amount=len(workflow_runs),
|
|
257
|
-
)
|
|
258
|
-
runs.extend(workflow_runs)
|
|
248
|
+
runs.extend(self.get_workflow_steps(run))
|
|
259
249
|
|
|
260
250
|
message = (
|
|
261
251
|
self.messages.get(run.state(), "").format(resource=resource)
|
|
@@ -399,9 +389,134 @@ class NotificationPusher(_NotificationPusherBase):
|
|
|
399
389
|
mask_params=False,
|
|
400
390
|
)
|
|
401
391
|
|
|
392
|
+
def get_workflow_steps(self, run: mlrun.model.RunObject) -> list:
|
|
393
|
+
steps = []
|
|
394
|
+
db = mlrun.get_run_db()
|
|
395
|
+
|
|
396
|
+
def _add_run_step(_step: mlrun_pipelines.models.PipelineStep):
|
|
397
|
+
try:
|
|
398
|
+
_run = db.list_runs(
|
|
399
|
+
project=run.metadata.project,
|
|
400
|
+
labels=f"{mlrun_constants.MLRunInternalLabels.runner_pod}={_step.node_name}",
|
|
401
|
+
)[0]
|
|
402
|
+
except IndexError:
|
|
403
|
+
_run = {
|
|
404
|
+
"metadata": {
|
|
405
|
+
"name": _step.display_name,
|
|
406
|
+
"project": run.metadata.project,
|
|
407
|
+
},
|
|
408
|
+
}
|
|
409
|
+
_run["step_kind"] = _step.step_type
|
|
410
|
+
if _step.skipped:
|
|
411
|
+
_run.setdefault("status", {})["state"] = (
|
|
412
|
+
mlrun.common.runtimes.constants.RunStates.skipped
|
|
413
|
+
)
|
|
414
|
+
steps.append(_run)
|
|
415
|
+
|
|
416
|
+
def _add_deploy_function_step(_step: mlrun_pipelines.models.PipelineStep):
|
|
417
|
+
project, name, hash_key = self._extract_function_uri(
|
|
418
|
+
_step.get_annotation("mlrun/function-uri")
|
|
419
|
+
)
|
|
420
|
+
if name:
|
|
421
|
+
try:
|
|
422
|
+
function = db.get_function(
|
|
423
|
+
project=project, name=name, hash_key=hash_key
|
|
424
|
+
)
|
|
425
|
+
except mlrun.errors.MLRunNotFoundError:
|
|
426
|
+
# If the function is not found (if build failed for example), we will create a dummy
|
|
427
|
+
# function object for the notification to display the function name
|
|
428
|
+
function = {
|
|
429
|
+
"metadata": {
|
|
430
|
+
"name": name,
|
|
431
|
+
"project": project,
|
|
432
|
+
"hash_key": hash_key,
|
|
433
|
+
},
|
|
434
|
+
}
|
|
435
|
+
pod_phase = _step.phase
|
|
436
|
+
if _step.skipped:
|
|
437
|
+
state = mlrun.common.schemas.FunctionState.skipped
|
|
438
|
+
else:
|
|
439
|
+
state = mlrun.common.runtimes.constants.PodPhases.pod_phase_to_run_state(
|
|
440
|
+
pod_phase
|
|
441
|
+
)
|
|
442
|
+
function["status"] = {"state": state}
|
|
443
|
+
if isinstance(function["metadata"].get("updated"), datetime.datetime):
|
|
444
|
+
function["metadata"]["updated"] = function["metadata"][
|
|
445
|
+
"updated"
|
|
446
|
+
].isoformat()
|
|
447
|
+
function["step_kind"] = _step.step_type
|
|
448
|
+
steps.append(function)
|
|
449
|
+
|
|
450
|
+
step_methods = {
|
|
451
|
+
mlrun_pipelines.common.ops.PipelineRunType.run: _add_run_step,
|
|
452
|
+
mlrun_pipelines.common.ops.PipelineRunType.build: _add_deploy_function_step,
|
|
453
|
+
mlrun_pipelines.common.ops.PipelineRunType.deploy: _add_deploy_function_step,
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
workflow_id = run.status.results.get("workflow_id", None)
|
|
457
|
+
if not workflow_id:
|
|
458
|
+
return steps
|
|
459
|
+
|
|
460
|
+
workflow_manifest = self._get_workflow_manifest(workflow_id)
|
|
461
|
+
if not workflow_manifest:
|
|
462
|
+
return steps
|
|
463
|
+
|
|
464
|
+
try:
|
|
465
|
+
for step in workflow_manifest.get_steps():
|
|
466
|
+
step_method = step_methods.get(step.step_type)
|
|
467
|
+
if step_method:
|
|
468
|
+
step_method(step)
|
|
469
|
+
return steps
|
|
470
|
+
except Exception:
|
|
471
|
+
# If we fail to read the pipeline steps, we will return the list of runs that have the same workflow id
|
|
472
|
+
logger.warning(
|
|
473
|
+
"Failed to extract workflow steps from workflow manifest, "
|
|
474
|
+
"returning all runs with the workflow id label",
|
|
475
|
+
workflow_id=workflow_id,
|
|
476
|
+
traceback=traceback.format_exc(),
|
|
477
|
+
)
|
|
478
|
+
return db.list_runs(
|
|
479
|
+
project=run.metadata.project,
|
|
480
|
+
labels=f"workflow={workflow_id}",
|
|
481
|
+
)
|
|
482
|
+
|
|
483
|
+
@staticmethod
|
|
484
|
+
def _get_workflow_manifest(
|
|
485
|
+
workflow_id: str,
|
|
486
|
+
) -> typing.Optional[mlrun_pipelines.models.PipelineManifest]:
|
|
487
|
+
kfp_client = mlrun_pipelines.utils.get_client(mlrun.mlconf.kfp_url)
|
|
488
|
+
|
|
489
|
+
# arbitrary timeout of 5 seconds, the workflow should be done by now
|
|
490
|
+
kfp_run = kfp_client.wait_for_run_completion(workflow_id, 5)
|
|
491
|
+
if not kfp_run:
|
|
492
|
+
return None
|
|
493
|
+
|
|
494
|
+
kfp_run = mlrun_pipelines.models.PipelineRun(kfp_run)
|
|
495
|
+
return kfp_run.workflow_manifest()
|
|
496
|
+
|
|
497
|
+
def _extract_function_uri(self, function_uri: str) -> tuple[str, str, str]:
|
|
498
|
+
"""
|
|
499
|
+
Extract the project, name, and hash key from a function uri.
|
|
500
|
+
Examples:
|
|
501
|
+
- "project/name@hash_key" returns project, name, hash_key
|
|
502
|
+
- "project/name returns" project, name, ""
|
|
503
|
+
"""
|
|
504
|
+
project, name, hash_key = None, None, None
|
|
505
|
+
hashed_pattern = r"^(.+)/(.+)@(.+)$"
|
|
506
|
+
pattern = r"^(.+)/(.+)$"
|
|
507
|
+
match = re.match(hashed_pattern, function_uri)
|
|
508
|
+
if match:
|
|
509
|
+
project, name, hash_key = match.groups()
|
|
510
|
+
else:
|
|
511
|
+
match = re.match(pattern, function_uri)
|
|
512
|
+
if match:
|
|
513
|
+
project, name = match.groups()
|
|
514
|
+
hash_key = ""
|
|
515
|
+
return project, name, hash_key
|
|
516
|
+
|
|
402
517
|
|
|
403
518
|
class CustomNotificationPusher(_NotificationPusherBase):
|
|
404
|
-
def __init__(self, notification_types:
|
|
519
|
+
def __init__(self, notification_types: list[str] = None):
|
|
405
520
|
notifications = {
|
|
406
521
|
notification_type: NotificationTypes(notification_type).get_notification()()
|
|
407
522
|
for notification_type in notification_types
|
|
@@ -417,6 +532,12 @@ class CustomNotificationPusher(_NotificationPusherBase):
|
|
|
417
532
|
if notification.is_async
|
|
418
533
|
}
|
|
419
534
|
|
|
535
|
+
@property
|
|
536
|
+
def notifications(self):
|
|
537
|
+
notifications = self._sync_notifications.copy()
|
|
538
|
+
notifications.update(self._async_notifications)
|
|
539
|
+
return notifications
|
|
540
|
+
|
|
420
541
|
def push(
|
|
421
542
|
self,
|
|
422
543
|
message: str,
|
|
@@ -446,7 +567,7 @@ class CustomNotificationPusher(_NotificationPusherBase):
|
|
|
446
567
|
def add_notification(
|
|
447
568
|
self,
|
|
448
569
|
notification_type: str,
|
|
449
|
-
params:
|
|
570
|
+
params: dict[str, str] = None,
|
|
450
571
|
):
|
|
451
572
|
if notification_type in self._async_notifications:
|
|
452
573
|
self._async_notifications[notification_type].load_notification(params)
|
|
@@ -471,9 +592,7 @@ class CustomNotificationPusher(_NotificationPusherBase):
|
|
|
471
592
|
else:
|
|
472
593
|
logger.warning(f"No notification of type {notification_type} in project")
|
|
473
594
|
|
|
474
|
-
def edit_notification(
|
|
475
|
-
self, notification_type: str, params: typing.Dict[str, str] = None
|
|
476
|
-
):
|
|
595
|
+
def edit_notification(self, notification_type: str, params: dict[str, str] = None):
|
|
477
596
|
self.remove_notification(notification_type)
|
|
478
597
|
self.add_notification(notification_type, params)
|
|
479
598
|
|
mlrun/utils/regex.py
CHANGED
|
@@ -92,3 +92,12 @@ artifact_key = [r"[^\/\\]+$"]
|
|
|
92
92
|
# must be alphanumeric or _
|
|
93
93
|
# max 256 length
|
|
94
94
|
v3io_stream_consumer_group = [r"^(?!_)[a-zA-Z0-9_]{1,256}$"]
|
|
95
|
+
|
|
96
|
+
# URI patterns
|
|
97
|
+
run_uri_pattern = r"^(?P<project>.*)@(?P<uid>.*)\#(?P<iteration>.*?)(:(?P<tag>.*))?$"
|
|
98
|
+
|
|
99
|
+
artifact_uri_pattern = r"^((?P<project>.*)/)?(?P<key>.*?)(\#(?P<iteration>.*?))?(:(?P<tag>.*?))?(@(?P<tree>.*))?$"
|
|
100
|
+
|
|
101
|
+
artifact_producer_uri_pattern = (
|
|
102
|
+
r"^((?P<project>.*)/)?(?P<uid>.*?)(\-(?P<iteration>.*?))?$"
|
|
103
|
+
)
|
mlrun/utils/retryer.py
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
# Copyright 2023 Iguazio
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
import asyncio
|
|
16
|
+
import time
|
|
17
|
+
|
|
18
|
+
import mlrun.errors
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def create_linear_backoff(base=2, coefficient=2, stop_value=120):
|
|
22
|
+
"""
|
|
23
|
+
Create a generator of linear backoff. Check out usage example in test_helpers.py
|
|
24
|
+
"""
|
|
25
|
+
x = 0
|
|
26
|
+
comparison = min if coefficient >= 0 else max
|
|
27
|
+
|
|
28
|
+
while True:
|
|
29
|
+
next_value = comparison(base + x * coefficient, stop_value)
|
|
30
|
+
yield next_value
|
|
31
|
+
x += 1
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def create_step_backoff(steps=None):
|
|
35
|
+
"""
|
|
36
|
+
Create a generator of steps backoff.
|
|
37
|
+
Example: steps = [[2, 5], [20, 10], [120, None]] will produce a generator in which the first 5
|
|
38
|
+
values will be 2, the next 10 values will be 20 and the rest will be 120.
|
|
39
|
+
:param steps: a list of lists [step_value, number_of_iteration_in_this_step]
|
|
40
|
+
"""
|
|
41
|
+
steps = steps if steps is not None else [[2, 10], [10, 10], [120, None]]
|
|
42
|
+
steps = iter(steps)
|
|
43
|
+
|
|
44
|
+
# Get first step
|
|
45
|
+
step = next(steps)
|
|
46
|
+
while True:
|
|
47
|
+
current_step_value, current_step_remain = step
|
|
48
|
+
if current_step_remain == 0:
|
|
49
|
+
# No more in this step, moving on
|
|
50
|
+
step = next(steps)
|
|
51
|
+
elif current_step_remain is None:
|
|
52
|
+
# We are in the last step, staying here forever
|
|
53
|
+
yield current_step_value
|
|
54
|
+
elif current_step_remain > 0:
|
|
55
|
+
# Still more remains in this step, just reduce the remaining number
|
|
56
|
+
step[1] -= 1
|
|
57
|
+
yield current_step_value
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def create_exponential_backoff(base=2, max_value=120, scale_factor=1):
|
|
61
|
+
"""
|
|
62
|
+
Create a generator of exponential backoff. Check out usage example in test_helpers.py
|
|
63
|
+
:param base: exponent base
|
|
64
|
+
:param max_value: max limit on the result
|
|
65
|
+
:param scale_factor: factor to be used as linear scaling coefficient
|
|
66
|
+
"""
|
|
67
|
+
exponent = 1
|
|
68
|
+
while True:
|
|
69
|
+
# This "complex" implementation (unlike the one in linear backoff) is to avoid exponent growing too fast and
|
|
70
|
+
# risking going behind max_int
|
|
71
|
+
next_value = scale_factor * (base**exponent)
|
|
72
|
+
if next_value < max_value:
|
|
73
|
+
exponent += 1
|
|
74
|
+
yield next_value
|
|
75
|
+
else:
|
|
76
|
+
yield max_value
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class Retryer:
|
|
80
|
+
def __init__(self, backoff, timeout, logger, verbose, function, *args, **kwargs):
|
|
81
|
+
"""
|
|
82
|
+
Initialize function retryer with given *args and **kwargs.
|
|
83
|
+
Tries to run it until success or timeout reached (timeout is optional)
|
|
84
|
+
:param backoff: can either be a:
|
|
85
|
+
- number (int / float) that will be used as interval.
|
|
86
|
+
- generator of waiting intervals. (support next())
|
|
87
|
+
:param timeout: pass None if timeout is not wanted, number of seconds if it is
|
|
88
|
+
:param logger: a logger so we can log the failures
|
|
89
|
+
:param verbose: whether to log the failure on each retry
|
|
90
|
+
:param _function: function to run
|
|
91
|
+
:param args: functions args
|
|
92
|
+
:param kwargs: functions kwargs
|
|
93
|
+
"""
|
|
94
|
+
self.backoff = backoff
|
|
95
|
+
self.timeout = timeout
|
|
96
|
+
self.logger = logger
|
|
97
|
+
self.verbose = verbose
|
|
98
|
+
self.function = function
|
|
99
|
+
self.args = args
|
|
100
|
+
self.kwargs = kwargs
|
|
101
|
+
self.start_time = None
|
|
102
|
+
self.last_exception = None
|
|
103
|
+
self.first_interval = None
|
|
104
|
+
|
|
105
|
+
def run(self):
|
|
106
|
+
self._prepare()
|
|
107
|
+
while not self._timeout_exceeded():
|
|
108
|
+
next_interval = self.first_interval or next(self.backoff)
|
|
109
|
+
result, exc, retry = self._perform_call(next_interval)
|
|
110
|
+
if retry:
|
|
111
|
+
time.sleep(next_interval)
|
|
112
|
+
elif not exc:
|
|
113
|
+
return result
|
|
114
|
+
else:
|
|
115
|
+
break
|
|
116
|
+
|
|
117
|
+
self._raise_last_exception()
|
|
118
|
+
|
|
119
|
+
def _prepare(self):
|
|
120
|
+
self.start_time = time.monotonic()
|
|
121
|
+
self.last_exception = None
|
|
122
|
+
|
|
123
|
+
# Check if backoff is just a simple interval
|
|
124
|
+
if isinstance(self.backoff, int) or isinstance(self.backoff, float):
|
|
125
|
+
self.backoff = create_linear_backoff(base=self.backoff, coefficient=0)
|
|
126
|
+
|
|
127
|
+
self.first_interval = next(self.backoff)
|
|
128
|
+
if self.timeout and self.timeout <= self.first_interval:
|
|
129
|
+
self.logger.warning(
|
|
130
|
+
f"Timeout ({self.timeout}) must be higher than backoff ({self.first_interval})."
|
|
131
|
+
f" Set timeout to be higher than backoff."
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
def _perform_call(self, next_interval):
|
|
135
|
+
try:
|
|
136
|
+
result = self.function(*self.args, **self.kwargs)
|
|
137
|
+
return result, None, False
|
|
138
|
+
except mlrun.errors.MLRunFatalFailureError as exc:
|
|
139
|
+
raise exc.original_exception
|
|
140
|
+
except Exception as exc:
|
|
141
|
+
self.last_exception = exc
|
|
142
|
+
return (
|
|
143
|
+
None,
|
|
144
|
+
self.last_exception,
|
|
145
|
+
self._assert_failure_timeout(next_interval, exc),
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
def _assert_failure_timeout(self, next_interval, exc):
|
|
149
|
+
self.last_exception = exc
|
|
150
|
+
|
|
151
|
+
# If next interval is within allowed time period - wait on interval, abort otherwise
|
|
152
|
+
if not self._timeout_exceeded(next_interval):
|
|
153
|
+
if self.logger is not None and self.verbose:
|
|
154
|
+
self.logger.debug(
|
|
155
|
+
f"Operation not yet successful, Retrying in {next_interval} seconds."
|
|
156
|
+
f" exc: {mlrun.errors.err_to_str(exc)}"
|
|
157
|
+
)
|
|
158
|
+
return True
|
|
159
|
+
else:
|
|
160
|
+
return False
|
|
161
|
+
|
|
162
|
+
def _raise_last_exception(self):
|
|
163
|
+
if self.logger is not None:
|
|
164
|
+
self.logger.warning(
|
|
165
|
+
f"Operation did not complete on time. last exception: {self.last_exception}"
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
raise mlrun.errors.MLRunRetryExhaustedError(
|
|
169
|
+
f"Failed to execute command by the given deadline."
|
|
170
|
+
f" last_exception: {self.last_exception},"
|
|
171
|
+
f" function_name: {self.function.__name__},"
|
|
172
|
+
f" timeout: {self.timeout}"
|
|
173
|
+
) from self.last_exception
|
|
174
|
+
|
|
175
|
+
def _timeout_exceeded(self, next_interval=None):
|
|
176
|
+
now = time.monotonic()
|
|
177
|
+
if next_interval:
|
|
178
|
+
now = now + next_interval
|
|
179
|
+
return self.timeout is not None and now >= self.start_time + self.timeout
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
class AsyncRetryer(Retryer):
|
|
183
|
+
async def run(self):
|
|
184
|
+
self._prepare()
|
|
185
|
+
while not self._timeout_exceeded():
|
|
186
|
+
next_interval = self.first_interval or next(self.backoff)
|
|
187
|
+
result, exc, retry = await self._perform_call(next_interval)
|
|
188
|
+
if retry:
|
|
189
|
+
await asyncio.sleep(next_interval)
|
|
190
|
+
elif not exc:
|
|
191
|
+
return result
|
|
192
|
+
else:
|
|
193
|
+
break
|
|
194
|
+
|
|
195
|
+
self._raise_last_exception()
|
|
196
|
+
|
|
197
|
+
async def _perform_call(self, next_interval):
|
|
198
|
+
try:
|
|
199
|
+
result = await self.function(*self.args, **self.kwargs)
|
|
200
|
+
return result, None, False
|
|
201
|
+
except mlrun.errors.MLRunFatalFailureError as exc:
|
|
202
|
+
raise exc.original_exception
|
|
203
|
+
except Exception as exc:
|
|
204
|
+
return (
|
|
205
|
+
None,
|
|
206
|
+
self.last_exception,
|
|
207
|
+
self._assert_failure_timeout(next_interval, exc),
|
|
208
|
+
)
|
mlrun/utils/singleton.py
CHANGED
|
@@ -20,7 +20,7 @@ class Singleton(type):
|
|
|
20
20
|
|
|
21
21
|
def __call__(cls, *args, **kwargs):
|
|
22
22
|
if cls not in cls._instances:
|
|
23
|
-
cls._instances[cls] = super(
|
|
23
|
+
cls._instances[cls] = super().__call__(*args, **kwargs)
|
|
24
24
|
return cls._instances[cls]
|
|
25
25
|
|
|
26
26
|
|
mlrun/utils/v3io_clients.py
CHANGED
|
@@ -11,22 +11,20 @@
|
|
|
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
|
-
#
|
|
15
|
-
from typing import Dict, FrozenSet
|
|
16
14
|
|
|
17
15
|
from v3io.dataplane import Client as V3IOClient
|
|
18
|
-
from v3io_frames import Client as
|
|
16
|
+
from v3io_frames import Client as V3IOFramesClient
|
|
19
17
|
from v3io_frames.client import ClientBase
|
|
20
18
|
|
|
21
|
-
_v3io_clients:
|
|
22
|
-
_frames_clients:
|
|
19
|
+
_v3io_clients: dict[frozenset, V3IOClient] = {}
|
|
20
|
+
_frames_clients: dict[frozenset, ClientBase] = {}
|
|
23
21
|
|
|
24
22
|
|
|
25
23
|
def get_frames_client(**kwargs) -> ClientBase:
|
|
26
24
|
global _frames_clients
|
|
27
25
|
kw_set = frozenset(kwargs.items())
|
|
28
26
|
if kw_set not in _frames_clients:
|
|
29
|
-
_frames_clients[kw_set] =
|
|
27
|
+
_frames_clients[kw_set] = V3IOFramesClient(**kwargs)
|
|
30
28
|
|
|
31
29
|
return _frames_clients[kw_set]
|
|
32
30
|
|
mlrun/utils/version/version.json
CHANGED
mlrun/utils/version/version.py
CHANGED
|
@@ -14,15 +14,11 @@
|
|
|
14
14
|
#
|
|
15
15
|
import json
|
|
16
16
|
import sys
|
|
17
|
+
from importlib.resources import read_text
|
|
17
18
|
|
|
18
19
|
import mlrun.utils
|
|
19
20
|
from mlrun.utils.singleton import Singleton
|
|
20
21
|
|
|
21
|
-
if sys.version_info >= (3, 7):
|
|
22
|
-
from importlib.resources import read_text
|
|
23
|
-
else:
|
|
24
|
-
from importlib_resources import read_text
|
|
25
|
-
|
|
26
22
|
|
|
27
23
|
class _VersionInfo:
|
|
28
24
|
def __init__(self, major, minor, patch):
|
|
@@ -56,5 +52,5 @@ class Version(metaclass=Singleton):
|
|
|
56
52
|
return self.python_version
|
|
57
53
|
|
|
58
54
|
@staticmethod
|
|
59
|
-
def _resolve_python_version() ->
|
|
55
|
+
def _resolve_python_version() -> _VersionInfo:
|
|
60
56
|
return _VersionInfo(*sys.version_info[:3])
|