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/http.py
CHANGED
|
@@ -95,7 +95,7 @@ class HTTPSessionWithRetry(requests.Session):
|
|
|
95
95
|
total=self.max_retries,
|
|
96
96
|
backoff_factor=self.retry_backoff_factor,
|
|
97
97
|
status_forcelist=config.http_retry_defaults.status_codes,
|
|
98
|
-
|
|
98
|
+
allowed_methods=self._retry_methods,
|
|
99
99
|
# we want to retry but not to raise since we do want that last response (to parse details on the
|
|
100
100
|
# error from response body) we'll handle raising ourselves
|
|
101
101
|
raise_on_status=False,
|
|
@@ -122,7 +122,7 @@ class HTTPSessionWithRetry(requests.Session):
|
|
|
122
122
|
|
|
123
123
|
self._logger.warning(
|
|
124
124
|
"Error during request handling, retrying",
|
|
125
|
-
exc=
|
|
125
|
+
exc=err_to_str(exc),
|
|
126
126
|
retry_count=retry_count,
|
|
127
127
|
url=url,
|
|
128
128
|
method=method,
|
mlrun/utils/logger.py
CHANGED
|
@@ -13,7 +13,10 @@
|
|
|
13
13
|
# limitations under the License.
|
|
14
14
|
|
|
15
15
|
import logging
|
|
16
|
+
import os
|
|
17
|
+
import typing
|
|
16
18
|
from enum import Enum
|
|
19
|
+
from functools import cached_property
|
|
17
20
|
from sys import stdout
|
|
18
21
|
from traceback import format_exception
|
|
19
22
|
from typing import IO, Optional, Union
|
|
@@ -91,15 +94,65 @@ class HumanReadableFormatter(_BaseFormatter):
|
|
|
91
94
|
|
|
92
95
|
|
|
93
96
|
class HumanReadableExtendedFormatter(HumanReadableFormatter):
|
|
97
|
+
_colors = {
|
|
98
|
+
logging.NOTSET: "",
|
|
99
|
+
logging.DEBUG: "\x1b[34m",
|
|
100
|
+
logging.INFO: "\x1b[36m",
|
|
101
|
+
logging.WARNING: "\x1b[33m",
|
|
102
|
+
logging.ERROR: "\x1b[0;31m",
|
|
103
|
+
logging.CRITICAL: "\x1b[1;31m",
|
|
104
|
+
}
|
|
105
|
+
_color_reset = "\x1b[0m"
|
|
106
|
+
|
|
94
107
|
def format(self, record) -> str:
|
|
95
|
-
more =
|
|
108
|
+
more = ""
|
|
109
|
+
record_with = self._record_with(record)
|
|
110
|
+
if record_with:
|
|
111
|
+
|
|
112
|
+
def _format_value(val):
|
|
113
|
+
formatted_val = (
|
|
114
|
+
val
|
|
115
|
+
if isinstance(val, str)
|
|
116
|
+
else str(orjson.loads(self._json_dump(val)))
|
|
117
|
+
)
|
|
118
|
+
return (
|
|
119
|
+
formatted_val.replace("\n", "\n\t\t")
|
|
120
|
+
if len(formatted_val) < 4096
|
|
121
|
+
else repr(formatted_val)
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
more = "\n\t" + "\n\t".join(
|
|
125
|
+
[f"{key}: {_format_value(val)}" for key, val in record_with.items()]
|
|
126
|
+
)
|
|
96
127
|
return (
|
|
97
|
-
"> "
|
|
128
|
+
f"{self._get_message_color(record.levelno)}> "
|
|
98
129
|
f"{self.formatTime(record, self.datefmt)} "
|
|
99
130
|
f"[{record.name}:{record.levelname.lower()}] "
|
|
100
|
-
f"{record.getMessage()}{more}"
|
|
131
|
+
f"{record.getMessage()}{more}{self._get_color_reset()}"
|
|
101
132
|
)
|
|
102
133
|
|
|
134
|
+
def _get_color_reset(self):
|
|
135
|
+
if not self._have_color_support:
|
|
136
|
+
return ""
|
|
137
|
+
|
|
138
|
+
return self._color_reset
|
|
139
|
+
|
|
140
|
+
def _get_message_color(self, levelno):
|
|
141
|
+
if not self._have_color_support:
|
|
142
|
+
return ""
|
|
143
|
+
|
|
144
|
+
return self._colors[levelno]
|
|
145
|
+
|
|
146
|
+
@cached_property
|
|
147
|
+
def _have_color_support(self):
|
|
148
|
+
if os.environ.get("PYCHARM_HOSTED"):
|
|
149
|
+
return True
|
|
150
|
+
if os.environ.get("NO_COLOR"):
|
|
151
|
+
return False
|
|
152
|
+
if os.environ.get("CLICOLOR_FORCE"):
|
|
153
|
+
return True
|
|
154
|
+
return stdout.isatty()
|
|
155
|
+
|
|
103
156
|
|
|
104
157
|
class Logger:
|
|
105
158
|
def __init__(
|
|
@@ -221,14 +274,27 @@ class FormatterKinds(Enum):
|
|
|
221
274
|
JSON = "json"
|
|
222
275
|
|
|
223
276
|
|
|
224
|
-
def
|
|
277
|
+
def resolve_formatter_by_kind(
|
|
278
|
+
formatter_kind: FormatterKinds,
|
|
279
|
+
) -> type[
|
|
280
|
+
typing.Union[HumanReadableFormatter, HumanReadableExtendedFormatter, JSONFormatter]
|
|
281
|
+
]:
|
|
225
282
|
return {
|
|
226
|
-
FormatterKinds.HUMAN: HumanReadableFormatter
|
|
227
|
-
FormatterKinds.HUMAN_EXTENDED: HumanReadableExtendedFormatter
|
|
228
|
-
FormatterKinds.JSON: JSONFormatter
|
|
283
|
+
FormatterKinds.HUMAN: HumanReadableFormatter,
|
|
284
|
+
FormatterKinds.HUMAN_EXTENDED: HumanReadableExtendedFormatter,
|
|
285
|
+
FormatterKinds.JSON: JSONFormatter,
|
|
229
286
|
}[formatter_kind]
|
|
230
287
|
|
|
231
288
|
|
|
289
|
+
def create_test_logger(name: str = "mlrun", stream: IO[str] = stdout) -> Logger:
|
|
290
|
+
return create_logger(
|
|
291
|
+
level="debug",
|
|
292
|
+
formatter_kind=FormatterKinds.HUMAN_EXTENDED.name,
|
|
293
|
+
name=name,
|
|
294
|
+
stream=stream,
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
|
|
232
298
|
def create_logger(
|
|
233
299
|
level: Optional[str] = None,
|
|
234
300
|
formatter_kind: str = FormatterKinds.HUMAN.name,
|
|
@@ -243,11 +309,11 @@ def create_logger(
|
|
|
243
309
|
logger_instance = Logger(level, name=name, propagate=False)
|
|
244
310
|
|
|
245
311
|
# resolve formatter
|
|
246
|
-
formatter_instance =
|
|
312
|
+
formatter_instance = resolve_formatter_by_kind(
|
|
247
313
|
FormatterKinds(formatter_kind.lower())
|
|
248
314
|
)
|
|
249
315
|
|
|
250
316
|
# set handler
|
|
251
|
-
logger_instance.set_handler("default", stream or stdout, formatter_instance)
|
|
317
|
+
logger_instance.set_handler("default", stream or stdout, formatter_instance())
|
|
252
318
|
|
|
253
319
|
return logger_instance
|
|
@@ -13,7 +13,6 @@
|
|
|
13
13
|
# limitations under the License.
|
|
14
14
|
|
|
15
15
|
import enum
|
|
16
|
-
import typing
|
|
17
16
|
|
|
18
17
|
from mlrun.common.schemas.notification import NotificationKind
|
|
19
18
|
|
|
@@ -51,14 +50,19 @@ class NotificationTypes(str, enum.Enum):
|
|
|
51
50
|
self.console: [self.ipython],
|
|
52
51
|
}.get(self, [])
|
|
53
52
|
|
|
53
|
+
@classmethod
|
|
54
|
+
def local(cls) -> list[str]:
|
|
55
|
+
return [
|
|
56
|
+
cls.console,
|
|
57
|
+
cls.ipython,
|
|
58
|
+
]
|
|
59
|
+
|
|
54
60
|
@classmethod
|
|
55
61
|
def all(cls) -> list[str]:
|
|
56
|
-
return
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
]
|
|
64
|
-
)
|
|
62
|
+
return [
|
|
63
|
+
cls.console,
|
|
64
|
+
cls.git,
|
|
65
|
+
cls.ipython,
|
|
66
|
+
cls.slack,
|
|
67
|
+
cls.webhook,
|
|
68
|
+
]
|
|
@@ -28,6 +28,10 @@ class NotificationBase:
|
|
|
28
28
|
self.name = name
|
|
29
29
|
self.params = params or {}
|
|
30
30
|
|
|
31
|
+
@classmethod
|
|
32
|
+
def validate_params(cls, params):
|
|
33
|
+
pass
|
|
34
|
+
|
|
31
35
|
@property
|
|
32
36
|
def active(self) -> bool:
|
|
33
37
|
return True
|
|
@@ -44,6 +48,8 @@ class NotificationBase:
|
|
|
44
48
|
] = mlrun.common.schemas.NotificationSeverity.INFO,
|
|
45
49
|
runs: typing.Union[mlrun.lists.RunList, list] = None,
|
|
46
50
|
custom_html: str = None,
|
|
51
|
+
alert: mlrun.common.schemas.AlertConfig = None,
|
|
52
|
+
event_data: mlrun.common.schemas.Event = None,
|
|
47
53
|
):
|
|
48
54
|
raise NotImplementedError()
|
|
49
55
|
|
|
@@ -61,10 +67,31 @@ class NotificationBase:
|
|
|
61
67
|
] = mlrun.common.schemas.NotificationSeverity.INFO,
|
|
62
68
|
runs: typing.Union[mlrun.lists.RunList, list] = None,
|
|
63
69
|
custom_html: str = None,
|
|
70
|
+
alert: mlrun.common.schemas.AlertConfig = None,
|
|
71
|
+
event_data: mlrun.common.schemas.Event = None,
|
|
64
72
|
) -> str:
|
|
65
73
|
if custom_html:
|
|
66
74
|
return custom_html
|
|
67
75
|
|
|
76
|
+
if alert:
|
|
77
|
+
if not event_data:
|
|
78
|
+
return f"[{severity}] {message}"
|
|
79
|
+
|
|
80
|
+
html = f"<h3>[{severity}] {message}</h3>"
|
|
81
|
+
html += f"<br>{alert.name} alert has occurred<br>"
|
|
82
|
+
html += f"<br><h4>Project:</h4>{alert.project}<br>"
|
|
83
|
+
html += f"<br><h4>ID:</h4>{event_data.entity.ids[0]}<br>"
|
|
84
|
+
html += f"<br><h4>Summary:</h4>{mlrun.utils.helpers.format_alert_summary(alert, event_data)}<br>"
|
|
85
|
+
|
|
86
|
+
if event_data.value_dict:
|
|
87
|
+
html += "<br><h4>Event data:</h4>"
|
|
88
|
+
for key, value in event_data.value_dict.items():
|
|
89
|
+
html += f"{key}: {value}<br>"
|
|
90
|
+
|
|
91
|
+
overview_type, url = self._get_overview_type_and_url(alert, event_data)
|
|
92
|
+
html += f"<br><h4>Overview:</h4><a href={url}>{overview_type}</a>"
|
|
93
|
+
return html
|
|
94
|
+
|
|
68
95
|
if self.name:
|
|
69
96
|
message = f"{self.name}: {message}"
|
|
70
97
|
|
|
@@ -78,3 +105,24 @@ class NotificationBase:
|
|
|
78
105
|
html += "<br>click the hyper links below to see detailed results<br>"
|
|
79
106
|
html += runs.show(display=False, short=True)
|
|
80
107
|
return html
|
|
108
|
+
|
|
109
|
+
def _get_overview_type_and_url(
|
|
110
|
+
self,
|
|
111
|
+
alert: mlrun.common.schemas.AlertConfig,
|
|
112
|
+
event_data: mlrun.common.schemas.Event,
|
|
113
|
+
) -> (str, str):
|
|
114
|
+
if (
|
|
115
|
+
event_data.entity.kind == mlrun.common.schemas.alert.EventEntityKind.JOB
|
|
116
|
+
): # JOB entity
|
|
117
|
+
uid = event_data.value_dict.get("uid")
|
|
118
|
+
url = mlrun.utils.helpers.get_ui_url(alert.project, uid)
|
|
119
|
+
overview_type = "Job overview"
|
|
120
|
+
else: # MODEL entity
|
|
121
|
+
model_name = event_data.value_dict.get("model")
|
|
122
|
+
model_endpoint_id = event_data.value_dict.get("model_endpoint_id")
|
|
123
|
+
url = mlrun.utils.helpers.get_model_endpoint_url(
|
|
124
|
+
alert.project, model_name, model_endpoint_id
|
|
125
|
+
)
|
|
126
|
+
overview_type = "Model endpoint"
|
|
127
|
+
|
|
128
|
+
return overview_type, url
|
|
@@ -36,6 +36,8 @@ class ConsoleNotification(NotificationBase):
|
|
|
36
36
|
] = mlrun.common.schemas.NotificationSeverity.INFO,
|
|
37
37
|
runs: typing.Union[mlrun.lists.RunList, list] = None,
|
|
38
38
|
custom_html: str = None,
|
|
39
|
+
alert: mlrun.common.schemas.AlertConfig = None,
|
|
40
|
+
event_data: mlrun.common.schemas.Event = None,
|
|
39
41
|
):
|
|
40
42
|
severity = self._resolve_severity(severity)
|
|
41
43
|
print(f"[{severity}] {message}")
|
|
@@ -30,6 +30,27 @@ class GitNotification(NotificationBase):
|
|
|
30
30
|
API/Client notification for setting a rich run statuses git issue comment (github/gitlab)
|
|
31
31
|
"""
|
|
32
32
|
|
|
33
|
+
@classmethod
|
|
34
|
+
def validate_params(cls, params):
|
|
35
|
+
git_repo = params.get("repo", None)
|
|
36
|
+
git_issue = params.get("issue", None)
|
|
37
|
+
git_merge_request = params.get("merge_request", None)
|
|
38
|
+
token = (
|
|
39
|
+
params.get("token", None)
|
|
40
|
+
or params.get("GIT_TOKEN", None)
|
|
41
|
+
or params.get("GITHUB_TOKEN", None)
|
|
42
|
+
)
|
|
43
|
+
if not git_repo:
|
|
44
|
+
raise ValueError("Parameter 'repo' is required for GitNotification")
|
|
45
|
+
|
|
46
|
+
if not token:
|
|
47
|
+
raise ValueError("Parameter 'token' is required for GitNotification")
|
|
48
|
+
|
|
49
|
+
if not git_issue and not git_merge_request:
|
|
50
|
+
raise ValueError(
|
|
51
|
+
"At least one of 'issue' or 'merge_request' is required for GitNotification"
|
|
52
|
+
)
|
|
53
|
+
|
|
33
54
|
async def push(
|
|
34
55
|
self,
|
|
35
56
|
message: str,
|
|
@@ -38,6 +59,8 @@ class GitNotification(NotificationBase):
|
|
|
38
59
|
] = mlrun.common.schemas.NotificationSeverity.INFO,
|
|
39
60
|
runs: typing.Union[mlrun.lists.RunList, list] = None,
|
|
40
61
|
custom_html: str = None,
|
|
62
|
+
alert: mlrun.common.schemas.AlertConfig = None,
|
|
63
|
+
event_data: mlrun.common.schemas.Event = None,
|
|
41
64
|
):
|
|
42
65
|
git_repo = self.params.get("repo", None)
|
|
43
66
|
git_issue = self.params.get("issue", None)
|
|
@@ -50,7 +73,7 @@ class GitNotification(NotificationBase):
|
|
|
50
73
|
server = self.params.get("server", None)
|
|
51
74
|
gitlab = self.params.get("gitlab", False)
|
|
52
75
|
await self._pr_comment(
|
|
53
|
-
self._get_html(message, severity, runs, custom_html),
|
|
76
|
+
self._get_html(message, severity, runs, custom_html, alert, event_data),
|
|
54
77
|
git_repo,
|
|
55
78
|
git_issue,
|
|
56
79
|
merge_request=git_merge_request,
|
|
@@ -53,6 +53,8 @@ class IPythonNotification(NotificationBase):
|
|
|
53
53
|
] = mlrun.common.schemas.NotificationSeverity.INFO,
|
|
54
54
|
runs: typing.Union[mlrun.lists.RunList, list] = None,
|
|
55
55
|
custom_html: str = None,
|
|
56
|
+
alert: mlrun.common.schemas.AlertConfig = None,
|
|
57
|
+
event_data: mlrun.common.schemas.Event = None,
|
|
56
58
|
):
|
|
57
59
|
if not self._ipython:
|
|
58
60
|
mlrun.utils.helpers.logger.debug(
|
|
@@ -32,8 +32,17 @@ class SlackNotification(NotificationBase):
|
|
|
32
32
|
"completed": ":smiley:",
|
|
33
33
|
"running": ":man-running:",
|
|
34
34
|
"error": ":x:",
|
|
35
|
+
"skipped": ":zzz:",
|
|
35
36
|
}
|
|
36
37
|
|
|
38
|
+
@classmethod
|
|
39
|
+
def validate_params(cls, params):
|
|
40
|
+
webhook = params.get("webhook", None) or mlrun.get_secret_or_env(
|
|
41
|
+
"SLACK_WEBHOOK"
|
|
42
|
+
)
|
|
43
|
+
if not webhook:
|
|
44
|
+
raise ValueError("Parameter 'webhook' is required for SlackNotification")
|
|
45
|
+
|
|
37
46
|
async def push(
|
|
38
47
|
self,
|
|
39
48
|
message: str,
|
|
@@ -42,6 +51,8 @@ class SlackNotification(NotificationBase):
|
|
|
42
51
|
] = mlrun.common.schemas.NotificationSeverity.INFO,
|
|
43
52
|
runs: typing.Union[mlrun.lists.RunList, list] = None,
|
|
44
53
|
custom_html: str = None,
|
|
54
|
+
alert: mlrun.common.schemas.AlertConfig = None,
|
|
55
|
+
event_data: mlrun.common.schemas.Event = None,
|
|
45
56
|
):
|
|
46
57
|
webhook = self.params.get("webhook", None) or mlrun.get_secret_or_env(
|
|
47
58
|
"SLACK_WEBHOOK"
|
|
@@ -53,7 +64,7 @@ class SlackNotification(NotificationBase):
|
|
|
53
64
|
)
|
|
54
65
|
return
|
|
55
66
|
|
|
56
|
-
data = self._generate_slack_data(message, severity, runs)
|
|
67
|
+
data = self._generate_slack_data(message, severity, runs, alert, event_data)
|
|
57
68
|
|
|
58
69
|
async with aiohttp.ClientSession() as session:
|
|
59
70
|
async with session.post(webhook, json=data) as response:
|
|
@@ -66,57 +77,121 @@ class SlackNotification(NotificationBase):
|
|
|
66
77
|
mlrun.common.schemas.NotificationSeverity, str
|
|
67
78
|
] = mlrun.common.schemas.NotificationSeverity.INFO,
|
|
68
79
|
runs: typing.Union[mlrun.lists.RunList, list] = None,
|
|
80
|
+
alert: mlrun.common.schemas.AlertConfig = None,
|
|
81
|
+
event_data: mlrun.common.schemas.Event = None,
|
|
69
82
|
) -> dict:
|
|
70
83
|
data = {
|
|
71
|
-
"blocks":
|
|
72
|
-
{
|
|
73
|
-
"type": "section",
|
|
74
|
-
"text": self._get_slack_row(f"[{severity}] {message}"),
|
|
75
|
-
},
|
|
76
|
-
]
|
|
84
|
+
"blocks": self._generate_slack_header_blocks(severity, message),
|
|
77
85
|
}
|
|
78
86
|
if self.name:
|
|
79
87
|
data["blocks"].append(
|
|
80
88
|
{"type": "section", "text": self._get_slack_row(self.name)}
|
|
81
89
|
)
|
|
82
90
|
|
|
83
|
-
if
|
|
84
|
-
|
|
91
|
+
if alert:
|
|
92
|
+
fields = self._get_alert_fields(alert, event_data)
|
|
85
93
|
|
|
86
|
-
|
|
87
|
-
|
|
94
|
+
for i in range(len(fields)):
|
|
95
|
+
data["blocks"].append({"type": "section", "text": fields[i]})
|
|
96
|
+
else:
|
|
97
|
+
if not runs:
|
|
98
|
+
return data
|
|
99
|
+
|
|
100
|
+
if isinstance(runs, list):
|
|
101
|
+
runs = mlrun.lists.RunList(runs)
|
|
88
102
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
103
|
+
fields = [self._get_slack_row("*Runs*"), self._get_slack_row("*Results*")]
|
|
104
|
+
for run in runs:
|
|
105
|
+
fields.append(self._get_run_line(run))
|
|
106
|
+
fields.append(self._get_run_result(run))
|
|
93
107
|
|
|
94
|
-
|
|
95
|
-
|
|
108
|
+
for i in range(0, len(fields), 8):
|
|
109
|
+
data["blocks"].append({"type": "section", "fields": fields[i : i + 8]})
|
|
96
110
|
|
|
97
111
|
return data
|
|
98
112
|
|
|
113
|
+
def _generate_slack_header_blocks(self, severity: str, message: str):
|
|
114
|
+
header_text = block_text = f"[{severity}] {message}"
|
|
115
|
+
section_text = None
|
|
116
|
+
|
|
117
|
+
# Slack doesn't allow headers to be longer than 150 characters
|
|
118
|
+
# If there's a comma in the message, split the message at the comma
|
|
119
|
+
# Otherwise, split the message at 150 characters
|
|
120
|
+
if len(block_text) > 150:
|
|
121
|
+
if ", " in block_text and block_text.index(", ") < 149:
|
|
122
|
+
header_text = block_text.split(",")[0]
|
|
123
|
+
section_text = block_text[len(header_text) + 2 :]
|
|
124
|
+
else:
|
|
125
|
+
header_text = block_text[:150]
|
|
126
|
+
section_text = block_text[150:]
|
|
127
|
+
blocks = [
|
|
128
|
+
{"type": "header", "text": {"type": "plain_text", "text": header_text}}
|
|
129
|
+
]
|
|
130
|
+
if section_text:
|
|
131
|
+
blocks.append(
|
|
132
|
+
{
|
|
133
|
+
"type": "section",
|
|
134
|
+
"text": self._get_slack_row(section_text),
|
|
135
|
+
}
|
|
136
|
+
)
|
|
137
|
+
return blocks
|
|
138
|
+
|
|
139
|
+
def _get_alert_fields(
|
|
140
|
+
self,
|
|
141
|
+
alert: mlrun.common.schemas.AlertConfig,
|
|
142
|
+
event_data: mlrun.common.schemas.Event,
|
|
143
|
+
) -> list:
|
|
144
|
+
line = [
|
|
145
|
+
self._get_slack_row(f":bell: {alert.name} alert has occurred"),
|
|
146
|
+
self._get_slack_row(f"*Project:*\n{alert.project}"),
|
|
147
|
+
self._get_slack_row(f"*ID:*\n{event_data.entity.ids[0]}"),
|
|
148
|
+
]
|
|
149
|
+
|
|
150
|
+
if alert.summary:
|
|
151
|
+
line.append(
|
|
152
|
+
self._get_slack_row(
|
|
153
|
+
f"*Summary:*\n{mlrun.utils.helpers.format_alert_summary(alert, event_data)}"
|
|
154
|
+
)
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
if event_data.value_dict:
|
|
158
|
+
data_lines = []
|
|
159
|
+
for key, value in event_data.value_dict.items():
|
|
160
|
+
data_lines.append(f"{key}: {value}")
|
|
161
|
+
data_text = "\n".join(data_lines)
|
|
162
|
+
line.append(self._get_slack_row(f"*Event data:*\n{data_text}"))
|
|
163
|
+
|
|
164
|
+
overview_type, url = self._get_overview_type_and_url(alert, event_data)
|
|
165
|
+
line.append(self._get_slack_row(f"*Overview:*\n<{url}|*{overview_type}*>"))
|
|
166
|
+
|
|
167
|
+
return line
|
|
168
|
+
|
|
99
169
|
def _get_run_line(self, run: dict) -> dict:
|
|
100
170
|
meta = run["metadata"]
|
|
101
171
|
url = mlrun.utils.helpers.get_ui_url(meta.get("project"), meta.get("uid"))
|
|
102
|
-
|
|
172
|
+
|
|
173
|
+
# Only show the URL if the run is not a function (serving or mlrun function)
|
|
174
|
+
kind = run.get("step_kind")
|
|
175
|
+
state = run["status"].get("state", "")
|
|
176
|
+
if state != "skipped" and (url and not kind or kind == "run"):
|
|
103
177
|
line = f'<{url}|*{meta.get("name")}*>'
|
|
104
178
|
else:
|
|
105
179
|
line = meta.get("name")
|
|
106
|
-
|
|
180
|
+
if kind:
|
|
181
|
+
line = f'{line} *({run.get("step_kind", run.get("kind", ""))})*'
|
|
107
182
|
line = f'{self.emojis.get(state, ":question:")} {line}'
|
|
108
183
|
return self._get_slack_row(line)
|
|
109
184
|
|
|
110
185
|
def _get_run_result(self, run: dict) -> dict:
|
|
111
186
|
state = run["status"].get("state", "")
|
|
112
187
|
if state == "error":
|
|
113
|
-
error_status = run["status"].get("error", "")
|
|
188
|
+
error_status = run["status"].get("error", "") or state
|
|
114
189
|
result = f"*{error_status}*"
|
|
115
190
|
else:
|
|
116
191
|
result = mlrun.utils.helpers.dict_to_str(
|
|
117
192
|
run["status"].get("results", {}), ", "
|
|
118
193
|
)
|
|
119
|
-
return self._get_slack_row(result or
|
|
194
|
+
return self._get_slack_row(result or state)
|
|
120
195
|
|
|
121
196
|
@staticmethod
|
|
122
197
|
def _get_slack_row(text: str) -> dict:
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
# See the License for the specific language governing permissions and
|
|
13
13
|
# limitations under the License.
|
|
14
14
|
|
|
15
|
+
import re
|
|
15
16
|
import typing
|
|
16
17
|
|
|
17
18
|
import aiohttp
|
|
@@ -28,6 +29,12 @@ class WebhookNotification(NotificationBase):
|
|
|
28
29
|
API/Client notification for sending run statuses in a http request
|
|
29
30
|
"""
|
|
30
31
|
|
|
32
|
+
@classmethod
|
|
33
|
+
def validate_params(cls, params):
|
|
34
|
+
url = params.get("url", None)
|
|
35
|
+
if not url:
|
|
36
|
+
raise ValueError("Parameter 'url' is required for WebhookNotification")
|
|
37
|
+
|
|
31
38
|
async def push(
|
|
32
39
|
self,
|
|
33
40
|
message: str,
|
|
@@ -36,6 +43,8 @@ class WebhookNotification(NotificationBase):
|
|
|
36
43
|
] = mlrun.common.schemas.NotificationSeverity.INFO,
|
|
37
44
|
runs: typing.Union[mlrun.lists.RunList, list] = None,
|
|
38
45
|
custom_html: str = None,
|
|
46
|
+
alert: mlrun.common.schemas.AlertConfig = None,
|
|
47
|
+
event_data: mlrun.common.schemas.Event = None,
|
|
39
48
|
):
|
|
40
49
|
url = self.params.get("url", None)
|
|
41
50
|
method = self.params.get("method", "post").lower()
|
|
@@ -46,14 +55,29 @@ class WebhookNotification(NotificationBase):
|
|
|
46
55
|
request_body = {
|
|
47
56
|
"message": message,
|
|
48
57
|
"severity": severity,
|
|
49
|
-
"runs": runs,
|
|
50
58
|
}
|
|
51
59
|
|
|
60
|
+
if runs:
|
|
61
|
+
request_body["runs"] = runs
|
|
62
|
+
|
|
63
|
+
if alert:
|
|
64
|
+
request_body["name"] = alert.name
|
|
65
|
+
request_body["project"] = alert.project
|
|
66
|
+
request_body["severity"] = alert.severity
|
|
67
|
+
if alert.summary:
|
|
68
|
+
request_body["summary"] = mlrun.utils.helpers.format_alert_summary(
|
|
69
|
+
alert, event_data
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
if event_data:
|
|
73
|
+
request_body["value"] = event_data.value_dict
|
|
74
|
+
request_body["id"] = event_data.entity.ids[0]
|
|
75
|
+
|
|
52
76
|
if custom_html:
|
|
53
77
|
request_body["custom_html"] = custom_html
|
|
54
78
|
|
|
55
79
|
if override_body:
|
|
56
|
-
request_body = override_body
|
|
80
|
+
request_body = self._serialize_runs_in_request_body(override_body, runs)
|
|
57
81
|
|
|
58
82
|
# Specify the `verify_ssl` parameter value only for HTTPS urls.
|
|
59
83
|
# The `ClientSession` allows using `ssl=None` for the default SSL check,
|
|
@@ -67,3 +91,40 @@ class WebhookNotification(NotificationBase):
|
|
|
67
91
|
url, headers=headers, json=request_body, ssl=verify_ssl
|
|
68
92
|
)
|
|
69
93
|
response.raise_for_status()
|
|
94
|
+
|
|
95
|
+
@staticmethod
|
|
96
|
+
def _serialize_runs_in_request_body(override_body, 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
|
+
"status": {"state": run["status"]["state"]},
|
|
109
|
+
}
|
|
110
|
+
if host := run["metadata"].get("labels", {}).get("host", ""):
|
|
111
|
+
parsed_run["host"] = host
|
|
112
|
+
if error := run["status"].get("error"):
|
|
113
|
+
parsed_run["status"]["error"] = error
|
|
114
|
+
elif results := run["status"].get("results"):
|
|
115
|
+
parsed_run["status"]["results"] = results
|
|
116
|
+
parsed_runs.append(parsed_run)
|
|
117
|
+
return str(parsed_runs)
|
|
118
|
+
|
|
119
|
+
if isinstance(override_body, dict):
|
|
120
|
+
for key, value in override_body.items():
|
|
121
|
+
if not isinstance(value, str):
|
|
122
|
+
# If the value is not a string, we don't want to parse it
|
|
123
|
+
continue
|
|
124
|
+
if re.search(r"{{\s*runs\s*}}", value):
|
|
125
|
+
str_parsed_runs = parse_runs()
|
|
126
|
+
override_body[key] = re.sub(
|
|
127
|
+
r"{{\s*runs\s*}}", str_parsed_runs, value
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
return override_body
|