mlrun 1.6.4rc8__py3-none-any.whl → 1.7.0__py3-none-any.whl

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

Potentially problematic release.


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

Files changed (305) hide show
  1. mlrun/__init__.py +11 -1
  2. mlrun/__main__.py +40 -122
  3. mlrun/alerts/__init__.py +15 -0
  4. mlrun/alerts/alert.py +248 -0
  5. mlrun/api/schemas/__init__.py +5 -4
  6. mlrun/artifacts/__init__.py +8 -3
  7. mlrun/artifacts/base.py +47 -257
  8. mlrun/artifacts/dataset.py +11 -192
  9. mlrun/artifacts/manager.py +79 -47
  10. mlrun/artifacts/model.py +31 -159
  11. mlrun/artifacts/plots.py +23 -380
  12. mlrun/common/constants.py +74 -1
  13. mlrun/common/db/sql_session.py +5 -5
  14. mlrun/common/formatters/__init__.py +21 -0
  15. mlrun/common/formatters/artifact.py +45 -0
  16. mlrun/common/formatters/base.py +113 -0
  17. mlrun/common/formatters/feature_set.py +33 -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 +12 -3
  23. mlrun/common/model_monitoring/helpers.py +9 -5
  24. mlrun/{runtimes → common/runtimes}/constants.py +37 -9
  25. mlrun/common/schemas/__init__.py +31 -5
  26. mlrun/common/schemas/alert.py +202 -0
  27. mlrun/common/schemas/api_gateway.py +196 -0
  28. mlrun/common/schemas/artifact.py +25 -4
  29. mlrun/common/schemas/auth.py +16 -5
  30. mlrun/common/schemas/background_task.py +1 -1
  31. mlrun/common/schemas/client_spec.py +4 -2
  32. mlrun/common/schemas/common.py +7 -4
  33. mlrun/common/schemas/constants.py +3 -0
  34. mlrun/common/schemas/feature_store.py +74 -44
  35. mlrun/common/schemas/frontend_spec.py +15 -7
  36. mlrun/common/schemas/function.py +12 -1
  37. mlrun/common/schemas/hub.py +11 -18
  38. mlrun/common/schemas/memory_reports.py +2 -2
  39. mlrun/common/schemas/model_monitoring/__init__.py +20 -4
  40. mlrun/common/schemas/model_monitoring/constants.py +123 -42
  41. mlrun/common/schemas/model_monitoring/grafana.py +13 -9
  42. mlrun/common/schemas/model_monitoring/model_endpoints.py +101 -54
  43. mlrun/common/schemas/notification.py +71 -14
  44. mlrun/common/schemas/object.py +2 -2
  45. mlrun/{model_monitoring/controller_handler.py → common/schemas/pagination.py} +9 -12
  46. mlrun/common/schemas/pipeline.py +8 -1
  47. mlrun/common/schemas/project.py +69 -18
  48. mlrun/common/schemas/runs.py +7 -1
  49. mlrun/common/schemas/runtime_resource.py +8 -12
  50. mlrun/common/schemas/schedule.py +4 -4
  51. mlrun/common/schemas/tag.py +1 -2
  52. mlrun/common/schemas/workflow.py +12 -4
  53. mlrun/common/types.py +14 -1
  54. mlrun/config.py +154 -69
  55. mlrun/data_types/data_types.py +6 -1
  56. mlrun/data_types/spark.py +2 -2
  57. mlrun/data_types/to_pandas.py +67 -37
  58. mlrun/datastore/__init__.py +6 -8
  59. mlrun/datastore/alibaba_oss.py +131 -0
  60. mlrun/datastore/azure_blob.py +143 -42
  61. mlrun/datastore/base.py +102 -58
  62. mlrun/datastore/datastore.py +34 -13
  63. mlrun/datastore/datastore_profile.py +146 -20
  64. mlrun/datastore/dbfs_store.py +3 -7
  65. mlrun/datastore/filestore.py +1 -4
  66. mlrun/datastore/google_cloud_storage.py +97 -33
  67. mlrun/datastore/hdfs.py +56 -0
  68. mlrun/datastore/inmem.py +6 -3
  69. mlrun/datastore/redis.py +7 -2
  70. mlrun/datastore/s3.py +34 -12
  71. mlrun/datastore/snowflake_utils.py +45 -0
  72. mlrun/datastore/sources.py +303 -111
  73. mlrun/datastore/spark_utils.py +31 -2
  74. mlrun/datastore/store_resources.py +9 -7
  75. mlrun/datastore/storeytargets.py +151 -0
  76. mlrun/datastore/targets.py +453 -176
  77. mlrun/datastore/utils.py +72 -58
  78. mlrun/datastore/v3io.py +6 -1
  79. mlrun/db/base.py +274 -41
  80. mlrun/db/factory.py +1 -1
  81. mlrun/db/httpdb.py +893 -225
  82. mlrun/db/nopdb.py +291 -33
  83. mlrun/errors.py +36 -6
  84. mlrun/execution.py +115 -42
  85. mlrun/feature_store/__init__.py +0 -2
  86. mlrun/feature_store/api.py +65 -73
  87. mlrun/feature_store/common.py +7 -12
  88. mlrun/feature_store/feature_set.py +76 -55
  89. mlrun/feature_store/feature_vector.py +39 -31
  90. mlrun/feature_store/ingestion.py +7 -6
  91. mlrun/feature_store/retrieval/base.py +16 -11
  92. mlrun/feature_store/retrieval/dask_merger.py +2 -0
  93. mlrun/feature_store/retrieval/job.py +13 -4
  94. mlrun/feature_store/retrieval/local_merger.py +2 -0
  95. mlrun/feature_store/retrieval/spark_merger.py +24 -32
  96. mlrun/feature_store/steps.py +45 -34
  97. mlrun/features.py +11 -21
  98. mlrun/frameworks/_common/artifacts_library.py +9 -9
  99. mlrun/frameworks/_common/mlrun_interface.py +5 -5
  100. mlrun/frameworks/_common/model_handler.py +48 -48
  101. mlrun/frameworks/_common/plan.py +5 -6
  102. mlrun/frameworks/_common/producer.py +3 -4
  103. mlrun/frameworks/_common/utils.py +5 -5
  104. mlrun/frameworks/_dl_common/loggers/logger.py +6 -7
  105. mlrun/frameworks/_dl_common/loggers/mlrun_logger.py +9 -9
  106. mlrun/frameworks/_dl_common/loggers/tensorboard_logger.py +23 -47
  107. mlrun/frameworks/_ml_common/artifacts_library.py +1 -2
  108. mlrun/frameworks/_ml_common/loggers/logger.py +3 -4
  109. mlrun/frameworks/_ml_common/loggers/mlrun_logger.py +4 -5
  110. mlrun/frameworks/_ml_common/model_handler.py +24 -24
  111. mlrun/frameworks/_ml_common/pkl_model_server.py +2 -2
  112. mlrun/frameworks/_ml_common/plan.py +2 -2
  113. mlrun/frameworks/_ml_common/plans/calibration_curve_plan.py +2 -3
  114. mlrun/frameworks/_ml_common/plans/confusion_matrix_plan.py +2 -3
  115. mlrun/frameworks/_ml_common/plans/dataset_plan.py +3 -3
  116. mlrun/frameworks/_ml_common/plans/feature_importance_plan.py +3 -3
  117. mlrun/frameworks/_ml_common/plans/roc_curve_plan.py +4 -4
  118. mlrun/frameworks/_ml_common/utils.py +4 -4
  119. mlrun/frameworks/auto_mlrun/auto_mlrun.py +9 -9
  120. mlrun/frameworks/huggingface/model_server.py +4 -4
  121. mlrun/frameworks/lgbm/__init__.py +33 -33
  122. mlrun/frameworks/lgbm/callbacks/callback.py +2 -4
  123. mlrun/frameworks/lgbm/callbacks/logging_callback.py +4 -5
  124. mlrun/frameworks/lgbm/callbacks/mlrun_logging_callback.py +4 -5
  125. mlrun/frameworks/lgbm/mlrun_interfaces/booster_mlrun_interface.py +1 -3
  126. mlrun/frameworks/lgbm/mlrun_interfaces/mlrun_interface.py +6 -6
  127. mlrun/frameworks/lgbm/model_handler.py +10 -10
  128. mlrun/frameworks/lgbm/model_server.py +6 -6
  129. mlrun/frameworks/lgbm/utils.py +5 -5
  130. mlrun/frameworks/onnx/dataset.py +8 -8
  131. mlrun/frameworks/onnx/mlrun_interface.py +3 -3
  132. mlrun/frameworks/onnx/model_handler.py +6 -6
  133. mlrun/frameworks/onnx/model_server.py +7 -7
  134. mlrun/frameworks/parallel_coordinates.py +6 -6
  135. mlrun/frameworks/pytorch/__init__.py +18 -18
  136. mlrun/frameworks/pytorch/callbacks/callback.py +4 -5
  137. mlrun/frameworks/pytorch/callbacks/logging_callback.py +17 -17
  138. mlrun/frameworks/pytorch/callbacks/mlrun_logging_callback.py +11 -11
  139. mlrun/frameworks/pytorch/callbacks/tensorboard_logging_callback.py +23 -29
  140. mlrun/frameworks/pytorch/callbacks_handler.py +38 -38
  141. mlrun/frameworks/pytorch/mlrun_interface.py +20 -20
  142. mlrun/frameworks/pytorch/model_handler.py +17 -17
  143. mlrun/frameworks/pytorch/model_server.py +7 -7
  144. mlrun/frameworks/sklearn/__init__.py +13 -13
  145. mlrun/frameworks/sklearn/estimator.py +4 -4
  146. mlrun/frameworks/sklearn/metrics_library.py +14 -14
  147. mlrun/frameworks/sklearn/mlrun_interface.py +16 -9
  148. mlrun/frameworks/sklearn/model_handler.py +2 -2
  149. mlrun/frameworks/tf_keras/__init__.py +10 -7
  150. mlrun/frameworks/tf_keras/callbacks/logging_callback.py +15 -15
  151. mlrun/frameworks/tf_keras/callbacks/mlrun_logging_callback.py +11 -11
  152. mlrun/frameworks/tf_keras/callbacks/tensorboard_logging_callback.py +19 -23
  153. mlrun/frameworks/tf_keras/mlrun_interface.py +9 -11
  154. mlrun/frameworks/tf_keras/model_handler.py +14 -14
  155. mlrun/frameworks/tf_keras/model_server.py +6 -6
  156. mlrun/frameworks/xgboost/__init__.py +13 -13
  157. mlrun/frameworks/xgboost/model_handler.py +6 -6
  158. mlrun/k8s_utils.py +61 -17
  159. mlrun/launcher/__init__.py +1 -1
  160. mlrun/launcher/base.py +16 -15
  161. mlrun/launcher/client.py +13 -11
  162. mlrun/launcher/factory.py +1 -1
  163. mlrun/launcher/local.py +23 -13
  164. mlrun/launcher/remote.py +17 -10
  165. mlrun/lists.py +7 -6
  166. mlrun/model.py +478 -103
  167. mlrun/model_monitoring/__init__.py +1 -1
  168. mlrun/model_monitoring/api.py +163 -371
  169. mlrun/{runtimes/mpijob/v1alpha1.py → model_monitoring/applications/__init__.py} +9 -15
  170. mlrun/model_monitoring/applications/_application_steps.py +188 -0
  171. mlrun/model_monitoring/applications/base.py +108 -0
  172. mlrun/model_monitoring/applications/context.py +341 -0
  173. mlrun/model_monitoring/{evidently_application.py → applications/evidently_base.py} +27 -22
  174. mlrun/model_monitoring/applications/histogram_data_drift.py +354 -0
  175. mlrun/model_monitoring/applications/results.py +99 -0
  176. mlrun/model_monitoring/controller.py +131 -278
  177. mlrun/model_monitoring/db/__init__.py +18 -0
  178. mlrun/model_monitoring/db/stores/__init__.py +136 -0
  179. mlrun/model_monitoring/db/stores/base/__init__.py +15 -0
  180. mlrun/model_monitoring/db/stores/base/store.py +213 -0
  181. mlrun/model_monitoring/db/stores/sqldb/__init__.py +13 -0
  182. mlrun/model_monitoring/db/stores/sqldb/models/__init__.py +71 -0
  183. mlrun/model_monitoring/db/stores/sqldb/models/base.py +190 -0
  184. mlrun/model_monitoring/db/stores/sqldb/models/mysql.py +103 -0
  185. mlrun/model_monitoring/{stores/models/mysql.py → db/stores/sqldb/models/sqlite.py} +19 -13
  186. mlrun/model_monitoring/db/stores/sqldb/sql_store.py +659 -0
  187. mlrun/model_monitoring/db/stores/v3io_kv/__init__.py +13 -0
  188. mlrun/model_monitoring/db/stores/v3io_kv/kv_store.py +726 -0
  189. mlrun/model_monitoring/db/tsdb/__init__.py +105 -0
  190. mlrun/model_monitoring/db/tsdb/base.py +448 -0
  191. mlrun/model_monitoring/db/tsdb/helpers.py +30 -0
  192. mlrun/model_monitoring/db/tsdb/tdengine/__init__.py +15 -0
  193. mlrun/model_monitoring/db/tsdb/tdengine/schemas.py +279 -0
  194. mlrun/model_monitoring/db/tsdb/tdengine/stream_graph_steps.py +42 -0
  195. mlrun/model_monitoring/db/tsdb/tdengine/tdengine_connector.py +507 -0
  196. mlrun/model_monitoring/db/tsdb/v3io/__init__.py +15 -0
  197. mlrun/model_monitoring/db/tsdb/v3io/stream_graph_steps.py +158 -0
  198. mlrun/model_monitoring/db/tsdb/v3io/v3io_connector.py +849 -0
  199. mlrun/model_monitoring/features_drift_table.py +134 -106
  200. mlrun/model_monitoring/helpers.py +199 -55
  201. mlrun/model_monitoring/metrics/__init__.py +13 -0
  202. mlrun/model_monitoring/metrics/histogram_distance.py +127 -0
  203. mlrun/model_monitoring/model_endpoint.py +3 -2
  204. mlrun/model_monitoring/stream_processing.py +134 -398
  205. mlrun/model_monitoring/tracking_policy.py +9 -2
  206. mlrun/model_monitoring/writer.py +161 -125
  207. mlrun/package/__init__.py +6 -6
  208. mlrun/package/context_handler.py +5 -5
  209. mlrun/package/packager.py +7 -7
  210. mlrun/package/packagers/default_packager.py +8 -8
  211. mlrun/package/packagers/numpy_packagers.py +15 -15
  212. mlrun/package/packagers/pandas_packagers.py +5 -5
  213. mlrun/package/packagers/python_standard_library_packagers.py +10 -10
  214. mlrun/package/packagers_manager.py +19 -23
  215. mlrun/package/utils/_formatter.py +6 -6
  216. mlrun/package/utils/_pickler.py +2 -2
  217. mlrun/package/utils/_supported_format.py +4 -4
  218. mlrun/package/utils/log_hint_utils.py +2 -2
  219. mlrun/package/utils/type_hint_utils.py +4 -9
  220. mlrun/platforms/__init__.py +11 -10
  221. mlrun/platforms/iguazio.py +24 -203
  222. mlrun/projects/operations.py +52 -25
  223. mlrun/projects/pipelines.py +191 -197
  224. mlrun/projects/project.py +1227 -400
  225. mlrun/render.py +16 -19
  226. mlrun/run.py +209 -184
  227. mlrun/runtimes/__init__.py +83 -15
  228. mlrun/runtimes/base.py +51 -35
  229. mlrun/runtimes/daskjob.py +17 -10
  230. mlrun/runtimes/databricks_job/databricks_cancel_task.py +1 -1
  231. mlrun/runtimes/databricks_job/databricks_runtime.py +8 -7
  232. mlrun/runtimes/databricks_job/databricks_wrapper.py +1 -1
  233. mlrun/runtimes/funcdoc.py +1 -29
  234. mlrun/runtimes/function_reference.py +1 -1
  235. mlrun/runtimes/kubejob.py +34 -128
  236. mlrun/runtimes/local.py +40 -11
  237. mlrun/runtimes/mpijob/__init__.py +0 -20
  238. mlrun/runtimes/mpijob/abstract.py +9 -10
  239. mlrun/runtimes/mpijob/v1.py +1 -1
  240. mlrun/{model_monitoring/stores/models/sqlite.py → runtimes/nuclio/__init__.py} +7 -9
  241. mlrun/runtimes/nuclio/api_gateway.py +769 -0
  242. mlrun/runtimes/nuclio/application/__init__.py +15 -0
  243. mlrun/runtimes/nuclio/application/application.py +758 -0
  244. mlrun/runtimes/nuclio/application/reverse_proxy.go +95 -0
  245. mlrun/runtimes/{function.py → nuclio/function.py} +200 -83
  246. mlrun/runtimes/{nuclio.py → nuclio/nuclio.py} +6 -6
  247. mlrun/runtimes/{serving.py → nuclio/serving.py} +65 -68
  248. mlrun/runtimes/pod.py +281 -101
  249. mlrun/runtimes/remotesparkjob.py +12 -9
  250. mlrun/runtimes/sparkjob/spark3job.py +67 -51
  251. mlrun/runtimes/utils.py +41 -75
  252. mlrun/secrets.py +9 -5
  253. mlrun/serving/__init__.py +8 -1
  254. mlrun/serving/remote.py +2 -7
  255. mlrun/serving/routers.py +85 -69
  256. mlrun/serving/server.py +69 -44
  257. mlrun/serving/states.py +209 -36
  258. mlrun/serving/utils.py +22 -14
  259. mlrun/serving/v1_serving.py +6 -7
  260. mlrun/serving/v2_serving.py +133 -54
  261. mlrun/track/tracker.py +2 -1
  262. mlrun/track/tracker_manager.py +3 -3
  263. mlrun/track/trackers/mlflow_tracker.py +6 -2
  264. mlrun/utils/async_http.py +6 -8
  265. mlrun/utils/azure_vault.py +1 -1
  266. mlrun/utils/clones.py +1 -2
  267. mlrun/utils/condition_evaluator.py +3 -3
  268. mlrun/utils/db.py +21 -3
  269. mlrun/utils/helpers.py +405 -225
  270. mlrun/utils/http.py +3 -6
  271. mlrun/utils/logger.py +112 -16
  272. mlrun/utils/notifications/notification/__init__.py +17 -13
  273. mlrun/utils/notifications/notification/base.py +50 -2
  274. mlrun/utils/notifications/notification/console.py +2 -0
  275. mlrun/utils/notifications/notification/git.py +24 -1
  276. mlrun/utils/notifications/notification/ipython.py +3 -1
  277. mlrun/utils/notifications/notification/slack.py +96 -21
  278. mlrun/utils/notifications/notification/webhook.py +59 -2
  279. mlrun/utils/notifications/notification_pusher.py +149 -30
  280. mlrun/utils/regex.py +9 -0
  281. mlrun/utils/retryer.py +208 -0
  282. mlrun/utils/singleton.py +1 -1
  283. mlrun/utils/v3io_clients.py +4 -6
  284. mlrun/utils/version/version.json +2 -2
  285. mlrun/utils/version/version.py +2 -6
  286. mlrun-1.7.0.dist-info/METADATA +378 -0
  287. mlrun-1.7.0.dist-info/RECORD +351 -0
  288. {mlrun-1.6.4rc8.dist-info → mlrun-1.7.0.dist-info}/WHEEL +1 -1
  289. mlrun/feature_store/retrieval/conversion.py +0 -273
  290. mlrun/kfpops.py +0 -868
  291. mlrun/model_monitoring/application.py +0 -310
  292. mlrun/model_monitoring/batch.py +0 -1095
  293. mlrun/model_monitoring/prometheus.py +0 -219
  294. mlrun/model_monitoring/stores/__init__.py +0 -111
  295. mlrun/model_monitoring/stores/kv_model_endpoint_store.py +0 -576
  296. mlrun/model_monitoring/stores/model_endpoint_store.py +0 -147
  297. mlrun/model_monitoring/stores/models/__init__.py +0 -27
  298. mlrun/model_monitoring/stores/models/base.py +0 -84
  299. mlrun/model_monitoring/stores/sql_model_endpoint_store.py +0 -384
  300. mlrun/platforms/other.py +0 -306
  301. mlrun-1.6.4rc8.dist-info/METADATA +0 -272
  302. mlrun-1.6.4rc8.dist-info/RECORD +0 -314
  303. {mlrun-1.6.4rc8.dist-info → mlrun-1.7.0.dist-info}/LICENSE +0 -0
  304. {mlrun-1.6.4rc8.dist-info → mlrun-1.7.0.dist-info}/entry_points.txt +0 -0
  305. {mlrun-1.6.4rc8.dist-info → mlrun-1.7.0.dist-info}/top_level.txt +0 -0
mlrun/utils/http.py CHANGED
@@ -14,7 +14,6 @@
14
14
  #
15
15
 
16
16
  import time
17
- import typing
18
17
 
19
18
  import requests
20
19
  import requests.adapters
@@ -96,7 +95,7 @@ class HTTPSessionWithRetry(requests.Session):
96
95
  total=self.max_retries,
97
96
  backoff_factor=self.retry_backoff_factor,
98
97
  status_forcelist=config.http_retry_defaults.status_codes,
99
- method_whitelist=self._retry_methods,
98
+ allowed_methods=self._retry_methods,
100
99
  # we want to retry but not to raise since we do want that last response (to parse details on the
101
100
  # error from response body) we'll handle raising ourselves
102
101
  raise_on_status=False,
@@ -123,7 +122,7 @@ class HTTPSessionWithRetry(requests.Session):
123
122
 
124
123
  self._logger.warning(
125
124
  "Error during request handling, retrying",
126
- exc=str(exc),
125
+ exc=err_to_str(exc),
127
126
  retry_count=retry_count,
128
127
  url=url,
129
128
  method=method,
@@ -202,9 +201,7 @@ class HTTPSessionWithRetry(requests.Session):
202
201
  def _method_retryable(self, method: str):
203
202
  return method in self._retry_methods
204
203
 
205
- def _resolve_retry_methods(
206
- self, retry_on_post: bool = False
207
- ) -> typing.FrozenSet[str]:
204
+ def _resolve_retry_methods(self, retry_on_post: bool = False) -> frozenset[str]:
208
205
  methods = urllib3.util.retry.Retry.DEFAULT_ALLOWED_METHODS
209
206
  methods = methods.union({"PATCH"})
210
207
  if retry_on_post:
mlrun/utils/logger.py CHANGED
@@ -12,23 +12,51 @@
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
14
 
15
- import json
16
15
  import logging
16
+ import os
17
17
  import typing
18
18
  from enum import Enum
19
+ from functools import cached_property
19
20
  from sys import stdout
20
21
  from traceback import format_exception
21
22
  from typing import IO, Optional, Union
22
23
 
23
- from mlrun.config import config
24
+ import orjson
25
+ import pydantic
24
26
 
27
+ from mlrun.config import config
25
28
 
26
- class JSONFormatter(logging.Formatter):
27
- def __init__(self):
28
- super(JSONFormatter, self).__init__()
29
- self._json_encoder = json.JSONEncoder()
30
29
 
31
- def format(self, record):
30
+ class _BaseFormatter(logging.Formatter):
31
+ def _json_dump(self, json_object):
32
+ def default(obj):
33
+ if isinstance(obj, pydantic.BaseModel):
34
+ return obj.dict()
35
+
36
+ # EAFP all the way.
37
+ # Leave the unused "exc" in for debugging ease
38
+ try:
39
+ return obj.__log__()
40
+ except Exception as exc: # noqa
41
+ try:
42
+ return obj.__repr__()
43
+ except Exception as exc: # noqa
44
+ try:
45
+ return str(obj)
46
+ except Exception as exc:
47
+ raise TypeError from exc
48
+
49
+ return orjson.dumps(
50
+ json_object,
51
+ option=orjson.OPT_NAIVE_UTC
52
+ | orjson.OPT_SERIALIZE_NUMPY
53
+ | orjson.OPT_SORT_KEYS,
54
+ default=default,
55
+ ).decode()
56
+
57
+
58
+ class JSONFormatter(_BaseFormatter):
59
+ def format(self, record) -> str:
32
60
  record_with = getattr(record, "with", {})
33
61
  if record.exc_info:
34
62
  record_with.update(exc_info=format_exception(*record.exc_info))
@@ -39,14 +67,24 @@ class JSONFormatter(logging.Formatter):
39
67
  "with": record_with,
40
68
  }
41
69
 
42
- return self._json_encoder.encode(record_fields)
70
+ return self._json_dump(record_fields)
43
71
 
44
72
 
45
- class HumanReadableFormatter(logging.Formatter):
73
+ class HumanReadableFormatter(_BaseFormatter):
46
74
  def format(self, record) -> str:
75
+ more = self._resolve_more(record)
76
+ return (
77
+ f"> {self.formatTime(record, self.datefmt)} "
78
+ f"[{record.levelname.lower()}] "
79
+ f"{record.getMessage().rstrip()}"
80
+ f"{more}"
81
+ )
82
+
83
+ def _resolve_more(self, record):
47
84
  record_with = self._record_with(record)
48
- more = f": {record_with}" if record_with else ""
49
- return f"> {self.formatTime(record, self.datefmt)} [{record.levelname.lower()}] {record.getMessage()}{more}"
85
+ record_with_encoded = self._json_dump(record_with) if record_with else ""
86
+ more = f": {record_with_encoded}" if record_with_encoded else ""
87
+ return more
50
88
 
51
89
  def _record_with(self, record):
52
90
  record_with = getattr(record, "with", {})
@@ -56,18 +94,67 @@ class HumanReadableFormatter(logging.Formatter):
56
94
 
57
95
 
58
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
+
59
107
  def format(self, record) -> str:
108
+ more = ""
60
109
  record_with = self._record_with(record)
61
- more = f": {record_with}" if record_with else ""
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
+ )
62
127
  return (
63
- "> "
128
+ f"{self._get_message_color(record.levelno)}> "
64
129
  f"{self.formatTime(record, self.datefmt)} "
65
130
  f"[{record.name}:{record.levelname.lower()}] "
66
- f"{record.getMessage()}{more}"
131
+ f"{record.getMessage()}{more}{self._get_color_reset()}"
67
132
  )
68
133
 
134
+ def _get_color_reset(self):
135
+ if not self._have_color_support:
136
+ return ""
69
137
 
70
- class Logger(object):
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
+
156
+
157
+ class Logger:
71
158
  def __init__(
72
159
  self,
73
160
  level,
@@ -189,7 +276,7 @@ class FormatterKinds(Enum):
189
276
 
190
277
  def resolve_formatter_by_kind(
191
278
  formatter_kind: FormatterKinds,
192
- ) -> typing.Type[
279
+ ) -> type[
193
280
  typing.Union[HumanReadableFormatter, HumanReadableExtendedFormatter, JSONFormatter]
194
281
  ]:
195
282
  return {
@@ -199,6 +286,15 @@ def resolve_formatter_by_kind(
199
286
  }[formatter_kind]
200
287
 
201
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
+
202
298
  def create_logger(
203
299
  level: Optional[str] = None,
204
300
  formatter_kind: str = FormatterKinds.HUMAN.name,
@@ -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
 
@@ -32,7 +31,7 @@ class NotificationTypes(str, enum.Enum):
32
31
  slack = NotificationKind.slack.value
33
32
  webhook = NotificationKind.webhook.value
34
33
 
35
- def get_notification(self) -> typing.Type[NotificationBase]:
34
+ def get_notification(self) -> type[NotificationBase]:
36
35
  return {
37
36
  self.console: ConsoleNotification,
38
37
  self.git: GitNotification,
@@ -41,7 +40,7 @@ class NotificationTypes(str, enum.Enum):
41
40
  self.webhook: WebhookNotification,
42
41
  }.get(self)
43
42
 
44
- def inverse_dependencies(self) -> typing.List[str]:
43
+ def inverse_dependencies(self) -> list[str]:
45
44
  """
46
45
  Some notifications should only run if another notification type didn't run.
47
46
  Per given notification type, return a list of notification types that should not run in order for this
@@ -52,13 +51,18 @@ class NotificationTypes(str, enum.Enum):
52
51
  }.get(self, [])
53
52
 
54
53
  @classmethod
55
- def all(cls) -> typing.List[str]:
56
- return list(
57
- [
58
- cls.console,
59
- cls.git,
60
- cls.ipython,
61
- cls.slack,
62
- cls.webhook,
63
- ]
64
- )
54
+ def local(cls) -> list[str]:
55
+ return [
56
+ cls.console,
57
+ cls.ipython,
58
+ ]
59
+
60
+ @classmethod
61
+ def all(cls) -> list[str]:
62
+ return [
63
+ cls.console,
64
+ cls.git,
65
+ cls.ipython,
66
+ cls.slack,
67
+ cls.webhook,
68
+ ]
@@ -23,11 +23,15 @@ class NotificationBase:
23
23
  def __init__(
24
24
  self,
25
25
  name: str = None,
26
- params: typing.Dict[str, str] = None,
26
+ params: dict[str, str] = None,
27
27
  ):
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,12 +48,14 @@ 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
 
50
56
  def load_notification(
51
57
  self,
52
- params: typing.Dict[str, str],
58
+ params: dict[str, str],
53
59
  ) -> None:
54
60
  self.params = params or {}
55
61
 
@@ -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,
@@ -29,7 +29,7 @@ class IPythonNotification(NotificationBase):
29
29
  def __init__(
30
30
  self,
31
31
  name: str = None,
32
- params: typing.Dict[str, str] = None,
32
+ params: dict[str, str] = None,
33
33
  ):
34
34
  super().__init__(name, params)
35
35
  self._ipython = None
@@ -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: