mlrun 1.8.0rc1__py3-none-any.whl → 1.8.0rc3__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 +5 -7
- mlrun/__main__.py +1 -1
- mlrun/artifacts/__init__.py +1 -0
- mlrun/artifacts/document.py +313 -0
- mlrun/artifacts/manager.py +2 -0
- mlrun/common/formatters/project.py +9 -0
- mlrun/common/schemas/__init__.py +4 -0
- mlrun/common/schemas/alert.py +31 -18
- mlrun/common/schemas/api_gateway.py +3 -3
- mlrun/common/schemas/artifact.py +7 -7
- mlrun/common/schemas/auth.py +6 -4
- mlrun/common/schemas/background_task.py +7 -7
- mlrun/common/schemas/client_spec.py +2 -2
- mlrun/common/schemas/clusterization_spec.py +2 -2
- mlrun/common/schemas/common.py +5 -5
- 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/grafana.py +1 -1
- mlrun/common/schemas/model_monitoring/model_endpoint_v2.py +1 -1
- mlrun/common/schemas/model_monitoring/model_endpoints.py +1 -1
- mlrun/common/schemas/notification.py +18 -3
- mlrun/common/schemas/object.py +1 -1
- mlrun/common/schemas/pagination.py +4 -4
- mlrun/common/schemas/partition.py +16 -1
- mlrun/common/schemas/pipeline.py +2 -2
- mlrun/common/schemas/project.py +22 -17
- mlrun/common/schemas/runs.py +2 -2
- mlrun/common/schemas/runtime_resource.py +5 -5
- mlrun/common/schemas/schedule.py +1 -1
- mlrun/common/schemas/secret.py +1 -1
- mlrun/common/schemas/tag.py +3 -3
- mlrun/common/schemas/workflow.py +5 -5
- mlrun/config.py +23 -1
- mlrun/datastore/datastore_profile.py +38 -19
- mlrun/datastore/vectorstore.py +186 -0
- mlrun/db/base.py +58 -6
- mlrun/db/httpdb.py +267 -15
- mlrun/db/nopdb.py +44 -5
- mlrun/execution.py +47 -1
- mlrun/model.py +2 -2
- mlrun/model_monitoring/applications/results.py +2 -2
- mlrun/model_monitoring/db/tsdb/base.py +2 -2
- mlrun/model_monitoring/db/tsdb/tdengine/schemas.py +37 -13
- mlrun/model_monitoring/db/tsdb/tdengine/tdengine_connector.py +32 -40
- mlrun/model_monitoring/helpers.py +4 -10
- mlrun/model_monitoring/stream_processing.py +14 -11
- mlrun/platforms/__init__.py +44 -13
- mlrun/projects/__init__.py +6 -1
- mlrun/projects/pipelines.py +184 -55
- mlrun/projects/project.py +309 -33
- mlrun/run.py +4 -1
- mlrun/runtimes/base.py +2 -1
- mlrun/runtimes/mounts.py +572 -0
- mlrun/runtimes/nuclio/function.py +1 -2
- mlrun/runtimes/pod.py +82 -18
- mlrun/runtimes/remotesparkjob.py +1 -1
- mlrun/runtimes/sparkjob/spark3job.py +1 -1
- mlrun/utils/clones.py +1 -1
- mlrun/utils/helpers.py +12 -2
- mlrun/utils/logger.py +2 -2
- mlrun/utils/notifications/notification/__init__.py +22 -19
- mlrun/utils/notifications/notification/base.py +12 -12
- mlrun/utils/notifications/notification/console.py +6 -6
- mlrun/utils/notifications/notification/git.py +6 -6
- mlrun/utils/notifications/notification/ipython.py +6 -6
- mlrun/utils/notifications/notification/mail.py +149 -0
- mlrun/utils/notifications/notification/slack.py +6 -6
- mlrun/utils/notifications/notification/webhook.py +6 -6
- mlrun/utils/notifications/notification_pusher.py +20 -12
- mlrun/utils/regex.py +2 -0
- mlrun/utils/version/version.json +2 -2
- {mlrun-1.8.0rc1.dist-info → mlrun-1.8.0rc3.dist-info}/METADATA +190 -186
- {mlrun-1.8.0rc1.dist-info → mlrun-1.8.0rc3.dist-info}/RECORD +83 -79
- {mlrun-1.8.0rc1.dist-info → mlrun-1.8.0rc3.dist-info}/WHEEL +1 -1
- {mlrun-1.8.0rc1.dist-info → mlrun-1.8.0rc3.dist-info}/LICENSE +0 -0
- {mlrun-1.8.0rc1.dist-info → mlrun-1.8.0rc3.dist-info}/entry_points.txt +0 -0
- {mlrun-1.8.0rc1.dist-info → mlrun-1.8.0rc3.dist-info}/top_level.txt +0 -0
mlrun/runtimes/pod.py
CHANGED
|
@@ -17,21 +17,22 @@ import os
|
|
|
17
17
|
import re
|
|
18
18
|
import time
|
|
19
19
|
import typing
|
|
20
|
+
from collections.abc import Iterable
|
|
20
21
|
from enum import Enum
|
|
21
22
|
|
|
22
23
|
import dotenv
|
|
23
24
|
import kubernetes.client as k8s_client
|
|
25
|
+
from kubernetes.client import V1Volume, V1VolumeMount
|
|
24
26
|
|
|
25
27
|
import mlrun.common.constants
|
|
26
28
|
import mlrun.errors
|
|
29
|
+
import mlrun.runtimes.mounts
|
|
27
30
|
import mlrun.utils.regex
|
|
28
|
-
import mlrun_pipelines.mounts
|
|
29
31
|
from mlrun.common.schemas import (
|
|
30
32
|
NodeSelectorOperator,
|
|
31
33
|
PreemptionModes,
|
|
32
34
|
SecurityContextEnrichmentModes,
|
|
33
35
|
)
|
|
34
|
-
from mlrun_pipelines.mixins import KfpAdapterMixin
|
|
35
36
|
|
|
36
37
|
from ..config import config as mlconf
|
|
37
38
|
from ..k8s_utils import (
|
|
@@ -367,6 +368,35 @@ class KubeResourceSpec(FunctionSpec):
|
|
|
367
368
|
+ f"service accounts {allowed_service_accounts}"
|
|
368
369
|
)
|
|
369
370
|
|
|
371
|
+
def with_volumes(
|
|
372
|
+
self,
|
|
373
|
+
volumes: typing.Union[list[dict], dict, V1Volume],
|
|
374
|
+
) -> "KubeResourceSpec":
|
|
375
|
+
"""Add volumes to the volumes dictionary, only used as part of the mlrun_pipelines mount functions."""
|
|
376
|
+
if isinstance(volumes, dict):
|
|
377
|
+
set_named_item(self._volumes, volumes)
|
|
378
|
+
elif isinstance(volumes, Iterable):
|
|
379
|
+
for volume in volumes:
|
|
380
|
+
set_named_item(self._volumes, volume)
|
|
381
|
+
else:
|
|
382
|
+
set_named_item(self._volumes, volumes)
|
|
383
|
+
return self
|
|
384
|
+
|
|
385
|
+
def with_volume_mounts(
|
|
386
|
+
self,
|
|
387
|
+
volume_mounts: typing.Union[list[dict], dict, V1VolumeMount],
|
|
388
|
+
) -> "KubeResourceSpec":
|
|
389
|
+
"""Add volume mounts to the volume mounts dictionary,
|
|
390
|
+
only used as part of the mlrun_pipelines mount functions."""
|
|
391
|
+
if isinstance(volume_mounts, dict):
|
|
392
|
+
self._set_volume_mount(volume_mounts)
|
|
393
|
+
elif isinstance(volume_mounts, Iterable):
|
|
394
|
+
for volume_mount in volume_mounts:
|
|
395
|
+
self._set_volume_mount(volume_mount)
|
|
396
|
+
else:
|
|
397
|
+
self._set_volume_mount(volume_mounts)
|
|
398
|
+
return self
|
|
399
|
+
|
|
370
400
|
def _set_volume_mount(
|
|
371
401
|
self, volume_mount, volume_mounts_field_name="_volume_mounts"
|
|
372
402
|
):
|
|
@@ -942,12 +972,12 @@ class AutoMountType(str, Enum):
|
|
|
942
972
|
@classmethod
|
|
943
973
|
def all_mount_modifiers(cls):
|
|
944
974
|
return [
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
975
|
+
mlrun.runtimes.mounts.v3io_cred.__name__,
|
|
976
|
+
mlrun.runtimes.mounts.mount_v3io.__name__,
|
|
977
|
+
mlrun.runtimes.mounts.mount_pvc.__name__,
|
|
978
|
+
mlrun.runtimes.mounts.auto_mount.__name__,
|
|
979
|
+
mlrun.runtimes.mounts.mount_s3.__name__,
|
|
980
|
+
mlrun.runtimes.mounts.set_env_variables.__name__,
|
|
951
981
|
]
|
|
952
982
|
|
|
953
983
|
@classmethod
|
|
@@ -964,27 +994,27 @@ class AutoMountType(str, Enum):
|
|
|
964
994
|
def _get_auto_modifier():
|
|
965
995
|
# If we're running on Iguazio - use v3io_cred
|
|
966
996
|
if mlconf.igz_version != "":
|
|
967
|
-
return
|
|
997
|
+
return mlrun.runtimes.mounts.v3io_cred
|
|
968
998
|
# Else, either pvc mount if it's configured or do nothing otherwise
|
|
969
999
|
pvc_configured = (
|
|
970
1000
|
"MLRUN_PVC_MOUNT" in os.environ
|
|
971
1001
|
or "pvc_name" in mlconf.get_storage_auto_mount_params()
|
|
972
1002
|
)
|
|
973
|
-
return
|
|
1003
|
+
return mlrun.runtimes.mounts.mount_pvc if pvc_configured else None
|
|
974
1004
|
|
|
975
1005
|
def get_modifier(self):
|
|
976
1006
|
return {
|
|
977
1007
|
AutoMountType.none: None,
|
|
978
|
-
AutoMountType.v3io_credentials:
|
|
979
|
-
AutoMountType.v3io_fuse:
|
|
980
|
-
AutoMountType.pvc:
|
|
1008
|
+
AutoMountType.v3io_credentials: mlrun.runtimes.mounts.v3io_cred,
|
|
1009
|
+
AutoMountType.v3io_fuse: mlrun.runtimes.mounts.mount_v3io,
|
|
1010
|
+
AutoMountType.pvc: mlrun.runtimes.mounts.mount_pvc,
|
|
981
1011
|
AutoMountType.auto: self._get_auto_modifier(),
|
|
982
|
-
AutoMountType.s3:
|
|
983
|
-
AutoMountType.env:
|
|
1012
|
+
AutoMountType.s3: mlrun.runtimes.mounts.mount_s3,
|
|
1013
|
+
AutoMountType.env: mlrun.runtimes.mounts.set_env_variables,
|
|
984
1014
|
}[self]
|
|
985
1015
|
|
|
986
1016
|
|
|
987
|
-
class KubeResource(BaseRuntime
|
|
1017
|
+
class KubeResource(BaseRuntime):
|
|
988
1018
|
"""
|
|
989
1019
|
A parent class for runtimes that generate k8s resources when executing.
|
|
990
1020
|
"""
|
|
@@ -1026,7 +1056,8 @@ class KubeResource(BaseRuntime, KfpAdapterMixin):
|
|
|
1026
1056
|
|
|
1027
1057
|
def get_env(self, name, default=None):
|
|
1028
1058
|
"""Get the pod environment variable for the given name, if not found return the default
|
|
1029
|
-
If it's a scalar value, will return it, if the value is from source, return the k8s struct (V1EnvVarSource)
|
|
1059
|
+
If it's a scalar value, will return it, if the value is from source, return the k8s struct (V1EnvVarSource)
|
|
1060
|
+
"""
|
|
1030
1061
|
for env_var in self.spec.env:
|
|
1031
1062
|
if get_item_name(env_var) == name:
|
|
1032
1063
|
# valueFrom is a workaround for now, for some reason the envs aren't getting sanitized
|
|
@@ -1080,7 +1111,10 @@ class KubeResource(BaseRuntime, KfpAdapterMixin):
|
|
|
1080
1111
|
else:
|
|
1081
1112
|
raise mlrun.errors.MLRunNotFoundError(f"{file_path} does not exist")
|
|
1082
1113
|
for name, value in env_vars.items():
|
|
1083
|
-
|
|
1114
|
+
if isinstance(value, dict) and "valueFrom" in value:
|
|
1115
|
+
self.set_env(name, value_from=value["valueFrom"])
|
|
1116
|
+
else:
|
|
1117
|
+
self.set_env(name, value)
|
|
1084
1118
|
return self
|
|
1085
1119
|
|
|
1086
1120
|
def set_image_pull_configuration(
|
|
@@ -1270,6 +1304,36 @@ class KubeResource(BaseRuntime, KfpAdapterMixin):
|
|
|
1270
1304
|
)
|
|
1271
1305
|
self.spec.security_context = security_context
|
|
1272
1306
|
|
|
1307
|
+
def apply(
|
|
1308
|
+
self,
|
|
1309
|
+
modifier: typing.Callable[["KubeResource"], "KubeResource"],
|
|
1310
|
+
) -> "KubeResource":
|
|
1311
|
+
"""
|
|
1312
|
+
Apply a modifier to the runtime which is used to change the runtimes k8s object's spec.
|
|
1313
|
+
All modifiers accept Kube, apply some changes on its spec and return it so modifiers can be chained
|
|
1314
|
+
one after the other.
|
|
1315
|
+
|
|
1316
|
+
:param modifier: a modifier callable object
|
|
1317
|
+
:return: the runtime (self) after the modifications
|
|
1318
|
+
"""
|
|
1319
|
+
modifier(self)
|
|
1320
|
+
if AutoMountType.is_auto_modifier(modifier):
|
|
1321
|
+
self.spec.disable_auto_mount = True
|
|
1322
|
+
|
|
1323
|
+
api_client = k8s_client.ApiClient()
|
|
1324
|
+
if self.spec.env:
|
|
1325
|
+
for index, env in enumerate(
|
|
1326
|
+
api_client.sanitize_for_serialization(self.spec.env)
|
|
1327
|
+
):
|
|
1328
|
+
self.spec.env[index] = env
|
|
1329
|
+
|
|
1330
|
+
if self.spec.volumes and self.spec.volume_mounts:
|
|
1331
|
+
vols = api_client.sanitize_for_serialization(self.spec.volumes)
|
|
1332
|
+
mounts = api_client.sanitize_for_serialization(self.spec.volume_mounts)
|
|
1333
|
+
self.spec.update_vols_and_mounts(vols, mounts)
|
|
1334
|
+
|
|
1335
|
+
return self
|
|
1336
|
+
|
|
1273
1337
|
def list_valid_priority_class_names(self):
|
|
1274
1338
|
return mlconf.get_valid_function_priority_class_names()
|
|
1275
1339
|
|
mlrun/runtimes/remotesparkjob.py
CHANGED
|
@@ -19,7 +19,7 @@ import kubernetes.client
|
|
|
19
19
|
|
|
20
20
|
import mlrun.errors
|
|
21
21
|
from mlrun.config import config
|
|
22
|
-
from
|
|
22
|
+
from mlrun.runtimes.mounts import mount_v3io, mount_v3iod
|
|
23
23
|
|
|
24
24
|
from .kubejob import KubejobRuntime
|
|
25
25
|
from .pod import KubeResourceSpec
|
|
@@ -20,7 +20,7 @@ import mlrun.errors
|
|
|
20
20
|
import mlrun.k8s_utils
|
|
21
21
|
import mlrun.runtimes.pod
|
|
22
22
|
from mlrun.config import config
|
|
23
|
-
from
|
|
23
|
+
from mlrun.runtimes.mounts import mount_v3io, mount_v3iod
|
|
24
24
|
|
|
25
25
|
from ...execution import MLClientCtx
|
|
26
26
|
from ...model import RunObject
|
mlrun/utils/clones.py
CHANGED
|
@@ -122,7 +122,7 @@ def add_credentials_git_remote_url(url: str, secrets=None) -> tuple[str, bool]:
|
|
|
122
122
|
username, password = get_git_username_password_from_token(token)
|
|
123
123
|
|
|
124
124
|
if username:
|
|
125
|
-
return f"https://{username}:{password}@{url_obj.
|
|
125
|
+
return f"https://{username}:{password}@{url_obj.netloc}{url_obj.path}", True
|
|
126
126
|
return url, False
|
|
127
127
|
|
|
128
128
|
|
mlrun/utils/helpers.py
CHANGED
|
@@ -1265,14 +1265,24 @@ def datetime_to_iso(time_obj: Optional[datetime]) -> Optional[str]:
|
|
|
1265
1265
|
return time_obj.isoformat()
|
|
1266
1266
|
|
|
1267
1267
|
|
|
1268
|
-
def enrich_datetime_with_tz_info(timestamp_string):
|
|
1268
|
+
def enrich_datetime_with_tz_info(timestamp_string) -> Optional[datetime]:
|
|
1269
1269
|
if not timestamp_string:
|
|
1270
1270
|
return timestamp_string
|
|
1271
1271
|
|
|
1272
1272
|
if timestamp_string and not mlrun.utils.helpers.has_timezone(timestamp_string):
|
|
1273
1273
|
timestamp_string += datetime.now(timezone.utc).astimezone().strftime("%z")
|
|
1274
1274
|
|
|
1275
|
-
|
|
1275
|
+
for _format in [
|
|
1276
|
+
# e.g: 2021-08-25 12:00:00.000Z
|
|
1277
|
+
"%Y-%m-%d %H:%M:%S.%f%z",
|
|
1278
|
+
# e.g: 2024-11-11 07:44:56+0000
|
|
1279
|
+
"%Y-%m-%d %H:%M:%S%z",
|
|
1280
|
+
]:
|
|
1281
|
+
try:
|
|
1282
|
+
return datetime.strptime(timestamp_string, _format)
|
|
1283
|
+
except ValueError as exc:
|
|
1284
|
+
last_exc = exc
|
|
1285
|
+
raise last_exc
|
|
1276
1286
|
|
|
1277
1287
|
|
|
1278
1288
|
def has_timezone(timestamp):
|
mlrun/utils/logger.py
CHANGED
|
@@ -24,7 +24,7 @@ from traceback import format_exception
|
|
|
24
24
|
from typing import IO, Optional, Union
|
|
25
25
|
|
|
26
26
|
import orjson
|
|
27
|
-
import pydantic
|
|
27
|
+
import pydantic.v1
|
|
28
28
|
|
|
29
29
|
from mlrun import errors
|
|
30
30
|
from mlrun.config import config
|
|
@@ -33,7 +33,7 @@ from mlrun.config import config
|
|
|
33
33
|
class _BaseFormatter(logging.Formatter):
|
|
34
34
|
def _json_dump(self, json_object):
|
|
35
35
|
def default(obj):
|
|
36
|
-
if isinstance(obj, pydantic.BaseModel):
|
|
36
|
+
if isinstance(obj, pydantic.v1.BaseModel):
|
|
37
37
|
return obj.dict()
|
|
38
38
|
|
|
39
39
|
# EAFP all the way.
|
|
@@ -14,30 +14,32 @@
|
|
|
14
14
|
|
|
15
15
|
import enum
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
17
|
+
import mlrun.common.schemas.notification as notifications
|
|
18
|
+
import mlrun.utils.notifications.notification.base as base
|
|
19
|
+
import mlrun.utils.notifications.notification.console as console
|
|
20
|
+
import mlrun.utils.notifications.notification.git as git
|
|
21
|
+
import mlrun.utils.notifications.notification.ipython as ipython
|
|
22
|
+
import mlrun.utils.notifications.notification.mail as mail
|
|
23
|
+
import mlrun.utils.notifications.notification.slack as slack
|
|
24
|
+
import mlrun.utils.notifications.notification.webhook as webhook
|
|
25
25
|
|
|
26
26
|
|
|
27
27
|
class NotificationTypes(str, enum.Enum):
|
|
28
|
-
console = NotificationKind.console.value
|
|
29
|
-
git = NotificationKind.git.value
|
|
30
|
-
ipython = NotificationKind.ipython.value
|
|
31
|
-
slack = NotificationKind.slack.value
|
|
32
|
-
|
|
28
|
+
console = notifications.NotificationKind.console.value
|
|
29
|
+
git = notifications.NotificationKind.git.value
|
|
30
|
+
ipython = notifications.NotificationKind.ipython.value
|
|
31
|
+
slack = notifications.NotificationKind.slack.value
|
|
32
|
+
mail = notifications.NotificationKind.mail.value
|
|
33
|
+
webhook = notifications.NotificationKind.webhook.value
|
|
33
34
|
|
|
34
|
-
def get_notification(self) -> type[NotificationBase]:
|
|
35
|
+
def get_notification(self) -> type[base.NotificationBase]:
|
|
35
36
|
return {
|
|
36
|
-
self.console: ConsoleNotification,
|
|
37
|
-
self.git: GitNotification,
|
|
38
|
-
self.ipython: IPythonNotification,
|
|
39
|
-
self.slack: SlackNotification,
|
|
40
|
-
self.
|
|
37
|
+
self.console: console.ConsoleNotification,
|
|
38
|
+
self.git: git.GitNotification,
|
|
39
|
+
self.ipython: ipython.IPythonNotification,
|
|
40
|
+
self.slack: slack.SlackNotification,
|
|
41
|
+
self.mail: mail.MailNotification,
|
|
42
|
+
self.webhook: webhook.WebhookNotification,
|
|
41
43
|
}.get(self)
|
|
42
44
|
|
|
43
45
|
def inverse_dependencies(self) -> list[str]:
|
|
@@ -64,5 +66,6 @@ class NotificationTypes(str, enum.Enum):
|
|
|
64
66
|
cls.git,
|
|
65
67
|
cls.ipython,
|
|
66
68
|
cls.slack,
|
|
69
|
+
cls.mail,
|
|
67
70
|
cls.webhook,
|
|
68
71
|
]
|
|
@@ -53,13 +53,13 @@ class NotificationBase:
|
|
|
53
53
|
def push(
|
|
54
54
|
self,
|
|
55
55
|
message: str,
|
|
56
|
-
severity: typing.
|
|
57
|
-
mlrun.common.schemas.NotificationSeverity, str
|
|
56
|
+
severity: typing.Optional[
|
|
57
|
+
typing.Union[mlrun.common.schemas.NotificationSeverity, str]
|
|
58
58
|
] = mlrun.common.schemas.NotificationSeverity.INFO,
|
|
59
|
-
runs: typing.Union[mlrun.lists.RunList, list] = None,
|
|
60
|
-
custom_html: typing.Optional[str] = None,
|
|
61
|
-
alert: mlrun.common.schemas.AlertConfig = None,
|
|
62
|
-
event_data: mlrun.common.schemas.Event = None,
|
|
59
|
+
runs: typing.Optional[typing.Union[mlrun.lists.RunList, list]] = None,
|
|
60
|
+
custom_html: typing.Optional[typing.Optional[str]] = None,
|
|
61
|
+
alert: typing.Optional[mlrun.common.schemas.AlertConfig] = None,
|
|
62
|
+
event_data: typing.Optional[mlrun.common.schemas.Event] = None,
|
|
63
63
|
):
|
|
64
64
|
raise NotImplementedError()
|
|
65
65
|
|
|
@@ -81,13 +81,13 @@ class NotificationBase:
|
|
|
81
81
|
def _get_html(
|
|
82
82
|
self,
|
|
83
83
|
message: str,
|
|
84
|
-
severity: typing.
|
|
85
|
-
mlrun.common.schemas.NotificationSeverity, str
|
|
84
|
+
severity: typing.Optional[
|
|
85
|
+
typing.Union[mlrun.common.schemas.NotificationSeverity, str]
|
|
86
86
|
] = mlrun.common.schemas.NotificationSeverity.INFO,
|
|
87
|
-
runs: typing.Union[mlrun.lists.RunList, list] = None,
|
|
88
|
-
custom_html: typing.Optional[str] = None,
|
|
89
|
-
alert: mlrun.common.schemas.AlertConfig = None,
|
|
90
|
-
event_data: mlrun.common.schemas.Event = None,
|
|
87
|
+
runs: typing.Optional[typing.Union[mlrun.lists.RunList, list]] = None,
|
|
88
|
+
custom_html: typing.Optional[typing.Optional[str]] = None,
|
|
89
|
+
alert: typing.Optional[mlrun.common.schemas.AlertConfig] = None,
|
|
90
|
+
event_data: typing.Optional[mlrun.common.schemas.Event] = None,
|
|
91
91
|
) -> str:
|
|
92
92
|
if custom_html:
|
|
93
93
|
return custom_html
|
|
@@ -31,13 +31,13 @@ class ConsoleNotification(NotificationBase):
|
|
|
31
31
|
def push(
|
|
32
32
|
self,
|
|
33
33
|
message: str,
|
|
34
|
-
severity: typing.
|
|
35
|
-
mlrun.common.schemas.NotificationSeverity, str
|
|
34
|
+
severity: typing.Optional[
|
|
35
|
+
typing.Union[mlrun.common.schemas.NotificationSeverity, str]
|
|
36
36
|
] = mlrun.common.schemas.NotificationSeverity.INFO,
|
|
37
|
-
runs: typing.Union[mlrun.lists.RunList, list] = None,
|
|
38
|
-
custom_html: typing.Optional[str] = None,
|
|
39
|
-
alert: mlrun.common.schemas.AlertConfig = None,
|
|
40
|
-
event_data: mlrun.common.schemas.Event = None,
|
|
37
|
+
runs: typing.Optional[typing.Union[mlrun.lists.RunList, list]] = None,
|
|
38
|
+
custom_html: typing.Optional[typing.Optional[str]] = None,
|
|
39
|
+
alert: typing.Optional[mlrun.common.schemas.AlertConfig] = None,
|
|
40
|
+
event_data: typing.Optional[mlrun.common.schemas.Event] = None,
|
|
41
41
|
):
|
|
42
42
|
severity = self._resolve_severity(severity)
|
|
43
43
|
print(f"[{severity}] {message}")
|
|
@@ -54,13 +54,13 @@ class GitNotification(NotificationBase):
|
|
|
54
54
|
async def push(
|
|
55
55
|
self,
|
|
56
56
|
message: str,
|
|
57
|
-
severity: typing.
|
|
58
|
-
mlrun.common.schemas.NotificationSeverity, str
|
|
57
|
+
severity: typing.Optional[
|
|
58
|
+
typing.Union[mlrun.common.schemas.NotificationSeverity, str]
|
|
59
59
|
] = mlrun.common.schemas.NotificationSeverity.INFO,
|
|
60
|
-
runs: typing.Union[mlrun.lists.RunList, list] = None,
|
|
61
|
-
custom_html: typing.Optional[str] = None,
|
|
62
|
-
alert: mlrun.common.schemas.AlertConfig = None,
|
|
63
|
-
event_data: mlrun.common.schemas.Event = None,
|
|
60
|
+
runs: typing.Optional[typing.Union[mlrun.lists.RunList, list]] = None,
|
|
61
|
+
custom_html: typing.Optional[typing.Optional[str]] = None,
|
|
62
|
+
alert: typing.Optional[mlrun.common.schemas.AlertConfig] = None,
|
|
63
|
+
event_data: typing.Optional[mlrun.common.schemas.Event] = None,
|
|
64
64
|
):
|
|
65
65
|
git_repo = self.params.get("repo", None)
|
|
66
66
|
git_issue = self.params.get("issue", None)
|
|
@@ -49,13 +49,13 @@ class IPythonNotification(NotificationBase):
|
|
|
49
49
|
def push(
|
|
50
50
|
self,
|
|
51
51
|
message: str,
|
|
52
|
-
severity: typing.
|
|
53
|
-
mlrun.common.schemas.NotificationSeverity, str
|
|
52
|
+
severity: typing.Optional[
|
|
53
|
+
typing.Union[mlrun.common.schemas.NotificationSeverity, str]
|
|
54
54
|
] = mlrun.common.schemas.NotificationSeverity.INFO,
|
|
55
|
-
runs: typing.Union[mlrun.lists.RunList, list] = None,
|
|
56
|
-
custom_html: typing.Optional[str] = None,
|
|
57
|
-
alert: mlrun.common.schemas.AlertConfig = None,
|
|
58
|
-
event_data: mlrun.common.schemas.Event = None,
|
|
55
|
+
runs: typing.Optional[typing.Union[mlrun.lists.RunList, list]] = None,
|
|
56
|
+
custom_html: typing.Optional[typing.Optional[str]] = None,
|
|
57
|
+
alert: typing.Optional[mlrun.common.schemas.AlertConfig] = None,
|
|
58
|
+
event_data: typing.Optional[mlrun.common.schemas.Event] = None,
|
|
59
59
|
):
|
|
60
60
|
if not self._ipython:
|
|
61
61
|
mlrun.utils.helpers.logger.debug(
|
|
@@ -0,0 +1,149 @@
|
|
|
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
|
+
import re
|
|
15
|
+
import typing
|
|
16
|
+
from email.message import EmailMessage
|
|
17
|
+
|
|
18
|
+
import aiosmtplib
|
|
19
|
+
|
|
20
|
+
import mlrun.common.schemas
|
|
21
|
+
import mlrun.lists
|
|
22
|
+
import mlrun.utils.helpers
|
|
23
|
+
import mlrun.utils.notifications.notification.base as base
|
|
24
|
+
import mlrun.utils.regex
|
|
25
|
+
|
|
26
|
+
DEFAULT_SMTP_PORT = 587
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class MailNotification(base.NotificationBase):
|
|
30
|
+
"""
|
|
31
|
+
API/Client notification for sending run statuses as a mail message
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
boolean_params = ["use_tls", "start_tls", "validate_certs"]
|
|
35
|
+
|
|
36
|
+
required_params = [
|
|
37
|
+
"server_host",
|
|
38
|
+
"server_port",
|
|
39
|
+
"sender_address",
|
|
40
|
+
"username",
|
|
41
|
+
"password",
|
|
42
|
+
"email_addresses",
|
|
43
|
+
] + boolean_params
|
|
44
|
+
|
|
45
|
+
@classmethod
|
|
46
|
+
def validate_params(cls, params):
|
|
47
|
+
for required_param in cls.required_params:
|
|
48
|
+
if required_param not in params:
|
|
49
|
+
raise ValueError(
|
|
50
|
+
f"Parameter '{required_param}' is required for MailNotification"
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
for boolean_param in cls.boolean_params:
|
|
54
|
+
if not isinstance(params.get(boolean_param, None), bool):
|
|
55
|
+
raise ValueError(
|
|
56
|
+
f"Parameter '{boolean_param}' must be a boolean for MailNotification"
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
cls._validate_emails(params)
|
|
60
|
+
|
|
61
|
+
async def push(
|
|
62
|
+
self,
|
|
63
|
+
message: str,
|
|
64
|
+
severity: typing.Optional[
|
|
65
|
+
typing.Union[mlrun.common.schemas.NotificationSeverity, str]
|
|
66
|
+
] = mlrun.common.schemas.NotificationSeverity.INFO,
|
|
67
|
+
runs: typing.Optional[typing.Union[mlrun.lists.RunList, list]] = None,
|
|
68
|
+
custom_html: typing.Optional[typing.Optional[str]] = None,
|
|
69
|
+
alert: typing.Optional[mlrun.common.schemas.AlertConfig] = None,
|
|
70
|
+
event_data: typing.Optional[mlrun.common.schemas.Event] = None,
|
|
71
|
+
):
|
|
72
|
+
self.params.setdefault("subject", f"[{severity}] {message}")
|
|
73
|
+
self.params.setdefault("body", message)
|
|
74
|
+
await self._send_email(**self.params)
|
|
75
|
+
|
|
76
|
+
@classmethod
|
|
77
|
+
def enrich_default_params(
|
|
78
|
+
cls, params: dict, default_params: typing.Optional[dict] = None
|
|
79
|
+
) -> dict:
|
|
80
|
+
params = super().enrich_default_params(params, default_params)
|
|
81
|
+
params.setdefault("use_tls", True)
|
|
82
|
+
params.setdefault("start_tls", False)
|
|
83
|
+
params.setdefault("validate_certs", True)
|
|
84
|
+
params.setdefault("server_port", DEFAULT_SMTP_PORT)
|
|
85
|
+
|
|
86
|
+
default_mail_address = params.pop("default_email_addresses", "")
|
|
87
|
+
email_addresses = params.get("email_addresses", default_mail_address)
|
|
88
|
+
if isinstance(email_addresses, list):
|
|
89
|
+
email_addresses = ",".join(email_addresses)
|
|
90
|
+
params["email_addresses"] = email_addresses
|
|
91
|
+
|
|
92
|
+
return params
|
|
93
|
+
|
|
94
|
+
@classmethod
|
|
95
|
+
def _validate_emails(cls, params):
|
|
96
|
+
cls._validate_email_address(params["sender_address"])
|
|
97
|
+
|
|
98
|
+
if not isinstance(params["email_addresses"], (str, list)):
|
|
99
|
+
raise ValueError(
|
|
100
|
+
"Parameter 'email_addresses' must be a string or a list of strings"
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
email_addresses = params["email_addresses"]
|
|
104
|
+
if isinstance(email_addresses, str):
|
|
105
|
+
email_addresses = email_addresses.split(",")
|
|
106
|
+
for email_address in email_addresses:
|
|
107
|
+
cls._validate_email_address(email_address)
|
|
108
|
+
|
|
109
|
+
@classmethod
|
|
110
|
+
def _validate_email_address(cls, email_address):
|
|
111
|
+
if not isinstance(email_address, str):
|
|
112
|
+
raise ValueError(f"Email address '{email_address}' must be a string")
|
|
113
|
+
|
|
114
|
+
if not re.match(mlrun.utils.regex.mail_regex, email_address):
|
|
115
|
+
raise ValueError(f"Invalid email address '{email_address}'")
|
|
116
|
+
|
|
117
|
+
@staticmethod
|
|
118
|
+
async def _send_email(
|
|
119
|
+
email_addresses: str,
|
|
120
|
+
sender_address: str,
|
|
121
|
+
server_host: str,
|
|
122
|
+
server_port: int,
|
|
123
|
+
username: str,
|
|
124
|
+
password: str,
|
|
125
|
+
use_tls: bool,
|
|
126
|
+
start_tls: bool,
|
|
127
|
+
validate_certs: bool,
|
|
128
|
+
subject: str,
|
|
129
|
+
body: str,
|
|
130
|
+
**kwargs,
|
|
131
|
+
):
|
|
132
|
+
# Create the email message
|
|
133
|
+
message = EmailMessage()
|
|
134
|
+
message["From"] = sender_address
|
|
135
|
+
message["To"] = email_addresses
|
|
136
|
+
message["Subject"] = subject
|
|
137
|
+
message.set_content(body)
|
|
138
|
+
|
|
139
|
+
# Send the email
|
|
140
|
+
await aiosmtplib.send(
|
|
141
|
+
message,
|
|
142
|
+
hostname=server_host,
|
|
143
|
+
port=server_port,
|
|
144
|
+
username=username,
|
|
145
|
+
password=password,
|
|
146
|
+
use_tls=use_tls,
|
|
147
|
+
validate_certs=validate_certs,
|
|
148
|
+
start_tls=start_tls,
|
|
149
|
+
)
|
|
@@ -46,13 +46,13 @@ class SlackNotification(NotificationBase):
|
|
|
46
46
|
async def push(
|
|
47
47
|
self,
|
|
48
48
|
message: str,
|
|
49
|
-
severity: typing.
|
|
50
|
-
mlrun.common.schemas.NotificationSeverity, str
|
|
49
|
+
severity: typing.Optional[
|
|
50
|
+
typing.Union[mlrun.common.schemas.NotificationSeverity, str]
|
|
51
51
|
] = mlrun.common.schemas.NotificationSeverity.INFO,
|
|
52
|
-
runs: typing.Union[mlrun.lists.RunList, list] = None,
|
|
53
|
-
custom_html: typing.Optional[str] = None,
|
|
54
|
-
alert: mlrun.common.schemas.AlertConfig = None,
|
|
55
|
-
event_data: mlrun.common.schemas.Event = None,
|
|
52
|
+
runs: typing.Optional[typing.Union[mlrun.lists.RunList, list]] = None,
|
|
53
|
+
custom_html: typing.Optional[typing.Optional[str]] = None,
|
|
54
|
+
alert: typing.Optional[mlrun.common.schemas.AlertConfig] = None,
|
|
55
|
+
event_data: typing.Optional[mlrun.common.schemas.Event] = None,
|
|
56
56
|
):
|
|
57
57
|
webhook = self.params.get("webhook", None) or mlrun.get_secret_or_env(
|
|
58
58
|
"SLACK_WEBHOOK"
|
|
@@ -37,13 +37,13 @@ class WebhookNotification(NotificationBase):
|
|
|
37
37
|
async def push(
|
|
38
38
|
self,
|
|
39
39
|
message: str,
|
|
40
|
-
severity: typing.
|
|
41
|
-
mlrun.common.schemas.NotificationSeverity, str
|
|
40
|
+
severity: typing.Optional[
|
|
41
|
+
typing.Union[mlrun.common.schemas.NotificationSeverity, str]
|
|
42
42
|
] = mlrun.common.schemas.NotificationSeverity.INFO,
|
|
43
|
-
runs: typing.Union[mlrun.lists.RunList, list] = None,
|
|
44
|
-
custom_html: typing.Optional[str] = None,
|
|
45
|
-
alert: mlrun.common.schemas.AlertConfig = None,
|
|
46
|
-
event_data: mlrun.common.schemas.Event = None,
|
|
43
|
+
runs: typing.Optional[typing.Union[mlrun.lists.RunList, list]] = None,
|
|
44
|
+
custom_html: typing.Optional[typing.Optional[str]] = None,
|
|
45
|
+
alert: typing.Optional[mlrun.common.schemas.AlertConfig] = None,
|
|
46
|
+
event_data: typing.Optional[mlrun.common.schemas.Event] = None,
|
|
47
47
|
):
|
|
48
48
|
url = self.params.get("url", None)
|
|
49
49
|
method = self.params.get("method", "post").lower()
|