mlrun 1.7.2rc3__py3-none-any.whl → 1.8.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of mlrun might be problematic. Click here for more details.

Files changed (275) hide show
  1. mlrun/__init__.py +26 -22
  2. mlrun/__main__.py +15 -16
  3. mlrun/alerts/alert.py +150 -15
  4. mlrun/api/schemas/__init__.py +1 -9
  5. mlrun/artifacts/__init__.py +2 -3
  6. mlrun/artifacts/base.py +62 -19
  7. mlrun/artifacts/dataset.py +17 -17
  8. mlrun/artifacts/document.py +454 -0
  9. mlrun/artifacts/manager.py +28 -18
  10. mlrun/artifacts/model.py +91 -59
  11. mlrun/artifacts/plots.py +2 -2
  12. mlrun/common/constants.py +8 -0
  13. mlrun/common/formatters/__init__.py +1 -0
  14. mlrun/common/formatters/artifact.py +1 -1
  15. mlrun/common/formatters/feature_set.py +2 -0
  16. mlrun/common/formatters/function.py +1 -0
  17. mlrun/{model_monitoring/db/stores/v3io_kv/__init__.py → common/formatters/model_endpoint.py} +17 -0
  18. mlrun/common/formatters/pipeline.py +1 -2
  19. mlrun/common/formatters/project.py +9 -0
  20. mlrun/common/model_monitoring/__init__.py +0 -5
  21. mlrun/common/model_monitoring/helpers.py +12 -62
  22. mlrun/common/runtimes/constants.py +25 -4
  23. mlrun/common/schemas/__init__.py +9 -5
  24. mlrun/common/schemas/alert.py +114 -19
  25. mlrun/common/schemas/api_gateway.py +3 -3
  26. mlrun/common/schemas/artifact.py +22 -9
  27. mlrun/common/schemas/auth.py +8 -4
  28. mlrun/common/schemas/background_task.py +7 -7
  29. mlrun/common/schemas/client_spec.py +4 -4
  30. mlrun/common/schemas/clusterization_spec.py +2 -2
  31. mlrun/common/schemas/common.py +53 -3
  32. mlrun/common/schemas/constants.py +15 -0
  33. mlrun/common/schemas/datastore_profile.py +1 -1
  34. mlrun/common/schemas/feature_store.py +9 -9
  35. mlrun/common/schemas/frontend_spec.py +4 -4
  36. mlrun/common/schemas/function.py +10 -10
  37. mlrun/common/schemas/hub.py +1 -1
  38. mlrun/common/schemas/k8s.py +3 -3
  39. mlrun/common/schemas/memory_reports.py +3 -3
  40. mlrun/common/schemas/model_monitoring/__init__.py +4 -8
  41. mlrun/common/schemas/model_monitoring/constants.py +127 -46
  42. mlrun/common/schemas/model_monitoring/grafana.py +18 -12
  43. mlrun/common/schemas/model_monitoring/model_endpoints.py +154 -160
  44. mlrun/common/schemas/notification.py +24 -3
  45. mlrun/common/schemas/object.py +1 -1
  46. mlrun/common/schemas/pagination.py +4 -4
  47. mlrun/common/schemas/partition.py +142 -0
  48. mlrun/common/schemas/pipeline.py +3 -3
  49. mlrun/common/schemas/project.py +26 -18
  50. mlrun/common/schemas/runs.py +3 -3
  51. mlrun/common/schemas/runtime_resource.py +5 -5
  52. mlrun/common/schemas/schedule.py +1 -1
  53. mlrun/common/schemas/secret.py +1 -1
  54. mlrun/{model_monitoring/db/stores/sqldb/__init__.py → common/schemas/serving.py} +10 -1
  55. mlrun/common/schemas/tag.py +3 -3
  56. mlrun/common/schemas/workflow.py +6 -5
  57. mlrun/common/types.py +1 -0
  58. mlrun/config.py +157 -89
  59. mlrun/data_types/__init__.py +5 -3
  60. mlrun/data_types/infer.py +13 -3
  61. mlrun/data_types/spark.py +2 -1
  62. mlrun/datastore/__init__.py +59 -18
  63. mlrun/datastore/alibaba_oss.py +4 -1
  64. mlrun/datastore/azure_blob.py +4 -1
  65. mlrun/datastore/base.py +19 -24
  66. mlrun/datastore/datastore.py +10 -4
  67. mlrun/datastore/datastore_profile.py +178 -45
  68. mlrun/datastore/dbfs_store.py +4 -1
  69. mlrun/datastore/filestore.py +4 -1
  70. mlrun/datastore/google_cloud_storage.py +4 -1
  71. mlrun/datastore/hdfs.py +4 -1
  72. mlrun/datastore/inmem.py +4 -1
  73. mlrun/datastore/redis.py +4 -1
  74. mlrun/datastore/s3.py +14 -3
  75. mlrun/datastore/sources.py +89 -92
  76. mlrun/datastore/store_resources.py +7 -4
  77. mlrun/datastore/storeytargets.py +51 -16
  78. mlrun/datastore/targets.py +38 -31
  79. mlrun/datastore/utils.py +87 -4
  80. mlrun/datastore/v3io.py +4 -1
  81. mlrun/datastore/vectorstore.py +291 -0
  82. mlrun/datastore/wasbfs/fs.py +13 -12
  83. mlrun/db/base.py +286 -100
  84. mlrun/db/httpdb.py +1562 -490
  85. mlrun/db/nopdb.py +250 -83
  86. mlrun/errors.py +6 -2
  87. mlrun/execution.py +194 -50
  88. mlrun/feature_store/__init__.py +2 -10
  89. mlrun/feature_store/api.py +20 -458
  90. mlrun/feature_store/common.py +9 -9
  91. mlrun/feature_store/feature_set.py +20 -18
  92. mlrun/feature_store/feature_vector.py +105 -479
  93. mlrun/feature_store/feature_vector_utils.py +466 -0
  94. mlrun/feature_store/retrieval/base.py +15 -11
  95. mlrun/feature_store/retrieval/job.py +2 -1
  96. mlrun/feature_store/retrieval/storey_merger.py +1 -1
  97. mlrun/feature_store/steps.py +3 -3
  98. mlrun/features.py +30 -13
  99. mlrun/frameworks/__init__.py +1 -2
  100. mlrun/frameworks/_common/__init__.py +1 -2
  101. mlrun/frameworks/_common/artifacts_library.py +2 -2
  102. mlrun/frameworks/_common/mlrun_interface.py +10 -6
  103. mlrun/frameworks/_common/model_handler.py +31 -31
  104. mlrun/frameworks/_common/producer.py +3 -1
  105. mlrun/frameworks/_dl_common/__init__.py +1 -2
  106. mlrun/frameworks/_dl_common/loggers/__init__.py +1 -2
  107. mlrun/frameworks/_dl_common/loggers/mlrun_logger.py +4 -4
  108. mlrun/frameworks/_dl_common/loggers/tensorboard_logger.py +3 -3
  109. mlrun/frameworks/_ml_common/__init__.py +1 -2
  110. mlrun/frameworks/_ml_common/loggers/__init__.py +1 -2
  111. mlrun/frameworks/_ml_common/model_handler.py +21 -21
  112. mlrun/frameworks/_ml_common/plans/__init__.py +1 -2
  113. mlrun/frameworks/_ml_common/plans/confusion_matrix_plan.py +3 -1
  114. mlrun/frameworks/_ml_common/plans/dataset_plan.py +3 -3
  115. mlrun/frameworks/_ml_common/plans/roc_curve_plan.py +4 -4
  116. mlrun/frameworks/auto_mlrun/__init__.py +1 -2
  117. mlrun/frameworks/auto_mlrun/auto_mlrun.py +22 -15
  118. mlrun/frameworks/huggingface/__init__.py +1 -2
  119. mlrun/frameworks/huggingface/model_server.py +9 -9
  120. mlrun/frameworks/lgbm/__init__.py +47 -44
  121. mlrun/frameworks/lgbm/callbacks/__init__.py +1 -2
  122. mlrun/frameworks/lgbm/callbacks/logging_callback.py +4 -2
  123. mlrun/frameworks/lgbm/callbacks/mlrun_logging_callback.py +4 -2
  124. mlrun/frameworks/lgbm/mlrun_interfaces/__init__.py +1 -2
  125. mlrun/frameworks/lgbm/mlrun_interfaces/mlrun_interface.py +5 -5
  126. mlrun/frameworks/lgbm/model_handler.py +15 -11
  127. mlrun/frameworks/lgbm/model_server.py +11 -7
  128. mlrun/frameworks/lgbm/utils.py +2 -2
  129. mlrun/frameworks/onnx/__init__.py +1 -2
  130. mlrun/frameworks/onnx/dataset.py +3 -3
  131. mlrun/frameworks/onnx/mlrun_interface.py +2 -2
  132. mlrun/frameworks/onnx/model_handler.py +7 -5
  133. mlrun/frameworks/onnx/model_server.py +8 -6
  134. mlrun/frameworks/parallel_coordinates.py +11 -11
  135. mlrun/frameworks/pytorch/__init__.py +22 -23
  136. mlrun/frameworks/pytorch/callbacks/__init__.py +1 -2
  137. mlrun/frameworks/pytorch/callbacks/callback.py +2 -1
  138. mlrun/frameworks/pytorch/callbacks/logging_callback.py +15 -8
  139. mlrun/frameworks/pytorch/callbacks/mlrun_logging_callback.py +19 -12
  140. mlrun/frameworks/pytorch/callbacks/tensorboard_logging_callback.py +22 -15
  141. mlrun/frameworks/pytorch/callbacks_handler.py +36 -30
  142. mlrun/frameworks/pytorch/mlrun_interface.py +17 -17
  143. mlrun/frameworks/pytorch/model_handler.py +21 -17
  144. mlrun/frameworks/pytorch/model_server.py +13 -9
  145. mlrun/frameworks/sklearn/__init__.py +19 -18
  146. mlrun/frameworks/sklearn/estimator.py +2 -2
  147. mlrun/frameworks/sklearn/metric.py +3 -3
  148. mlrun/frameworks/sklearn/metrics_library.py +8 -6
  149. mlrun/frameworks/sklearn/mlrun_interface.py +3 -2
  150. mlrun/frameworks/sklearn/model_handler.py +4 -3
  151. mlrun/frameworks/tf_keras/__init__.py +11 -12
  152. mlrun/frameworks/tf_keras/callbacks/__init__.py +1 -2
  153. mlrun/frameworks/tf_keras/callbacks/logging_callback.py +17 -14
  154. mlrun/frameworks/tf_keras/callbacks/mlrun_logging_callback.py +15 -12
  155. mlrun/frameworks/tf_keras/callbacks/tensorboard_logging_callback.py +21 -18
  156. mlrun/frameworks/tf_keras/model_handler.py +17 -13
  157. mlrun/frameworks/tf_keras/model_server.py +12 -8
  158. mlrun/frameworks/xgboost/__init__.py +19 -18
  159. mlrun/frameworks/xgboost/model_handler.py +13 -9
  160. mlrun/k8s_utils.py +2 -5
  161. mlrun/launcher/base.py +3 -4
  162. mlrun/launcher/client.py +2 -2
  163. mlrun/launcher/local.py +6 -2
  164. mlrun/launcher/remote.py +1 -1
  165. mlrun/lists.py +8 -4
  166. mlrun/model.py +132 -46
  167. mlrun/model_monitoring/__init__.py +3 -5
  168. mlrun/model_monitoring/api.py +113 -98
  169. mlrun/model_monitoring/applications/__init__.py +0 -5
  170. mlrun/model_monitoring/applications/_application_steps.py +81 -50
  171. mlrun/model_monitoring/applications/base.py +467 -14
  172. mlrun/model_monitoring/applications/context.py +212 -134
  173. mlrun/model_monitoring/{db/stores/base → applications/evidently}/__init__.py +6 -2
  174. mlrun/model_monitoring/applications/evidently/base.py +146 -0
  175. mlrun/model_monitoring/applications/histogram_data_drift.py +89 -56
  176. mlrun/model_monitoring/applications/results.py +67 -15
  177. mlrun/model_monitoring/controller.py +701 -315
  178. mlrun/model_monitoring/db/__init__.py +0 -2
  179. mlrun/model_monitoring/db/_schedules.py +242 -0
  180. mlrun/model_monitoring/db/_stats.py +189 -0
  181. mlrun/model_monitoring/db/tsdb/__init__.py +33 -22
  182. mlrun/model_monitoring/db/tsdb/base.py +243 -49
  183. mlrun/model_monitoring/db/tsdb/tdengine/schemas.py +76 -36
  184. mlrun/model_monitoring/db/tsdb/tdengine/stream_graph_steps.py +33 -0
  185. mlrun/model_monitoring/db/tsdb/tdengine/tdengine_connection.py +213 -0
  186. mlrun/model_monitoring/db/tsdb/tdengine/tdengine_connector.py +534 -88
  187. mlrun/model_monitoring/db/tsdb/v3io/stream_graph_steps.py +1 -0
  188. mlrun/model_monitoring/db/tsdb/v3io/v3io_connector.py +436 -106
  189. mlrun/model_monitoring/helpers.py +356 -114
  190. mlrun/model_monitoring/stream_processing.py +190 -345
  191. mlrun/model_monitoring/tracking_policy.py +11 -4
  192. mlrun/model_monitoring/writer.py +49 -90
  193. mlrun/package/__init__.py +3 -6
  194. mlrun/package/context_handler.py +2 -2
  195. mlrun/package/packager.py +12 -9
  196. mlrun/package/packagers/__init__.py +0 -2
  197. mlrun/package/packagers/default_packager.py +14 -11
  198. mlrun/package/packagers/numpy_packagers.py +16 -7
  199. mlrun/package/packagers/pandas_packagers.py +18 -18
  200. mlrun/package/packagers/python_standard_library_packagers.py +25 -11
  201. mlrun/package/packagers_manager.py +35 -32
  202. mlrun/package/utils/__init__.py +0 -3
  203. mlrun/package/utils/_pickler.py +6 -6
  204. mlrun/platforms/__init__.py +47 -16
  205. mlrun/platforms/iguazio.py +4 -1
  206. mlrun/projects/operations.py +30 -30
  207. mlrun/projects/pipelines.py +116 -47
  208. mlrun/projects/project.py +1292 -329
  209. mlrun/render.py +5 -9
  210. mlrun/run.py +57 -14
  211. mlrun/runtimes/__init__.py +1 -3
  212. mlrun/runtimes/base.py +30 -22
  213. mlrun/runtimes/daskjob.py +9 -9
  214. mlrun/runtimes/databricks_job/databricks_runtime.py +6 -5
  215. mlrun/runtimes/function_reference.py +5 -2
  216. mlrun/runtimes/generators.py +3 -2
  217. mlrun/runtimes/kubejob.py +6 -7
  218. mlrun/runtimes/mounts.py +574 -0
  219. mlrun/runtimes/mpijob/__init__.py +0 -2
  220. mlrun/runtimes/mpijob/abstract.py +7 -6
  221. mlrun/runtimes/nuclio/api_gateway.py +7 -7
  222. mlrun/runtimes/nuclio/application/application.py +11 -13
  223. mlrun/runtimes/nuclio/application/reverse_proxy.go +66 -64
  224. mlrun/runtimes/nuclio/function.py +127 -70
  225. mlrun/runtimes/nuclio/serving.py +105 -37
  226. mlrun/runtimes/pod.py +159 -54
  227. mlrun/runtimes/remotesparkjob.py +3 -2
  228. mlrun/runtimes/sparkjob/__init__.py +0 -2
  229. mlrun/runtimes/sparkjob/spark3job.py +22 -12
  230. mlrun/runtimes/utils.py +7 -6
  231. mlrun/secrets.py +2 -2
  232. mlrun/serving/__init__.py +8 -0
  233. mlrun/serving/merger.py +7 -5
  234. mlrun/serving/remote.py +35 -22
  235. mlrun/serving/routers.py +186 -240
  236. mlrun/serving/server.py +41 -10
  237. mlrun/serving/states.py +432 -118
  238. mlrun/serving/utils.py +13 -2
  239. mlrun/serving/v1_serving.py +3 -2
  240. mlrun/serving/v2_serving.py +161 -203
  241. mlrun/track/__init__.py +1 -1
  242. mlrun/track/tracker.py +2 -2
  243. mlrun/track/trackers/mlflow_tracker.py +6 -5
  244. mlrun/utils/async_http.py +35 -22
  245. mlrun/utils/clones.py +7 -4
  246. mlrun/utils/helpers.py +511 -58
  247. mlrun/utils/logger.py +119 -13
  248. mlrun/utils/notifications/notification/__init__.py +22 -19
  249. mlrun/utils/notifications/notification/base.py +39 -15
  250. mlrun/utils/notifications/notification/console.py +6 -6
  251. mlrun/utils/notifications/notification/git.py +11 -11
  252. mlrun/utils/notifications/notification/ipython.py +10 -9
  253. mlrun/utils/notifications/notification/mail.py +176 -0
  254. mlrun/utils/notifications/notification/slack.py +16 -8
  255. mlrun/utils/notifications/notification/webhook.py +24 -8
  256. mlrun/utils/notifications/notification_pusher.py +191 -200
  257. mlrun/utils/regex.py +12 -2
  258. mlrun/utils/version/version.json +2 -2
  259. {mlrun-1.7.2rc3.dist-info → mlrun-1.8.0.dist-info}/METADATA +81 -54
  260. mlrun-1.8.0.dist-info/RECORD +351 -0
  261. {mlrun-1.7.2rc3.dist-info → mlrun-1.8.0.dist-info}/WHEEL +1 -1
  262. mlrun/model_monitoring/applications/evidently_base.py +0 -137
  263. mlrun/model_monitoring/db/stores/__init__.py +0 -136
  264. mlrun/model_monitoring/db/stores/base/store.py +0 -213
  265. mlrun/model_monitoring/db/stores/sqldb/models/__init__.py +0 -71
  266. mlrun/model_monitoring/db/stores/sqldb/models/base.py +0 -190
  267. mlrun/model_monitoring/db/stores/sqldb/models/mysql.py +0 -103
  268. mlrun/model_monitoring/db/stores/sqldb/models/sqlite.py +0 -40
  269. mlrun/model_monitoring/db/stores/sqldb/sql_store.py +0 -659
  270. mlrun/model_monitoring/db/stores/v3io_kv/kv_store.py +0 -726
  271. mlrun/model_monitoring/model_endpoint.py +0 -118
  272. mlrun-1.7.2rc3.dist-info/RECORD +0 -351
  273. {mlrun-1.7.2rc3.dist-info → mlrun-1.8.0.dist-info}/entry_points.txt +0 -0
  274. {mlrun-1.7.2rc3.dist-info → mlrun-1.8.0.dist-info/licenses}/LICENSE +0 -0
  275. {mlrun-1.7.2rc3.dist-info → mlrun-1.8.0.dist-info}/top_level.txt +0 -0
@@ -16,6 +16,7 @@ import re
16
16
  import typing
17
17
 
18
18
  import aiohttp
19
+ import orjson
19
20
 
20
21
  import mlrun.common.schemas
21
22
  import mlrun.lists
@@ -38,13 +39,13 @@ class WebhookNotification(NotificationBase):
38
39
  async def push(
39
40
  self,
40
41
  message: str,
41
- severity: typing.Union[
42
- mlrun.common.schemas.NotificationSeverity, str
42
+ severity: typing.Optional[
43
+ typing.Union[mlrun.common.schemas.NotificationSeverity, str]
43
44
  ] = mlrun.common.schemas.NotificationSeverity.INFO,
44
- runs: typing.Union[mlrun.lists.RunList, list] = None,
45
- custom_html: str = None,
46
- alert: mlrun.common.schemas.AlertConfig = None,
47
- event_data: mlrun.common.schemas.Event = None,
45
+ runs: typing.Optional[typing.Union[mlrun.lists.RunList, list]] = None,
46
+ custom_html: typing.Optional[typing.Optional[str]] = None,
47
+ alert: typing.Optional[mlrun.common.schemas.AlertConfig] = None,
48
+ event_data: typing.Optional[mlrun.common.schemas.Event] = None,
48
49
  ):
49
50
  url = self.params.get("url", None)
50
51
  method = self.params.get("method", "post").lower()
@@ -86,9 +87,14 @@ class WebhookNotification(NotificationBase):
86
87
  # we automatically handle it as `ssl=None` for their convenience.
87
88
  verify_ssl = verify_ssl and None if url.startswith("https") else None
88
89
 
89
- async with aiohttp.ClientSession() as session:
90
+ async with aiohttp.ClientSession(
91
+ json_serialize=self._encoder,
92
+ ) as session:
90
93
  response = await getattr(session, method)(
91
- url, headers=headers, json=request_body, ssl=verify_ssl
94
+ url,
95
+ headers=headers,
96
+ json=request_body,
97
+ ssl=verify_ssl,
92
98
  )
93
99
  response.raise_for_status()
94
100
 
@@ -128,3 +134,13 @@ class WebhookNotification(NotificationBase):
128
134
  )
129
135
 
130
136
  return override_body
137
+
138
+ @property
139
+ def _encoder(self):
140
+ return lambda body: orjson.dumps(
141
+ body,
142
+ option=orjson.OPT_NAIVE_UTC
143
+ | orjson.OPT_SERIALIZE_NUMPY
144
+ | orjson.OPT_NON_STR_KEYS
145
+ | orjson.OPT_SORT_KEYS,
146
+ ).decode()
@@ -15,17 +15,12 @@
15
15
  import asyncio
16
16
  import datetime
17
17
  import os
18
- import re
19
18
  import traceback
20
19
  import typing
21
20
  from concurrent.futures import ThreadPoolExecutor
22
21
 
23
- import mlrun_pipelines.common.ops
24
- import mlrun_pipelines.models
25
- import mlrun_pipelines.utils
26
-
27
22
  import mlrun.common.constants as mlrun_constants
28
- import mlrun.common.runtimes.constants
23
+ import mlrun.common.runtimes.constants as runtimes_constants
29
24
  import mlrun.common.schemas
30
25
  import mlrun.config
31
26
  import mlrun.db.base
@@ -33,11 +28,11 @@ import mlrun.errors
33
28
  import mlrun.lists
34
29
  import mlrun.model
35
30
  import mlrun.utils.helpers
36
- from mlrun.utils import logger
31
+ import mlrun.utils.notifications.notification as notification_module
32
+ import mlrun.utils.notifications.notification.base as base
33
+ from mlrun.utils import Workflow, logger
37
34
  from mlrun.utils.condition_evaluator import evaluate_condition_in_separate_process
38
35
 
39
- from .notification import NotificationBase, NotificationTypes
40
-
41
36
 
42
37
  class _NotificationPusherBase:
43
38
  def _push(
@@ -60,6 +55,7 @@ class _NotificationPusherBase:
60
55
  event_loop = asyncio.get_event_loop()
61
56
  except RuntimeError:
62
57
  event_loop = asyncio.new_event_loop()
58
+ asyncio.set_event_loop(event_loop)
63
59
 
64
60
  if not event_loop.is_running():
65
61
  event_loop.run_until_complete(async_push_callback())
@@ -98,35 +94,71 @@ class NotificationPusher(_NotificationPusherBase):
98
94
  "completed": "{resource} completed",
99
95
  "error": "{resource} failed",
100
96
  "aborted": "{resource} aborted",
97
+ "running": "{resource} started",
101
98
  }
102
99
 
103
- def __init__(self, runs: typing.Union[mlrun.lists.RunList, list]):
100
+ def __init__(
101
+ self,
102
+ runs: typing.Union[mlrun.lists.RunList, list],
103
+ default_params: typing.Optional[dict] = None,
104
+ ):
104
105
  self._runs = runs
106
+ self._default_params = default_params or {}
105
107
  self._sync_notifications: list[
106
- tuple[NotificationBase, mlrun.model.RunObject, mlrun.model.Notification]
108
+ tuple[
109
+ base.NotificationBase, mlrun.model.RunObject, mlrun.model.Notification
110
+ ]
107
111
  ] = []
108
112
  self._async_notifications: list[
109
- tuple[NotificationBase, mlrun.model.RunObject, mlrun.model.Notification]
113
+ tuple[
114
+ base.NotificationBase, mlrun.model.RunObject, mlrun.model.Notification
115
+ ]
110
116
  ] = []
111
117
 
112
118
  for run in self._runs:
113
- if isinstance(run, dict):
114
- run = mlrun.model.RunObject.from_dict(run)
119
+ try:
120
+ self._process_run(run)
121
+ except Exception as exc:
122
+ logger.warning(
123
+ "Failed to process run",
124
+ run_uid=run.metadata.uid,
125
+ error=mlrun.errors.err_to_str(exc),
126
+ )
115
127
 
116
- for notification in run.spec.notifications:
117
- try:
118
- notification.status = run.status.notifications.get(
119
- notification.name
120
- ).get("status", mlrun.common.schemas.NotificationStatus.PENDING)
121
- except (AttributeError, KeyError):
122
- notification.status = (
123
- mlrun.common.schemas.NotificationStatus.PENDING
124
- )
128
+ def _process_run(self, run):
129
+ if isinstance(run, dict):
130
+ run = mlrun.model.RunObject.from_dict(run)
125
131
 
126
- if self._should_notify(run, notification):
127
- self._load_notification(run, notification)
132
+ for notification in run.spec.notifications:
133
+ try:
134
+ self._process_notification(notification, run)
135
+ except Exception as exc:
136
+ logger.warning(
137
+ "Failed to process notification",
138
+ run_uid=run.metadata.uid,
139
+ notification=notification,
140
+ error=mlrun.errors.err_to_str(exc),
141
+ )
128
142
 
129
- def push(self):
143
+ def _process_notification(self, notification_object, run):
144
+ notification_object.status = run.status.notifications.get(
145
+ notification_object.name, {}
146
+ ).get(
147
+ "status",
148
+ mlrun.common.schemas.NotificationStatus.PENDING,
149
+ )
150
+ if self._should_notify(run, notification_object):
151
+ notification = self._load_notification(notification_object)
152
+ if notification.is_async:
153
+ self._async_notifications.append(
154
+ (notification, run, notification_object)
155
+ )
156
+ else:
157
+ self._sync_notifications.append(
158
+ (notification, run, notification_object)
159
+ )
160
+
161
+ def push(self, sync_push_callback=None, async_push_callback=None):
130
162
  """
131
163
  Asynchronously push notifications for all runs in the initialized runs list (if they should be pushed).
132
164
  When running from a sync environment, the notifications will be pushed asynchronously however the function will
@@ -169,7 +201,7 @@ class NotificationPusher(_NotificationPusherBase):
169
201
  "Failed to push notification async",
170
202
  error=mlrun.errors.err_to_str(result),
171
203
  traceback=traceback.format_exception(
172
- etype=type(result),
204
+ result,
173
205
  value=result,
174
206
  tb=result.__traceback__,
175
207
  ),
@@ -180,8 +212,9 @@ class NotificationPusher(_NotificationPusherBase):
180
212
  notifications_amount=len(self._sync_notifications)
181
213
  + len(self._async_notifications),
182
214
  )
183
-
184
- self._push(sync_push, async_push)
215
+ sync_push_callback = sync_push_callback or sync_push
216
+ async_push_callback = async_push_callback or async_push
217
+ self._push(sync_push_callback, async_push_callback)
185
218
 
186
219
  @staticmethod
187
220
  def _should_notify(
@@ -202,7 +235,7 @@ class NotificationPusher(_NotificationPusherBase):
202
235
  for when_state in when_states:
203
236
  if when_state == run_state:
204
237
  if (
205
- run_state == "completed"
238
+ run_state == runtimes_constants.RunStates.completed
206
239
  and evaluate_condition_in_separate_process(
207
240
  notification.condition,
208
241
  context={
@@ -210,27 +243,29 @@ class NotificationPusher(_NotificationPusherBase):
210
243
  "notification": notification.to_dict(),
211
244
  },
212
245
  )
213
- ) or run_state in ["error", "aborted"]:
246
+ ) or run_state in [
247
+ runtimes_constants.RunStates.error,
248
+ runtimes_constants.RunStates.aborted,
249
+ runtimes_constants.RunStates.running,
250
+ ]:
214
251
  return True
215
252
 
216
253
  return False
217
254
 
218
255
  def _load_notification(
219
- self, run: mlrun.model.RunObject, notification_object: mlrun.model.Notification
220
- ) -> NotificationBase:
256
+ self, notification_object: mlrun.model.Notification
257
+ ) -> base.NotificationBase:
221
258
  name = notification_object.name
222
- notification_type = NotificationTypes(
223
- notification_object.kind or NotificationTypes.console
259
+ notification_type = notification_module.NotificationTypes(
260
+ notification_object.kind or notification_module.NotificationTypes.console
224
261
  )
225
262
  params = {}
226
- params.update(notification_object.secret_params)
227
- params.update(notification_object.params)
228
- notification = notification_type.get_notification()(name, params)
229
- if notification.is_async:
230
- self._async_notifications.append((notification, run, notification_object))
231
- else:
232
- self._sync_notifications.append((notification, run, notification_object))
233
-
263
+ params.update(notification_object.secret_params or {})
264
+ params.update(notification_object.params or {})
265
+ default_params = self._default_params.get(notification_type.value, {})
266
+ notification = notification_type.get_notification()(
267
+ name, params, default_params
268
+ )
234
269
  logger.debug(
235
270
  "Loaded notification", notification=name, type=notification_type.value
236
271
  )
@@ -250,10 +285,14 @@ class NotificationPusher(_NotificationPusherBase):
250
285
  custom_message = (
251
286
  f" (workflow: {run.metadata.labels['workflow']}){custom_message}"
252
287
  )
253
- runs.extend(self.get_workflow_steps(run))
288
+ project = run.metadata.project
289
+ workflow_id = run.status.results.get("workflow_id", None)
290
+ db = mlrun.get_run_db()
291
+ runs.extend(Workflow.get_workflow_steps(db, workflow_id, project))
254
292
 
255
293
  message = (
256
294
  self.messages.get(run.state(), "").format(resource=resource)
295
+ + f" in project {run.metadata.project}"
257
296
  + custom_message
258
297
  )
259
298
 
@@ -265,13 +304,14 @@ class NotificationPusher(_NotificationPusherBase):
265
304
 
266
305
  def _push_notification_sync(
267
306
  self,
268
- notification: NotificationBase,
307
+ notification: base.NotificationBase,
269
308
  run: mlrun.model.RunObject,
270
309
  notification_object: mlrun.model.Notification,
271
310
  ):
272
311
  message, severity, runs = self._prepare_notification_args(
273
312
  run, notification_object
274
313
  )
314
+
275
315
  logger.debug(
276
316
  "Pushing sync notification",
277
317
  notification=sanitize_notification(notification_object.to_dict()),
@@ -282,6 +322,7 @@ class NotificationPusher(_NotificationPusherBase):
282
322
  "project": run.metadata.project,
283
323
  "notification": notification_object,
284
324
  "status": mlrun.common.schemas.NotificationStatus.SENT,
325
+ "run_state": run.state(),
285
326
  }
286
327
  try:
287
328
  notification.push(message, severity, runs)
@@ -313,13 +354,14 @@ class NotificationPusher(_NotificationPusherBase):
313
354
 
314
355
  async def _push_notification_async(
315
356
  self,
316
- notification: NotificationBase,
357
+ notification: base.NotificationBase,
317
358
  run: mlrun.model.RunObject,
318
359
  notification_object: mlrun.model.Notification,
319
360
  ):
320
361
  message, severity, runs = self._prepare_notification_args(
321
362
  run, notification_object
322
363
  )
364
+
323
365
  logger.debug(
324
366
  "Pushing async notification",
325
367
  notification=sanitize_notification(notification_object.to_dict()),
@@ -329,6 +371,7 @@ class NotificationPusher(_NotificationPusherBase):
329
371
  "run_uid": run.metadata.uid,
330
372
  "project": run.metadata.project,
331
373
  "notification": notification_object,
374
+ "run_state": run.state(),
332
375
  "status": mlrun.common.schemas.NotificationStatus.SENT,
333
376
  }
334
377
  try:
@@ -366,10 +409,29 @@ class NotificationPusher(_NotificationPusherBase):
366
409
  run_uid: str,
367
410
  project: str,
368
411
  notification: mlrun.model.Notification,
369
- status: str = None,
412
+ run_state: runtimes_constants.RunStates,
413
+ status: typing.Optional[str] = None,
370
414
  sent_time: typing.Optional[datetime.datetime] = None,
371
415
  reason: typing.Optional[str] = None,
372
416
  ):
417
+ # Skip update the notification state if the following conditions are met:
418
+ # 1. the run is not in a terminal state
419
+ # 2. the when contains only one state (which is the current state)
420
+ # Skip updating because currently each notification has only one row in the db, even if it has multiple when.
421
+ # This means that if the notification is updated to sent for running state for example, it will not send for
422
+ # The terminal state
423
+ # TODO: Change this behavior after implementing ML-8723
424
+ if (
425
+ run_state not in runtimes_constants.RunStates.terminal_states()
426
+ and len(notification.when) > 1
427
+ ):
428
+ logger.debug(
429
+ "Skip updating notification status - run not in terminal state",
430
+ run_uid=run_uid,
431
+ state=run_state,
432
+ )
433
+ return
434
+
373
435
  db = mlrun.get_run_db()
374
436
  notification.status = status or notification.status
375
437
  notification.sent_time = sent_time or notification.sent_time
@@ -394,136 +456,13 @@ class NotificationPusher(_NotificationPusherBase):
394
456
  mask_params=False,
395
457
  )
396
458
 
397
- def get_workflow_steps(self, run: mlrun.model.RunObject) -> list:
398
- steps = []
399
- db = mlrun.get_run_db()
400
-
401
- def _add_run_step(_step: mlrun_pipelines.models.PipelineStep):
402
- try:
403
- _run = db.list_runs(
404
- project=run.metadata.project,
405
- labels=f"{mlrun_constants.MLRunInternalLabels.runner_pod}={_step.node_name}",
406
- )[0]
407
- except IndexError:
408
- _run = {
409
- "metadata": {
410
- "name": _step.display_name,
411
- "project": run.metadata.project,
412
- },
413
- }
414
- _run["step_kind"] = _step.step_type
415
- if _step.skipped:
416
- _run.setdefault("status", {})["state"] = (
417
- mlrun.common.runtimes.constants.RunStates.skipped
418
- )
419
- steps.append(_run)
420
-
421
- def _add_deploy_function_step(_step: mlrun_pipelines.models.PipelineStep):
422
- project, name, hash_key = self._extract_function_uri(
423
- _step.get_annotation("mlrun/function-uri")
424
- )
425
- if name:
426
- try:
427
- function = db.get_function(
428
- project=project, name=name, hash_key=hash_key
429
- )
430
- except mlrun.errors.MLRunNotFoundError:
431
- # If the function is not found (if build failed for example), we will create a dummy
432
- # function object for the notification to display the function name
433
- function = {
434
- "metadata": {
435
- "name": name,
436
- "project": project,
437
- "hash_key": hash_key,
438
- },
439
- }
440
- pod_phase = _step.phase
441
- if _step.skipped:
442
- state = mlrun.common.schemas.FunctionState.skipped
443
- else:
444
- state = mlrun.common.runtimes.constants.PodPhases.pod_phase_to_run_state(
445
- pod_phase
446
- )
447
- function["status"] = {"state": state}
448
- if isinstance(function["metadata"].get("updated"), datetime.datetime):
449
- function["metadata"]["updated"] = function["metadata"][
450
- "updated"
451
- ].isoformat()
452
- function["step_kind"] = _step.step_type
453
- steps.append(function)
454
-
455
- step_methods = {
456
- mlrun_pipelines.common.ops.PipelineRunType.run: _add_run_step,
457
- mlrun_pipelines.common.ops.PipelineRunType.build: _add_deploy_function_step,
458
- mlrun_pipelines.common.ops.PipelineRunType.deploy: _add_deploy_function_step,
459
- }
460
-
461
- workflow_id = run.status.results.get("workflow_id", None)
462
- if not workflow_id:
463
- return steps
464
-
465
- workflow_manifest = self._get_workflow_manifest(workflow_id)
466
- if not workflow_manifest:
467
- return steps
468
-
469
- try:
470
- for step in workflow_manifest.get_steps():
471
- step_method = step_methods.get(step.step_type)
472
- if step_method:
473
- step_method(step)
474
- return steps
475
- except Exception:
476
- # If we fail to read the pipeline steps, we will return the list of runs that have the same workflow id
477
- logger.warning(
478
- "Failed to extract workflow steps from workflow manifest, "
479
- "returning all runs with the workflow id label",
480
- workflow_id=workflow_id,
481
- traceback=traceback.format_exc(),
482
- )
483
- return db.list_runs(
484
- project=run.metadata.project,
485
- labels=f"workflow={workflow_id}",
486
- )
487
-
488
- @staticmethod
489
- def _get_workflow_manifest(
490
- workflow_id: str,
491
- ) -> typing.Optional[mlrun_pipelines.models.PipelineManifest]:
492
- kfp_client = mlrun_pipelines.utils.get_client(mlrun.mlconf.kfp_url)
493
-
494
- # arbitrary timeout of 5 seconds, the workflow should be done by now
495
- kfp_run = kfp_client.wait_for_run_completion(workflow_id, 5)
496
- if not kfp_run:
497
- return None
498
-
499
- kfp_run = mlrun_pipelines.models.PipelineRun(kfp_run)
500
- return kfp_run.workflow_manifest()
501
-
502
- def _extract_function_uri(self, function_uri: str) -> tuple[str, str, str]:
503
- """
504
- Extract the project, name, and hash key from a function uri.
505
- Examples:
506
- - "project/name@hash_key" returns project, name, hash_key
507
- - "project/name returns" project, name, ""
508
- """
509
- project, name, hash_key = None, None, None
510
- hashed_pattern = r"^(.+)/(.+)@(.+)$"
511
- pattern = r"^(.+)/(.+)$"
512
- match = re.match(hashed_pattern, function_uri)
513
- if match:
514
- project, name, hash_key = match.groups()
515
- else:
516
- match = re.match(pattern, function_uri)
517
- if match:
518
- project, name = match.groups()
519
- hash_key = ""
520
- return project, name, hash_key
521
-
522
459
 
523
460
  class CustomNotificationPusher(_NotificationPusherBase):
524
- def __init__(self, notification_types: list[str] = None):
461
+ def __init__(self, notification_types: typing.Optional[list[str]] = None):
525
462
  notifications = {
526
- notification_type: NotificationTypes(notification_type).get_notification()()
463
+ notification_type: notification_module.NotificationTypes(
464
+ notification_type
465
+ ).get_notification()()
527
466
  for notification_type in notification_types
528
467
  }
529
468
  self._sync_notifications = {
@@ -536,6 +475,7 @@ class CustomNotificationPusher(_NotificationPusherBase):
536
475
  for notification_type, notification in notifications.items()
537
476
  if notification.is_async
538
477
  }
478
+ self._server_notifications = []
539
479
 
540
480
  @property
541
481
  def notifications(self):
@@ -543,6 +483,10 @@ class CustomNotificationPusher(_NotificationPusherBase):
543
483
  notifications.update(self._async_notifications)
544
484
  return notifications
545
485
 
486
+ @property
487
+ def server_notifications(self):
488
+ return self._server_notifications
489
+
546
490
  def push(
547
491
  self,
548
492
  message: str,
@@ -550,7 +494,7 @@ class CustomNotificationPusher(_NotificationPusherBase):
550
494
  mlrun.common.schemas.NotificationSeverity, str
551
495
  ] = mlrun.common.schemas.NotificationSeverity.INFO,
552
496
  runs: typing.Union[mlrun.lists.RunList, list] = None,
553
- custom_html: str = None,
497
+ custom_html: typing.Optional[str] = None,
554
498
  ):
555
499
  def sync_push():
556
500
  for notification_type, notification in self._sync_notifications.items():
@@ -572,14 +516,43 @@ class CustomNotificationPusher(_NotificationPusherBase):
572
516
  def add_notification(
573
517
  self,
574
518
  notification_type: str,
575
- params: dict[str, str] = None,
519
+ params: typing.Optional[dict[str, str]] = None,
520
+ name: typing.Optional[str] = None,
521
+ message: typing.Optional[str] = None,
522
+ severity: mlrun.common.schemas.notification.NotificationSeverity = (
523
+ mlrun.common.schemas.notification.NotificationSeverity.INFO
524
+ ),
525
+ when: typing.Optional[list[str]] = None,
526
+ condition: typing.Optional[str] = None,
527
+ secret_params: typing.Optional[dict[str, str]] = None,
576
528
  ):
529
+ if notification_type not in [
530
+ notification_module.NotificationTypes.console,
531
+ notification_module.NotificationTypes.ipython,
532
+ ]:
533
+ # We want that only the console and ipython notifications will be notified by the client.
534
+ # The rest of the notifications will be notified by the BE.
535
+ self._server_notifications.append(
536
+ mlrun.model.Notification(
537
+ kind=notification_type,
538
+ name=name,
539
+ message=message,
540
+ severity=severity,
541
+ when=when or runtimes_constants.RunStates.notification_states(),
542
+ params=params,
543
+ secret_params=secret_params,
544
+ )
545
+ )
546
+ return
547
+
577
548
  if notification_type in self._async_notifications:
578
549
  self._async_notifications[notification_type].load_notification(params)
579
550
  elif notification_type in self._sync_notifications:
580
551
  self._sync_notifications[notification_type].load_notification(params)
581
552
  else:
582
- notification = NotificationTypes(notification_type).get_notification()(
553
+ notification = notification_module.NotificationTypes(
554
+ notification_type
555
+ ).get_notification()(
583
556
  params=params,
584
557
  )
585
558
  if notification.is_async:
@@ -597,7 +570,9 @@ class CustomNotificationPusher(_NotificationPusherBase):
597
570
  else:
598
571
  logger.warning(f"No notification of type {notification_type} in project")
599
572
 
600
- def edit_notification(self, notification_type: str, params: dict[str, str] = None):
573
+ def edit_notification(
574
+ self, notification_type: str, params: typing.Optional[dict[str, str]] = None
575
+ ):
601
576
  self.remove_notification(notification_type)
602
577
  self.add_notification(notification_type, params)
603
578
 
@@ -611,7 +586,7 @@ class CustomNotificationPusher(_NotificationPusherBase):
611
586
 
612
587
  # get notification's inverse dependencies, and only push the notification if
613
588
  # none of its inverse dependencies are being sent
614
- inverse_dependencies = NotificationTypes(
589
+ inverse_dependencies = notification_module.NotificationTypes(
615
590
  notification_type
616
591
  ).inverse_dependencies()
617
592
  for inverse_dependency in inverse_dependencies:
@@ -627,36 +602,28 @@ class CustomNotificationPusher(_NotificationPusherBase):
627
602
  def push_pipeline_start_message(
628
603
  self,
629
604
  project: str,
630
- commit_id: str = None,
631
- pipeline_id: str = None,
605
+ pipeline_id: typing.Optional[str] = None,
606
+ ):
607
+ db = mlrun.get_run_db()
608
+ db.push_run_notifications(pipeline_id, project)
609
+
610
+ def push_pipeline_start_message_from_client(
611
+ self,
612
+ project: str,
613
+ commit_id: typing.Optional[str] = None,
614
+ pipeline_id: typing.Optional[str] = None,
632
615
  has_workflow_url: bool = False,
633
616
  ):
634
- message = f"Workflow started in project {project}"
635
- if pipeline_id:
636
- message += f" id={pipeline_id}"
637
- commit_id = (
638
- commit_id or os.environ.get("GITHUB_SHA") or os.environ.get("CI_COMMIT_SHA")
617
+ html, message = self.generate_start_message(
618
+ commit_id, has_workflow_url, pipeline_id, project
639
619
  )
640
- if commit_id:
641
- message += f", commit={commit_id}"
642
- if has_workflow_url:
643
- url = mlrun.utils.helpers.get_workflow_url(project, pipeline_id)
644
- else:
645
- url = mlrun.utils.helpers.get_ui_url(project)
646
- html = ""
647
- if url:
648
- html = (
649
- message
650
- + f'<div><a href="{url}" target="_blank">click here to view progress</a></div>'
651
- )
652
- message = message + f", check progress in {url}"
653
620
  self.push(message, "info", custom_html=html)
654
621
 
655
622
  def push_pipeline_run_results(
656
623
  self,
657
624
  runs: typing.Union[mlrun.lists.RunList, list],
658
625
  push_all: bool = False,
659
- state: str = None,
626
+ state: typing.Optional[str] = None,
660
627
  ):
661
628
  """
662
629
  push a structured table with run results to notification targets
@@ -682,6 +649,30 @@ class CustomNotificationPusher(_NotificationPusherBase):
682
649
  text += f", state={state}"
683
650
  self.push(text, "info", runs=runs_list)
684
651
 
652
+ def generate_start_message(
653
+ self, commit_id=None, has_workflow_url=None, pipeline_id=None, project=None
654
+ ):
655
+ message = f"Workflow started in project {project}"
656
+ if pipeline_id:
657
+ message += f" id={pipeline_id}"
658
+ commit_id = (
659
+ commit_id or os.environ.get("GITHUB_SHA") or os.environ.get("CI_COMMIT_SHA")
660
+ )
661
+ if commit_id:
662
+ message += f", commit={commit_id}"
663
+ if has_workflow_url:
664
+ url = mlrun.utils.helpers.get_workflow_url(project, pipeline_id)
665
+ else:
666
+ url = mlrun.utils.helpers.get_runs_url(project)
667
+ html = ""
668
+ if url:
669
+ html = (
670
+ message
671
+ + f'<div><a href="{url}" target="_blank">click here to view progress</a></div>'
672
+ )
673
+ message = message + f", check progress in {url}"
674
+ return html, message
675
+
685
676
 
686
677
  def sanitize_notification(notification_dict: dict):
687
678
  notification_dict.pop("secret_params", None)