mlrun 1.7.0rc5__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.

Files changed (234) hide show
  1. mlrun/__init__.py +11 -1
  2. mlrun/__main__.py +39 -121
  3. mlrun/{datastore/helpers.py → alerts/__init__.py} +2 -5
  4. mlrun/alerts/alert.py +248 -0
  5. mlrun/api/schemas/__init__.py +4 -3
  6. mlrun/artifacts/__init__.py +8 -3
  7. mlrun/artifacts/base.py +39 -254
  8. mlrun/artifacts/dataset.py +9 -190
  9. mlrun/artifacts/manager.py +73 -46
  10. mlrun/artifacts/model.py +30 -158
  11. mlrun/artifacts/plots.py +23 -380
  12. mlrun/common/constants.py +73 -2
  13. mlrun/common/db/sql_session.py +3 -2
  14. mlrun/common/formatters/__init__.py +21 -0
  15. mlrun/common/formatters/artifact.py +46 -0
  16. mlrun/common/formatters/base.py +113 -0
  17. mlrun/common/formatters/feature_set.py +44 -0
  18. mlrun/common/formatters/function.py +46 -0
  19. mlrun/common/formatters/pipeline.py +53 -0
  20. mlrun/common/formatters/project.py +51 -0
  21. mlrun/common/formatters/run.py +29 -0
  22. mlrun/common/helpers.py +11 -1
  23. mlrun/{runtimes → common/runtimes}/constants.py +32 -4
  24. mlrun/common/schemas/__init__.py +21 -4
  25. mlrun/common/schemas/alert.py +202 -0
  26. mlrun/common/schemas/api_gateway.py +113 -2
  27. mlrun/common/schemas/artifact.py +28 -1
  28. mlrun/common/schemas/auth.py +11 -0
  29. mlrun/common/schemas/client_spec.py +2 -1
  30. mlrun/common/schemas/common.py +7 -4
  31. mlrun/common/schemas/constants.py +3 -0
  32. mlrun/common/schemas/feature_store.py +58 -28
  33. mlrun/common/schemas/frontend_spec.py +8 -0
  34. mlrun/common/schemas/function.py +11 -0
  35. mlrun/common/schemas/hub.py +7 -9
  36. mlrun/common/schemas/model_monitoring/__init__.py +21 -4
  37. mlrun/common/schemas/model_monitoring/constants.py +136 -42
  38. mlrun/common/schemas/model_monitoring/grafana.py +9 -5
  39. mlrun/common/schemas/model_monitoring/model_endpoints.py +89 -41
  40. mlrun/common/schemas/notification.py +69 -12
  41. mlrun/{runtimes/mpijob/v1alpha1.py → common/schemas/pagination.py} +10 -13
  42. mlrun/common/schemas/pipeline.py +7 -0
  43. mlrun/common/schemas/project.py +67 -16
  44. mlrun/common/schemas/runs.py +17 -0
  45. mlrun/common/schemas/schedule.py +1 -1
  46. mlrun/common/schemas/workflow.py +10 -2
  47. mlrun/common/types.py +14 -1
  48. mlrun/config.py +224 -58
  49. mlrun/data_types/data_types.py +11 -1
  50. mlrun/data_types/spark.py +5 -4
  51. mlrun/data_types/to_pandas.py +75 -34
  52. mlrun/datastore/__init__.py +8 -10
  53. mlrun/datastore/alibaba_oss.py +131 -0
  54. mlrun/datastore/azure_blob.py +131 -43
  55. mlrun/datastore/base.py +107 -47
  56. mlrun/datastore/datastore.py +17 -7
  57. mlrun/datastore/datastore_profile.py +91 -7
  58. mlrun/datastore/dbfs_store.py +3 -7
  59. mlrun/datastore/filestore.py +1 -3
  60. mlrun/datastore/google_cloud_storage.py +92 -32
  61. mlrun/datastore/hdfs.py +5 -0
  62. mlrun/datastore/inmem.py +6 -3
  63. mlrun/datastore/redis.py +3 -2
  64. mlrun/datastore/s3.py +30 -12
  65. mlrun/datastore/snowflake_utils.py +45 -0
  66. mlrun/datastore/sources.py +274 -59
  67. mlrun/datastore/spark_utils.py +30 -0
  68. mlrun/datastore/store_resources.py +9 -7
  69. mlrun/datastore/storeytargets.py +151 -0
  70. mlrun/datastore/targets.py +374 -102
  71. mlrun/datastore/utils.py +68 -5
  72. mlrun/datastore/v3io.py +28 -50
  73. mlrun/db/auth_utils.py +152 -0
  74. mlrun/db/base.py +231 -22
  75. mlrun/db/factory.py +1 -4
  76. mlrun/db/httpdb.py +864 -228
  77. mlrun/db/nopdb.py +268 -16
  78. mlrun/errors.py +35 -5
  79. mlrun/execution.py +111 -38
  80. mlrun/feature_store/__init__.py +0 -2
  81. mlrun/feature_store/api.py +46 -53
  82. mlrun/feature_store/common.py +6 -11
  83. mlrun/feature_store/feature_set.py +48 -23
  84. mlrun/feature_store/feature_vector.py +13 -2
  85. mlrun/feature_store/ingestion.py +7 -6
  86. mlrun/feature_store/retrieval/base.py +9 -4
  87. mlrun/feature_store/retrieval/dask_merger.py +2 -0
  88. mlrun/feature_store/retrieval/job.py +13 -4
  89. mlrun/feature_store/retrieval/local_merger.py +2 -0
  90. mlrun/feature_store/retrieval/spark_merger.py +24 -32
  91. mlrun/feature_store/steps.py +38 -19
  92. mlrun/features.py +6 -14
  93. mlrun/frameworks/_common/plan.py +3 -3
  94. mlrun/frameworks/_dl_common/loggers/tensorboard_logger.py +7 -12
  95. mlrun/frameworks/_ml_common/plan.py +1 -1
  96. mlrun/frameworks/auto_mlrun/auto_mlrun.py +2 -2
  97. mlrun/frameworks/lgbm/__init__.py +1 -1
  98. mlrun/frameworks/lgbm/callbacks/callback.py +2 -4
  99. mlrun/frameworks/lgbm/model_handler.py +1 -1
  100. mlrun/frameworks/parallel_coordinates.py +4 -4
  101. mlrun/frameworks/pytorch/__init__.py +2 -2
  102. mlrun/frameworks/sklearn/__init__.py +1 -1
  103. mlrun/frameworks/sklearn/mlrun_interface.py +13 -3
  104. mlrun/frameworks/tf_keras/__init__.py +5 -2
  105. mlrun/frameworks/tf_keras/callbacks/logging_callback.py +1 -1
  106. mlrun/frameworks/tf_keras/mlrun_interface.py +2 -2
  107. mlrun/frameworks/xgboost/__init__.py +1 -1
  108. mlrun/k8s_utils.py +57 -12
  109. mlrun/launcher/__init__.py +1 -1
  110. mlrun/launcher/base.py +6 -5
  111. mlrun/launcher/client.py +13 -11
  112. mlrun/launcher/factory.py +1 -1
  113. mlrun/launcher/local.py +15 -5
  114. mlrun/launcher/remote.py +10 -3
  115. mlrun/lists.py +6 -2
  116. mlrun/model.py +297 -48
  117. mlrun/model_monitoring/__init__.py +1 -1
  118. mlrun/model_monitoring/api.py +152 -357
  119. mlrun/model_monitoring/applications/__init__.py +10 -0
  120. mlrun/model_monitoring/applications/_application_steps.py +190 -0
  121. mlrun/model_monitoring/applications/base.py +108 -0
  122. mlrun/model_monitoring/applications/context.py +341 -0
  123. mlrun/model_monitoring/{evidently_application.py → applications/evidently_base.py} +27 -22
  124. mlrun/model_monitoring/applications/histogram_data_drift.py +227 -91
  125. mlrun/model_monitoring/applications/results.py +99 -0
  126. mlrun/model_monitoring/controller.py +130 -303
  127. mlrun/model_monitoring/{stores/models/sqlite.py → db/__init__.py} +5 -10
  128. mlrun/model_monitoring/db/stores/__init__.py +136 -0
  129. mlrun/model_monitoring/db/stores/base/__init__.py +15 -0
  130. mlrun/model_monitoring/db/stores/base/store.py +213 -0
  131. mlrun/model_monitoring/db/stores/sqldb/__init__.py +13 -0
  132. mlrun/model_monitoring/db/stores/sqldb/models/__init__.py +71 -0
  133. mlrun/model_monitoring/db/stores/sqldb/models/base.py +190 -0
  134. mlrun/model_monitoring/db/stores/sqldb/models/mysql.py +103 -0
  135. mlrun/model_monitoring/{stores/models/mysql.py → db/stores/sqldb/models/sqlite.py} +19 -13
  136. mlrun/model_monitoring/db/stores/sqldb/sql_store.py +659 -0
  137. mlrun/model_monitoring/db/stores/v3io_kv/__init__.py +13 -0
  138. mlrun/model_monitoring/db/stores/v3io_kv/kv_store.py +726 -0
  139. mlrun/model_monitoring/db/tsdb/__init__.py +105 -0
  140. mlrun/model_monitoring/db/tsdb/base.py +448 -0
  141. mlrun/model_monitoring/db/tsdb/helpers.py +30 -0
  142. mlrun/model_monitoring/db/tsdb/tdengine/__init__.py +15 -0
  143. mlrun/model_monitoring/db/tsdb/tdengine/schemas.py +298 -0
  144. mlrun/model_monitoring/db/tsdb/tdengine/stream_graph_steps.py +42 -0
  145. mlrun/model_monitoring/db/tsdb/tdengine/tdengine_connector.py +522 -0
  146. mlrun/model_monitoring/db/tsdb/v3io/__init__.py +15 -0
  147. mlrun/model_monitoring/db/tsdb/v3io/stream_graph_steps.py +158 -0
  148. mlrun/model_monitoring/db/tsdb/v3io/v3io_connector.py +849 -0
  149. mlrun/model_monitoring/features_drift_table.py +34 -22
  150. mlrun/model_monitoring/helpers.py +177 -39
  151. mlrun/model_monitoring/model_endpoint.py +3 -2
  152. mlrun/model_monitoring/stream_processing.py +165 -398
  153. mlrun/model_monitoring/tracking_policy.py +7 -1
  154. mlrun/model_monitoring/writer.py +161 -125
  155. mlrun/package/packagers/default_packager.py +2 -2
  156. mlrun/package/packagers_manager.py +1 -0
  157. mlrun/package/utils/_formatter.py +2 -2
  158. mlrun/platforms/__init__.py +11 -10
  159. mlrun/platforms/iguazio.py +67 -228
  160. mlrun/projects/__init__.py +6 -1
  161. mlrun/projects/operations.py +47 -20
  162. mlrun/projects/pipelines.py +396 -249
  163. mlrun/projects/project.py +1125 -414
  164. mlrun/render.py +28 -22
  165. mlrun/run.py +207 -180
  166. mlrun/runtimes/__init__.py +76 -11
  167. mlrun/runtimes/base.py +40 -14
  168. mlrun/runtimes/daskjob.py +9 -2
  169. mlrun/runtimes/databricks_job/databricks_runtime.py +1 -0
  170. mlrun/runtimes/databricks_job/databricks_wrapper.py +1 -1
  171. mlrun/runtimes/funcdoc.py +1 -29
  172. mlrun/runtimes/kubejob.py +34 -128
  173. mlrun/runtimes/local.py +39 -10
  174. mlrun/runtimes/mpijob/__init__.py +0 -20
  175. mlrun/runtimes/mpijob/abstract.py +8 -8
  176. mlrun/runtimes/mpijob/v1.py +1 -1
  177. mlrun/runtimes/nuclio/api_gateway.py +646 -177
  178. mlrun/runtimes/nuclio/application/__init__.py +15 -0
  179. mlrun/runtimes/nuclio/application/application.py +758 -0
  180. mlrun/runtimes/nuclio/application/reverse_proxy.go +95 -0
  181. mlrun/runtimes/nuclio/function.py +188 -68
  182. mlrun/runtimes/nuclio/serving.py +57 -60
  183. mlrun/runtimes/pod.py +191 -58
  184. mlrun/runtimes/remotesparkjob.py +11 -8
  185. mlrun/runtimes/sparkjob/spark3job.py +17 -18
  186. mlrun/runtimes/utils.py +40 -73
  187. mlrun/secrets.py +6 -2
  188. mlrun/serving/__init__.py +8 -1
  189. mlrun/serving/remote.py +2 -3
  190. mlrun/serving/routers.py +89 -64
  191. mlrun/serving/server.py +54 -26
  192. mlrun/serving/states.py +187 -56
  193. mlrun/serving/utils.py +19 -11
  194. mlrun/serving/v2_serving.py +136 -63
  195. mlrun/track/tracker.py +2 -1
  196. mlrun/track/trackers/mlflow_tracker.py +5 -0
  197. mlrun/utils/async_http.py +26 -6
  198. mlrun/utils/db.py +18 -0
  199. mlrun/utils/helpers.py +375 -105
  200. mlrun/utils/http.py +2 -2
  201. mlrun/utils/logger.py +75 -9
  202. mlrun/utils/notifications/notification/__init__.py +14 -10
  203. mlrun/utils/notifications/notification/base.py +48 -0
  204. mlrun/utils/notifications/notification/console.py +2 -0
  205. mlrun/utils/notifications/notification/git.py +24 -1
  206. mlrun/utils/notifications/notification/ipython.py +2 -0
  207. mlrun/utils/notifications/notification/slack.py +96 -21
  208. mlrun/utils/notifications/notification/webhook.py +63 -2
  209. mlrun/utils/notifications/notification_pusher.py +146 -16
  210. mlrun/utils/regex.py +9 -0
  211. mlrun/utils/retryer.py +3 -2
  212. mlrun/utils/v3io_clients.py +2 -3
  213. mlrun/utils/version/version.json +2 -2
  214. mlrun-1.7.2.dist-info/METADATA +390 -0
  215. mlrun-1.7.2.dist-info/RECORD +351 -0
  216. {mlrun-1.7.0rc5.dist-info → mlrun-1.7.2.dist-info}/WHEEL +1 -1
  217. mlrun/feature_store/retrieval/conversion.py +0 -271
  218. mlrun/kfpops.py +0 -868
  219. mlrun/model_monitoring/application.py +0 -310
  220. mlrun/model_monitoring/batch.py +0 -974
  221. mlrun/model_monitoring/controller_handler.py +0 -37
  222. mlrun/model_monitoring/prometheus.py +0 -216
  223. mlrun/model_monitoring/stores/__init__.py +0 -111
  224. mlrun/model_monitoring/stores/kv_model_endpoint_store.py +0 -574
  225. mlrun/model_monitoring/stores/model_endpoint_store.py +0 -145
  226. mlrun/model_monitoring/stores/models/__init__.py +0 -27
  227. mlrun/model_monitoring/stores/models/base.py +0 -84
  228. mlrun/model_monitoring/stores/sql_model_endpoint_store.py +0 -382
  229. mlrun/platforms/other.py +0 -305
  230. mlrun-1.7.0rc5.dist-info/METADATA +0 -269
  231. mlrun-1.7.0rc5.dist-info/RECORD +0 -323
  232. {mlrun-1.7.0rc5.dist-info → mlrun-1.7.2.dist-info}/LICENSE +0 -0
  233. {mlrun-1.7.0rc5.dist-info → mlrun-1.7.2.dist-info}/entry_points.txt +0 -0
  234. {mlrun-1.7.0rc5.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
- method_whitelist=self._retry_methods,
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=str(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 = self._resolve_more(record)
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 create_formatter_instance(formatter_kind: FormatterKinds) -> logging.Formatter:
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 = create_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 list(
57
- [
58
- cls.console,
59
- cls.git,
60
- cls.ipython,
61
- cls.slack,
62
- cls.webhook,
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 not runs:
84
- return data
91
+ if alert:
92
+ fields = self._get_alert_fields(alert, event_data)
85
93
 
86
- if isinstance(runs, list):
87
- runs = mlrun.lists.RunList(runs)
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
- fields = [self._get_slack_row("*Runs*"), self._get_slack_row("*Results*")]
90
- for run in runs:
91
- fields.append(self._get_run_line(run))
92
- fields.append(self._get_run_result(run))
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
- for i in range(0, len(fields), 8):
95
- data["blocks"].append({"type": "section", "fields": fields[i : i + 8]})
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
- if url:
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
- state = run["status"].get("state", "")
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 "None")
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