mlrun 1.7.1rc4__py3-none-any.whl → 1.8.0rc8__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 (257) hide show
  1. mlrun/__init__.py +23 -21
  2. mlrun/__main__.py +3 -3
  3. mlrun/alerts/alert.py +148 -14
  4. mlrun/artifacts/__init__.py +1 -2
  5. mlrun/artifacts/base.py +46 -12
  6. mlrun/artifacts/dataset.py +16 -16
  7. mlrun/artifacts/document.py +334 -0
  8. mlrun/artifacts/manager.py +15 -13
  9. mlrun/artifacts/model.py +66 -53
  10. mlrun/common/constants.py +7 -0
  11. mlrun/common/formatters/__init__.py +1 -0
  12. mlrun/common/formatters/feature_set.py +1 -0
  13. mlrun/common/formatters/function.py +1 -0
  14. mlrun/{model_monitoring/db/stores/base/__init__.py → common/formatters/model_endpoint.py} +16 -1
  15. mlrun/common/formatters/pipeline.py +1 -2
  16. mlrun/common/formatters/project.py +9 -0
  17. mlrun/common/model_monitoring/__init__.py +0 -5
  18. mlrun/common/model_monitoring/helpers.py +1 -29
  19. mlrun/common/runtimes/constants.py +1 -2
  20. mlrun/common/schemas/__init__.py +6 -2
  21. mlrun/common/schemas/alert.py +111 -19
  22. mlrun/common/schemas/api_gateway.py +3 -3
  23. mlrun/common/schemas/artifact.py +11 -7
  24. mlrun/common/schemas/auth.py +6 -4
  25. mlrun/common/schemas/background_task.py +7 -7
  26. mlrun/common/schemas/client_spec.py +2 -3
  27. mlrun/common/schemas/clusterization_spec.py +2 -2
  28. mlrun/common/schemas/common.py +53 -3
  29. mlrun/common/schemas/constants.py +15 -0
  30. mlrun/common/schemas/datastore_profile.py +1 -1
  31. mlrun/common/schemas/feature_store.py +9 -9
  32. mlrun/common/schemas/frontend_spec.py +4 -4
  33. mlrun/common/schemas/function.py +10 -10
  34. mlrun/common/schemas/hub.py +1 -1
  35. mlrun/common/schemas/k8s.py +3 -3
  36. mlrun/common/schemas/memory_reports.py +3 -3
  37. mlrun/common/schemas/model_monitoring/__init__.py +2 -1
  38. mlrun/common/schemas/model_monitoring/constants.py +66 -14
  39. mlrun/common/schemas/model_monitoring/grafana.py +1 -1
  40. mlrun/common/schemas/model_monitoring/model_endpoints.py +91 -147
  41. mlrun/common/schemas/notification.py +24 -3
  42. mlrun/common/schemas/object.py +1 -1
  43. mlrun/common/schemas/pagination.py +4 -4
  44. mlrun/common/schemas/partition.py +137 -0
  45. mlrun/common/schemas/pipeline.py +2 -2
  46. mlrun/common/schemas/project.py +25 -17
  47. mlrun/common/schemas/runs.py +2 -2
  48. mlrun/common/schemas/runtime_resource.py +5 -5
  49. mlrun/common/schemas/schedule.py +1 -1
  50. mlrun/common/schemas/secret.py +1 -1
  51. mlrun/common/schemas/tag.py +3 -3
  52. mlrun/common/schemas/workflow.py +5 -5
  53. mlrun/config.py +67 -10
  54. mlrun/data_types/__init__.py +0 -2
  55. mlrun/data_types/infer.py +3 -1
  56. mlrun/data_types/spark.py +2 -1
  57. mlrun/datastore/__init__.py +0 -2
  58. mlrun/datastore/alibaba_oss.py +4 -1
  59. mlrun/datastore/azure_blob.py +4 -1
  60. mlrun/datastore/base.py +12 -4
  61. mlrun/datastore/datastore.py +9 -3
  62. mlrun/datastore/datastore_profile.py +79 -20
  63. mlrun/datastore/dbfs_store.py +4 -1
  64. mlrun/datastore/filestore.py +4 -1
  65. mlrun/datastore/google_cloud_storage.py +4 -1
  66. mlrun/datastore/hdfs.py +4 -1
  67. mlrun/datastore/inmem.py +4 -1
  68. mlrun/datastore/redis.py +4 -1
  69. mlrun/datastore/s3.py +4 -1
  70. mlrun/datastore/sources.py +52 -51
  71. mlrun/datastore/store_resources.py +0 -2
  72. mlrun/datastore/targets.py +21 -21
  73. mlrun/datastore/utils.py +2 -2
  74. mlrun/datastore/v3io.py +4 -1
  75. mlrun/datastore/vectorstore.py +194 -0
  76. mlrun/datastore/wasbfs/fs.py +13 -12
  77. mlrun/db/base.py +208 -82
  78. mlrun/db/factory.py +0 -3
  79. mlrun/db/httpdb.py +1237 -386
  80. mlrun/db/nopdb.py +201 -74
  81. mlrun/errors.py +2 -2
  82. mlrun/execution.py +136 -50
  83. mlrun/feature_store/__init__.py +0 -2
  84. mlrun/feature_store/api.py +41 -40
  85. mlrun/feature_store/common.py +9 -9
  86. mlrun/feature_store/feature_set.py +20 -18
  87. mlrun/feature_store/feature_vector.py +27 -24
  88. mlrun/feature_store/retrieval/base.py +14 -9
  89. mlrun/feature_store/retrieval/job.py +2 -1
  90. mlrun/feature_store/steps.py +2 -2
  91. mlrun/features.py +30 -13
  92. mlrun/frameworks/__init__.py +1 -2
  93. mlrun/frameworks/_common/__init__.py +1 -2
  94. mlrun/frameworks/_common/artifacts_library.py +2 -2
  95. mlrun/frameworks/_common/mlrun_interface.py +10 -6
  96. mlrun/frameworks/_common/model_handler.py +29 -27
  97. mlrun/frameworks/_common/producer.py +3 -1
  98. mlrun/frameworks/_dl_common/__init__.py +1 -2
  99. mlrun/frameworks/_dl_common/loggers/__init__.py +1 -2
  100. mlrun/frameworks/_dl_common/loggers/mlrun_logger.py +4 -4
  101. mlrun/frameworks/_dl_common/loggers/tensorboard_logger.py +3 -3
  102. mlrun/frameworks/_ml_common/__init__.py +1 -2
  103. mlrun/frameworks/_ml_common/loggers/__init__.py +1 -2
  104. mlrun/frameworks/_ml_common/model_handler.py +21 -21
  105. mlrun/frameworks/_ml_common/plans/__init__.py +1 -2
  106. mlrun/frameworks/_ml_common/plans/confusion_matrix_plan.py +3 -1
  107. mlrun/frameworks/_ml_common/plans/dataset_plan.py +3 -3
  108. mlrun/frameworks/_ml_common/plans/roc_curve_plan.py +4 -4
  109. mlrun/frameworks/auto_mlrun/__init__.py +1 -2
  110. mlrun/frameworks/auto_mlrun/auto_mlrun.py +22 -15
  111. mlrun/frameworks/huggingface/__init__.py +1 -2
  112. mlrun/frameworks/huggingface/model_server.py +9 -9
  113. mlrun/frameworks/lgbm/__init__.py +47 -44
  114. mlrun/frameworks/lgbm/callbacks/__init__.py +1 -2
  115. mlrun/frameworks/lgbm/callbacks/logging_callback.py +4 -2
  116. mlrun/frameworks/lgbm/callbacks/mlrun_logging_callback.py +4 -2
  117. mlrun/frameworks/lgbm/mlrun_interfaces/__init__.py +1 -2
  118. mlrun/frameworks/lgbm/mlrun_interfaces/mlrun_interface.py +5 -5
  119. mlrun/frameworks/lgbm/model_handler.py +15 -11
  120. mlrun/frameworks/lgbm/model_server.py +11 -7
  121. mlrun/frameworks/lgbm/utils.py +2 -2
  122. mlrun/frameworks/onnx/__init__.py +1 -2
  123. mlrun/frameworks/onnx/dataset.py +3 -3
  124. mlrun/frameworks/onnx/mlrun_interface.py +2 -2
  125. mlrun/frameworks/onnx/model_handler.py +7 -5
  126. mlrun/frameworks/onnx/model_server.py +8 -6
  127. mlrun/frameworks/parallel_coordinates.py +11 -11
  128. mlrun/frameworks/pytorch/__init__.py +22 -23
  129. mlrun/frameworks/pytorch/callbacks/__init__.py +1 -2
  130. mlrun/frameworks/pytorch/callbacks/callback.py +2 -1
  131. mlrun/frameworks/pytorch/callbacks/logging_callback.py +15 -8
  132. mlrun/frameworks/pytorch/callbacks/mlrun_logging_callback.py +19 -12
  133. mlrun/frameworks/pytorch/callbacks/tensorboard_logging_callback.py +22 -15
  134. mlrun/frameworks/pytorch/callbacks_handler.py +36 -30
  135. mlrun/frameworks/pytorch/mlrun_interface.py +17 -17
  136. mlrun/frameworks/pytorch/model_handler.py +21 -17
  137. mlrun/frameworks/pytorch/model_server.py +13 -9
  138. mlrun/frameworks/sklearn/__init__.py +19 -18
  139. mlrun/frameworks/sklearn/estimator.py +2 -2
  140. mlrun/frameworks/sklearn/metric.py +3 -3
  141. mlrun/frameworks/sklearn/metrics_library.py +8 -6
  142. mlrun/frameworks/sklearn/mlrun_interface.py +3 -2
  143. mlrun/frameworks/sklearn/model_handler.py +4 -3
  144. mlrun/frameworks/tf_keras/__init__.py +11 -12
  145. mlrun/frameworks/tf_keras/callbacks/__init__.py +1 -2
  146. mlrun/frameworks/tf_keras/callbacks/logging_callback.py +17 -14
  147. mlrun/frameworks/tf_keras/callbacks/mlrun_logging_callback.py +15 -12
  148. mlrun/frameworks/tf_keras/callbacks/tensorboard_logging_callback.py +21 -18
  149. mlrun/frameworks/tf_keras/model_handler.py +17 -13
  150. mlrun/frameworks/tf_keras/model_server.py +12 -8
  151. mlrun/frameworks/xgboost/__init__.py +19 -18
  152. mlrun/frameworks/xgboost/model_handler.py +13 -9
  153. mlrun/launcher/base.py +3 -4
  154. mlrun/launcher/local.py +1 -1
  155. mlrun/launcher/remote.py +1 -1
  156. mlrun/lists.py +4 -3
  157. mlrun/model.py +117 -46
  158. mlrun/model_monitoring/__init__.py +4 -4
  159. mlrun/model_monitoring/api.py +61 -59
  160. mlrun/model_monitoring/applications/_application_steps.py +17 -17
  161. mlrun/model_monitoring/applications/base.py +165 -6
  162. mlrun/model_monitoring/applications/context.py +88 -37
  163. mlrun/model_monitoring/applications/evidently_base.py +1 -2
  164. mlrun/model_monitoring/applications/histogram_data_drift.py +43 -21
  165. mlrun/model_monitoring/applications/results.py +55 -3
  166. mlrun/model_monitoring/controller.py +207 -239
  167. mlrun/model_monitoring/db/__init__.py +0 -2
  168. mlrun/model_monitoring/db/_schedules.py +156 -0
  169. mlrun/model_monitoring/db/_stats.py +189 -0
  170. mlrun/model_monitoring/db/tsdb/base.py +78 -25
  171. mlrun/model_monitoring/db/tsdb/tdengine/schemas.py +90 -16
  172. mlrun/model_monitoring/db/tsdb/tdengine/stream_graph_steps.py +33 -0
  173. mlrun/model_monitoring/db/tsdb/tdengine/tdengine_connector.py +279 -59
  174. mlrun/model_monitoring/db/tsdb/v3io/stream_graph_steps.py +1 -0
  175. mlrun/model_monitoring/db/tsdb/v3io/v3io_connector.py +78 -17
  176. mlrun/model_monitoring/helpers.py +152 -49
  177. mlrun/model_monitoring/stream_processing.py +99 -283
  178. mlrun/model_monitoring/tracking_policy.py +10 -3
  179. mlrun/model_monitoring/writer.py +48 -36
  180. mlrun/package/__init__.py +3 -6
  181. mlrun/package/context_handler.py +1 -1
  182. mlrun/package/packager.py +12 -9
  183. mlrun/package/packagers/__init__.py +0 -2
  184. mlrun/package/packagers/default_packager.py +14 -11
  185. mlrun/package/packagers/numpy_packagers.py +16 -7
  186. mlrun/package/packagers/pandas_packagers.py +18 -18
  187. mlrun/package/packagers/python_standard_library_packagers.py +25 -11
  188. mlrun/package/packagers_manager.py +31 -14
  189. mlrun/package/utils/__init__.py +0 -3
  190. mlrun/package/utils/_pickler.py +6 -6
  191. mlrun/platforms/__init__.py +47 -16
  192. mlrun/platforms/iguazio.py +4 -1
  193. mlrun/projects/operations.py +27 -27
  194. mlrun/projects/pipelines.py +75 -38
  195. mlrun/projects/project.py +865 -206
  196. mlrun/run.py +53 -10
  197. mlrun/runtimes/__init__.py +1 -3
  198. mlrun/runtimes/base.py +15 -11
  199. mlrun/runtimes/daskjob.py +9 -9
  200. mlrun/runtimes/generators.py +2 -1
  201. mlrun/runtimes/kubejob.py +4 -5
  202. mlrun/runtimes/mounts.py +572 -0
  203. mlrun/runtimes/mpijob/__init__.py +0 -2
  204. mlrun/runtimes/mpijob/abstract.py +7 -6
  205. mlrun/runtimes/nuclio/api_gateway.py +7 -7
  206. mlrun/runtimes/nuclio/application/application.py +11 -11
  207. mlrun/runtimes/nuclio/function.py +19 -17
  208. mlrun/runtimes/nuclio/serving.py +18 -11
  209. mlrun/runtimes/pod.py +154 -45
  210. mlrun/runtimes/remotesparkjob.py +3 -2
  211. mlrun/runtimes/sparkjob/__init__.py +0 -2
  212. mlrun/runtimes/sparkjob/spark3job.py +21 -11
  213. mlrun/runtimes/utils.py +6 -5
  214. mlrun/serving/merger.py +6 -4
  215. mlrun/serving/remote.py +18 -17
  216. mlrun/serving/routers.py +185 -172
  217. mlrun/serving/server.py +7 -1
  218. mlrun/serving/states.py +97 -78
  219. mlrun/serving/utils.py +13 -2
  220. mlrun/serving/v1_serving.py +3 -2
  221. mlrun/serving/v2_serving.py +74 -65
  222. mlrun/track/__init__.py +1 -1
  223. mlrun/track/tracker.py +2 -2
  224. mlrun/track/trackers/mlflow_tracker.py +6 -5
  225. mlrun/utils/async_http.py +1 -1
  226. mlrun/utils/clones.py +1 -1
  227. mlrun/utils/helpers.py +66 -18
  228. mlrun/utils/logger.py +106 -4
  229. mlrun/utils/notifications/notification/__init__.py +22 -19
  230. mlrun/utils/notifications/notification/base.py +33 -14
  231. mlrun/utils/notifications/notification/console.py +6 -6
  232. mlrun/utils/notifications/notification/git.py +11 -11
  233. mlrun/utils/notifications/notification/ipython.py +10 -9
  234. mlrun/utils/notifications/notification/mail.py +176 -0
  235. mlrun/utils/notifications/notification/slack.py +6 -6
  236. mlrun/utils/notifications/notification/webhook.py +6 -6
  237. mlrun/utils/notifications/notification_pusher.py +86 -44
  238. mlrun/utils/regex.py +3 -1
  239. mlrun/utils/version/version.json +2 -2
  240. {mlrun-1.7.1rc4.dist-info → mlrun-1.8.0rc8.dist-info}/METADATA +191 -186
  241. mlrun-1.8.0rc8.dist-info/RECORD +347 -0
  242. {mlrun-1.7.1rc4.dist-info → mlrun-1.8.0rc8.dist-info}/WHEEL +1 -1
  243. mlrun/model_monitoring/db/stores/__init__.py +0 -136
  244. mlrun/model_monitoring/db/stores/base/store.py +0 -213
  245. mlrun/model_monitoring/db/stores/sqldb/__init__.py +0 -13
  246. mlrun/model_monitoring/db/stores/sqldb/models/__init__.py +0 -71
  247. mlrun/model_monitoring/db/stores/sqldb/models/base.py +0 -190
  248. mlrun/model_monitoring/db/stores/sqldb/models/mysql.py +0 -103
  249. mlrun/model_monitoring/db/stores/sqldb/models/sqlite.py +0 -40
  250. mlrun/model_monitoring/db/stores/sqldb/sql_store.py +0 -659
  251. mlrun/model_monitoring/db/stores/v3io_kv/__init__.py +0 -13
  252. mlrun/model_monitoring/db/stores/v3io_kv/kv_store.py +0 -726
  253. mlrun/model_monitoring/model_endpoint.py +0 -118
  254. mlrun-1.7.1rc4.dist-info/RECORD +0 -351
  255. {mlrun-1.7.1rc4.dist-info → mlrun-1.8.0rc8.dist-info}/LICENSE +0 -0
  256. {mlrun-1.7.1rc4.dist-info → mlrun-1.8.0rc8.dist-info}/entry_points.txt +0 -0
  257. {mlrun-1.7.1rc4.dist-info → mlrun-1.8.0rc8.dist-info}/top_level.txt +0 -0
mlrun/utils/helpers.py CHANGED
@@ -40,7 +40,6 @@ import pandas
40
40
  import semver
41
41
  import yaml
42
42
  from dateutil import parser
43
- from mlrun_pipelines.models import PipelineRun
44
43
  from pandas import Timedelta, Timestamp
45
44
  from yaml.representer import RepresenterError
46
45
 
@@ -52,6 +51,7 @@ import mlrun.utils.regex
52
51
  import mlrun.utils.version.version
53
52
  from mlrun.common.constants import MYSQL_MEDIUMBLOB_SIZE_BYTES
54
53
  from mlrun.config import config
54
+ from mlrun_pipelines.models import PipelineRun
55
55
 
56
56
  from .logger import create_logger
57
57
  from .retryer import ( # noqa: F401
@@ -167,6 +167,7 @@ class RunKeys:
167
167
  inputs = "inputs"
168
168
  returns = "returns"
169
169
  artifacts = "artifacts"
170
+ artifact_uris = "artifact_uris"
170
171
  outputs = "outputs"
171
172
  data_stores = "data_stores"
172
173
  secrets = "secret_sources"
@@ -220,7 +221,7 @@ def verify_field_regex(
220
221
 
221
222
 
222
223
  def validate_builder_source(
223
- source: str, pull_at_runtime: bool = False, workdir: str = None
224
+ source: str, pull_at_runtime: bool = False, workdir: Optional[str] = None
224
225
  ):
225
226
  if pull_at_runtime or not source:
226
227
  return
@@ -268,12 +269,14 @@ def validate_tag_name(
268
269
  def validate_artifact_key_name(
269
270
  artifact_key: str, field_name: str, raise_on_failure: bool = True
270
271
  ) -> bool:
272
+ field_type = "key" if field_name == "artifact.key" else "db_key"
271
273
  return mlrun.utils.helpers.verify_field_regex(
272
274
  field_name,
273
275
  artifact_key,
274
276
  mlrun.utils.regex.artifact_key,
275
277
  raise_on_failure=raise_on_failure,
276
- log_message="Slashes are not permitted in the artifact key (both \\ and /)",
278
+ log_message=f"Artifact {field_type} must start and end with an alphanumeric character, and may only contain "
279
+ "letters, numbers, hyphens, underscores, and dots.",
277
280
  )
278
281
 
279
282
 
@@ -354,8 +357,8 @@ def verify_field_list_of_type(
354
357
  def verify_dict_items_type(
355
358
  name: str,
356
359
  dictionary: dict,
357
- expected_keys_types: list = None,
358
- expected_values_types: list = None,
360
+ expected_keys_types: Optional[list] = None,
361
+ expected_values_types: Optional[list] = None,
359
362
  ):
360
363
  if dictionary:
361
364
  if not isinstance(dictionary, dict):
@@ -372,7 +375,7 @@ def verify_dict_items_type(
372
375
  ) from exc
373
376
 
374
377
 
375
- def verify_list_items_type(list_, expected_types: list = None):
378
+ def verify_list_items_type(list_, expected_types: Optional[list] = None):
376
379
  if list_ and expected_types:
377
380
  list_items_types = set(map(type, list_))
378
381
  expected_types = set(expected_types)
@@ -396,6 +399,10 @@ def now_date(tz: timezone = timezone.utc) -> datetime:
396
399
  return datetime.now(tz=tz)
397
400
 
398
401
 
402
+ def datetime_min(tz: timezone = timezone.utc) -> datetime:
403
+ return datetime(1970, 1, 1, tzinfo=tz)
404
+
405
+
399
406
  datetime_now = now_date
400
407
 
401
408
 
@@ -816,7 +823,9 @@ def _convert_python_package_version_to_image_tag(version: typing.Optional[str]):
816
823
 
817
824
 
818
825
  def enrich_image_url(
819
- image_url: str, client_version: str = None, client_python_version: str = None
826
+ image_url: str,
827
+ client_version: Optional[str] = None,
828
+ client_python_version: Optional[str] = None,
820
829
  ) -> str:
821
830
  client_version = _convert_python_package_version_to_image_tag(client_version)
822
831
  server_version = _convert_python_package_version_to_image_tag(
@@ -856,7 +865,7 @@ def enrich_image_url(
856
865
 
857
866
 
858
867
  def resolve_image_tag_suffix(
859
- mlrun_version: str = None, python_version: str = None
868
+ mlrun_version: Optional[str] = None, python_version: Optional[str] = None
860
869
  ) -> str:
861
870
  """
862
871
  resolves what suffix should be appended to the image tag
@@ -1175,7 +1184,7 @@ def get_function(function, namespaces, reload_modules: bool = False):
1175
1184
  def get_handler_extended(
1176
1185
  handler_path: str,
1177
1186
  context=None,
1178
- class_args: dict = None,
1187
+ class_args: Optional[dict] = None,
1179
1188
  namespaces=None,
1180
1189
  reload_modules: bool = False,
1181
1190
  ):
@@ -1226,14 +1235,24 @@ def datetime_to_iso(time_obj: Optional[datetime]) -> Optional[str]:
1226
1235
  return time_obj.isoformat()
1227
1236
 
1228
1237
 
1229
- def enrich_datetime_with_tz_info(timestamp_string):
1238
+ def enrich_datetime_with_tz_info(timestamp_string) -> Optional[datetime]:
1230
1239
  if not timestamp_string:
1231
1240
  return timestamp_string
1232
1241
 
1233
1242
  if timestamp_string and not mlrun.utils.helpers.has_timezone(timestamp_string):
1234
1243
  timestamp_string += datetime.now(timezone.utc).astimezone().strftime("%z")
1235
1244
 
1236
- return datetime.strptime(timestamp_string, "%Y-%m-%d %H:%M:%S.%f%z")
1245
+ for _format in [
1246
+ # e.g: 2021-08-25 12:00:00.000Z
1247
+ "%Y-%m-%d %H:%M:%S.%f%z",
1248
+ # e.g: 2024-11-11 07:44:56+0000
1249
+ "%Y-%m-%d %H:%M:%S%z",
1250
+ ]:
1251
+ try:
1252
+ return datetime.strptime(timestamp_string, _format)
1253
+ except ValueError as exc:
1254
+ last_exc = exc
1255
+ raise last_exc
1237
1256
 
1238
1257
 
1239
1258
  def has_timezone(timestamp):
@@ -1684,17 +1703,22 @@ def merge_dicts_with_precedence(*dicts: dict) -> dict:
1684
1703
 
1685
1704
 
1686
1705
  def validate_component_version_compatibility(
1687
- component_name: typing.Literal["iguazio", "nuclio"], *min_versions: str
1706
+ component_name: typing.Literal["iguazio", "nuclio", "mlrun-client"],
1707
+ *min_versions: str,
1708
+ mlrun_client_version: Optional[str] = None,
1688
1709
  ):
1689
1710
  """
1690
1711
  :param component_name: Name of the component to validate compatibility for.
1691
1712
  :param min_versions: Valid minimum version(s) required, assuming no 2 versions has equal major and minor.
1713
+ :param mlrun_client_version: Client version to validate when component_name is "mlrun-client".
1692
1714
  """
1693
1715
  parsed_min_versions = [
1694
1716
  semver.VersionInfo.parse(min_version) for min_version in min_versions
1695
1717
  ]
1696
1718
  parsed_current_version = None
1697
1719
  component_current_version = None
1720
+ # For mlrun client we don't assume compatability if we fail to parse the client version
1721
+ assume_compatible = component_name not in ["mlrun-client"]
1698
1722
  try:
1699
1723
  if component_name == "iguazio":
1700
1724
  component_current_version = mlrun.mlconf.igz_version
@@ -1711,18 +1735,29 @@ def validate_component_version_compatibility(
1711
1735
  parsed_current_version = semver.VersionInfo.parse(
1712
1736
  mlrun.mlconf.nuclio_version
1713
1737
  )
1738
+ if component_name == "mlrun-client":
1739
+ # dev version, assume compatible
1740
+ if mlrun_client_version and (
1741
+ mlrun_client_version.startswith("0.0.0+")
1742
+ or "unstable" in mlrun_client_version
1743
+ ):
1744
+ return True
1745
+
1746
+ component_current_version = mlrun_client_version
1747
+ parsed_current_version = semver.Version.parse(mlrun_client_version)
1714
1748
  if not parsed_current_version:
1715
- return True
1749
+ return assume_compatible
1716
1750
  except ValueError:
1717
1751
  # only log when version is set but invalid
1718
1752
  if component_current_version:
1719
1753
  logger.warning(
1720
- "Unable to parse current version, assuming compatibility",
1754
+ "Unable to parse current version",
1721
1755
  component_name=component_name,
1722
1756
  current_version=component_current_version,
1723
1757
  min_versions=min_versions,
1758
+ assume_compatible=assume_compatible,
1724
1759
  )
1725
- return True
1760
+ return assume_compatible
1726
1761
 
1727
1762
  # Feature might have been back-ported e.g. nuclio node selection is supported from
1728
1763
  # 1.5.20 and 1.6.10 but not in 1.6.9 - therefore we reverse sort to validate against 1.6.x 1st and
@@ -1787,9 +1822,8 @@ def _reload(module, max_recursion_depth):
1787
1822
  def run_with_retry(
1788
1823
  retry_count: int,
1789
1824
  func: typing.Callable,
1790
- retry_on_exceptions: typing.Union[
1791
- type[Exception],
1792
- tuple[type[Exception]],
1825
+ retry_on_exceptions: Optional[
1826
+ typing.Union[type[Exception], tuple[type[Exception]]]
1793
1827
  ] = None,
1794
1828
  *args,
1795
1829
  **kwargs,
@@ -1822,3 +1856,17 @@ def run_with_retry(
1822
1856
  if attempt == retry_count:
1823
1857
  raise
1824
1858
  raise last_exception
1859
+
1860
+
1861
+ def join_urls(base_url: Optional[str], path: Optional[str]) -> str:
1862
+ """
1863
+ Joins a base URL with a path, ensuring proper handling of slashes.
1864
+
1865
+ :param base_url: The base URL (e.g., "http://example.com").
1866
+ :param path: The path to append to the base URL (e.g., "/path/to/resource").
1867
+
1868
+ :return: A unified URL with exactly one slash between base_url and path.
1869
+ """
1870
+ if base_url is None:
1871
+ base_url = ""
1872
+ return f"{base_url.rstrip('/')}/{path.lstrip('/')}" if path else base_url
mlrun/utils/logger.py CHANGED
@@ -11,9 +11,11 @@
11
11
  # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
-
14
+ import datetime
15
15
  import logging
16
16
  import os
17
+ import string
18
+ import sys
17
19
  import typing
18
20
  from enum import Enum
19
21
  from functools import cached_property
@@ -22,15 +24,16 @@ from traceback import format_exception
22
24
  from typing import IO, Optional, Union
23
25
 
24
26
  import orjson
25
- import pydantic
27
+ import pydantic.v1
26
28
 
29
+ from mlrun import errors
27
30
  from mlrun.config import config
28
31
 
29
32
 
30
33
  class _BaseFormatter(logging.Formatter):
31
34
  def _json_dump(self, json_object):
32
35
  def default(obj):
33
- if isinstance(obj, pydantic.BaseModel):
36
+ if isinstance(obj, pydantic.v1.BaseModel):
34
37
  return obj.dict()
35
38
 
36
39
  # EAFP all the way.
@@ -93,6 +96,98 @@ class HumanReadableFormatter(_BaseFormatter):
93
96
  return record_with
94
97
 
95
98
 
99
+ class CustomFormatter(HumanReadableFormatter):
100
+ """
101
+ To enable custom logger formatter, configure MLRun with the following env variables:
102
+ 1. "MLRUN_LOG_FORMATTER" = "custom" - change the default log formatter.
103
+ 2. "MLRUN_LOG_FORMAT_OVERRIDE" = "> {timestamp} [{level}] Running module: {module} {message} {more}" - logger format
104
+ * Note that your custom format must include those 4 fields - timestamp, level, message and more
105
+ If the custom format is not configured properly , MLRun will use the default logger (human format).
106
+ """
107
+
108
+ # This attribute is used to solve an issue
109
+ # that causes the warning to be written numerous times(for any log generation).
110
+ # We want to print the errors just once, not for each logger generation.
111
+ fail_on_format_configuration = False # for issues that relates to unrecognized keys
112
+ fail_on_missing_default_keys_key = (
113
+ False # for issues that relates to missing default keys
114
+ )
115
+
116
+ def format(self, record) -> str:
117
+ more = self._resolve_more(record)
118
+ custom_format = config.log_format_override
119
+ _custom_format = None
120
+ current_time = datetime.datetime.now()
121
+ formatted_time = current_time.strftime("%Y-%m-%d %H:%M:%S,%f")[:-3]
122
+ try:
123
+ if custom_format:
124
+ default_keys = ["timestamp", "level", "message", "more"]
125
+ formatter = string.Formatter()
126
+ custom_format_keys = [
127
+ key
128
+ for _, key, _, _ in formatter.parse(custom_format)
129
+ if key is not None
130
+ ]
131
+ missing_default_flags = list(
132
+ set(default_keys) - set(custom_format_keys)
133
+ )
134
+
135
+ if (
136
+ missing_default_flags
137
+ and not CustomFormatter.fail_on_missing_default_keys_key
138
+ ):
139
+ print(
140
+ f'> {formatted_time} [warning] Custom loggers must '
141
+ f'include those keys within the logger format, {", ".join(default_keys)} '
142
+ f'your format is missing: {", ".join(missing_default_flags)}',
143
+ file=sys.stderr,
144
+ )
145
+ CustomFormatter.fail_on_missing_default_keys_key = True
146
+ record_dict = record.__dict__
147
+ missing_format_configuraiton_keys = list(
148
+ set(custom_format_keys)
149
+ - set(default_keys)
150
+ - set(record_dict.keys())
151
+ )
152
+ if missing_format_configuraiton_keys:
153
+ if not CustomFormatter.fail_on_format_configuration:
154
+ print(
155
+ f"> {formatted_time} [warning] Failed to create custom logger due "
156
+ f'to missing format key in the log record: {", ".join(missing_format_configuraiton_keys)}',
157
+ file=sys.stderr,
158
+ )
159
+ CustomFormatter.fail_on_format_configuration = True
160
+ _format = (
161
+ f"> {self.formatTime(record, self.datefmt)} "
162
+ f"[{record.levelname.lower()}] "
163
+ f"{record.getMessage().rstrip()}"
164
+ f"{more}"
165
+ )
166
+ _custom_format = custom_format.format(
167
+ timestamp=self.formatTime(record, self.datefmt),
168
+ level=record.levelname.lower(),
169
+ message=record.getMessage().rstrip(),
170
+ more=more or "",
171
+ **record_dict,
172
+ )
173
+ CustomFormatter.fail_on_format_configuration = True
174
+ except Exception as e:
175
+ if not CustomFormatter.fail_on_format_configuration:
176
+ print(
177
+ f"> {formatted_time} [warning] Failed to create custom logger, "
178
+ f"see Exception: {errors.err_to_str(e)}",
179
+ file=sys.stderr,
180
+ )
181
+ CustomFormatter.fail_on_format_configuration = True
182
+ _format = _custom_format or (
183
+ f"> {self.formatTime(record, self.datefmt)} "
184
+ f"[{record.levelname.lower()}] "
185
+ f"{record.getMessage().rstrip()}"
186
+ f"{more}"
187
+ )
188
+ return _format
189
+
190
+
96
191
  class HumanReadableExtendedFormatter(HumanReadableFormatter):
97
192
  _colors = {
98
193
  logging.NOTSET: "",
@@ -272,17 +367,24 @@ class FormatterKinds(Enum):
272
367
  HUMAN = "human"
273
368
  HUMAN_EXTENDED = "human_extended"
274
369
  JSON = "json"
370
+ CUSTOM = "custom"
275
371
 
276
372
 
277
373
  def resolve_formatter_by_kind(
278
374
  formatter_kind: FormatterKinds,
279
375
  ) -> type[
280
- typing.Union[HumanReadableFormatter, HumanReadableExtendedFormatter, JSONFormatter]
376
+ typing.Union[
377
+ HumanReadableFormatter,
378
+ HumanReadableExtendedFormatter,
379
+ JSONFormatter,
380
+ CustomFormatter,
381
+ ]
281
382
  ]:
282
383
  return {
283
384
  FormatterKinds.HUMAN: HumanReadableFormatter,
284
385
  FormatterKinds.HUMAN_EXTENDED: HumanReadableExtendedFormatter,
285
386
  FormatterKinds.JSON: JSONFormatter,
387
+ FormatterKinds.CUSTOM: CustomFormatter,
286
388
  }[formatter_kind]
287
389
 
288
390
 
@@ -14,30 +14,32 @@
14
14
 
15
15
  import enum
16
16
 
17
- from mlrun.common.schemas.notification import NotificationKind
18
-
19
- from .base import NotificationBase
20
- from .console import ConsoleNotification
21
- from .git import GitNotification
22
- from .ipython import IPythonNotification
23
- from .slack import SlackNotification
24
- from .webhook import WebhookNotification
17
+ import mlrun.common.schemas.notification as notifications
18
+ import mlrun.utils.notifications.notification.base as base
19
+ import mlrun.utils.notifications.notification.console as console
20
+ import mlrun.utils.notifications.notification.git as git
21
+ import mlrun.utils.notifications.notification.ipython as ipython
22
+ import mlrun.utils.notifications.notification.mail as mail
23
+ import mlrun.utils.notifications.notification.slack as slack
24
+ import mlrun.utils.notifications.notification.webhook as webhook
25
25
 
26
26
 
27
27
  class NotificationTypes(str, enum.Enum):
28
- console = NotificationKind.console.value
29
- git = NotificationKind.git.value
30
- ipython = NotificationKind.ipython.value
31
- slack = NotificationKind.slack.value
32
- webhook = NotificationKind.webhook.value
28
+ console = notifications.NotificationKind.console.value
29
+ git = notifications.NotificationKind.git.value
30
+ ipython = notifications.NotificationKind.ipython.value
31
+ slack = notifications.NotificationKind.slack.value
32
+ mail = notifications.NotificationKind.mail.value
33
+ webhook = notifications.NotificationKind.webhook.value
33
34
 
34
- def get_notification(self) -> type[NotificationBase]:
35
+ def get_notification(self) -> type[base.NotificationBase]:
35
36
  return {
36
- self.console: ConsoleNotification,
37
- self.git: GitNotification,
38
- self.ipython: IPythonNotification,
39
- self.slack: SlackNotification,
40
- self.webhook: WebhookNotification,
37
+ self.console: console.ConsoleNotification,
38
+ self.git: git.GitNotification,
39
+ self.ipython: ipython.IPythonNotification,
40
+ self.slack: slack.SlackNotification,
41
+ self.mail: mail.MailNotification,
42
+ self.webhook: webhook.WebhookNotification,
41
43
  }.get(self)
42
44
 
43
45
  def inverse_dependencies(self) -> list[str]:
@@ -64,5 +66,6 @@ class NotificationTypes(str, enum.Enum):
64
66
  cls.git,
65
67
  cls.ipython,
66
68
  cls.slack,
69
+ cls.mail,
67
70
  cls.webhook,
68
71
  ]
@@ -14,6 +14,7 @@
14
14
 
15
15
  import asyncio
16
16
  import typing
17
+ from copy import deepcopy
17
18
 
18
19
  import mlrun.common.schemas
19
20
  import mlrun.lists
@@ -22,11 +23,20 @@ import mlrun.lists
22
23
  class NotificationBase:
23
24
  def __init__(
24
25
  self,
25
- name: str = None,
26
- params: dict[str, str] = None,
26
+ name: typing.Optional[str] = None,
27
+ params: typing.Optional[dict[str, str]] = None,
28
+ default_params: typing.Optional[dict[str, str]] = None,
27
29
  ):
30
+ """
31
+ NotificationBase is the base class for all notification types.
32
+
33
+ :param name: The name of the notification.
34
+ :param params: The parameters of the notification.
35
+ :param default_params: The default parameters of the notification. Used for server-side enrichment purposes.
36
+ """
28
37
  self.name = name
29
38
  self.params = params or {}
39
+ self.params = self.enrich_default_params(self.params, default_params)
30
40
 
31
41
  @classmethod
32
42
  def validate_params(cls, params):
@@ -43,13 +53,13 @@ class NotificationBase:
43
53
  def push(
44
54
  self,
45
55
  message: str,
46
- severity: typing.Union[
47
- mlrun.common.schemas.NotificationSeverity, str
56
+ severity: typing.Optional[
57
+ typing.Union[mlrun.common.schemas.NotificationSeverity, str]
48
58
  ] = mlrun.common.schemas.NotificationSeverity.INFO,
49
- runs: typing.Union[mlrun.lists.RunList, list] = None,
50
- custom_html: str = None,
51
- alert: mlrun.common.schemas.AlertConfig = None,
52
- event_data: mlrun.common.schemas.Event = None,
59
+ runs: typing.Optional[typing.Union[mlrun.lists.RunList, list]] = None,
60
+ custom_html: typing.Optional[typing.Optional[str]] = None,
61
+ alert: typing.Optional[mlrun.common.schemas.AlertConfig] = None,
62
+ event_data: typing.Optional[mlrun.common.schemas.Event] = None,
53
63
  ):
54
64
  raise NotImplementedError()
55
65
 
@@ -59,16 +69,25 @@ class NotificationBase:
59
69
  ) -> None:
60
70
  self.params = params or {}
61
71
 
72
+ @classmethod
73
+ def enrich_default_params(
74
+ cls, params: dict, default_params: typing.Optional[dict] = None
75
+ ) -> dict:
76
+ default_params = default_params or {}
77
+ returned_params = deepcopy(default_params)
78
+ returned_params.update(params)
79
+ return returned_params
80
+
62
81
  def _get_html(
63
82
  self,
64
83
  message: str,
65
- severity: typing.Union[
66
- mlrun.common.schemas.NotificationSeverity, str
84
+ severity: typing.Optional[
85
+ typing.Union[mlrun.common.schemas.NotificationSeverity, str]
67
86
  ] = mlrun.common.schemas.NotificationSeverity.INFO,
68
- runs: typing.Union[mlrun.lists.RunList, list] = None,
69
- custom_html: str = None,
70
- alert: mlrun.common.schemas.AlertConfig = None,
71
- event_data: mlrun.common.schemas.Event = None,
87
+ runs: typing.Optional[typing.Union[mlrun.lists.RunList, list]] = None,
88
+ custom_html: typing.Optional[typing.Optional[str]] = None,
89
+ alert: typing.Optional[mlrun.common.schemas.AlertConfig] = None,
90
+ event_data: typing.Optional[mlrun.common.schemas.Event] = None,
72
91
  ) -> str:
73
92
  if custom_html:
74
93
  return custom_html
@@ -31,13 +31,13 @@ class ConsoleNotification(NotificationBase):
31
31
  def push(
32
32
  self,
33
33
  message: str,
34
- severity: typing.Union[
35
- mlrun.common.schemas.NotificationSeverity, str
34
+ severity: typing.Optional[
35
+ typing.Union[mlrun.common.schemas.NotificationSeverity, str]
36
36
  ] = mlrun.common.schemas.NotificationSeverity.INFO,
37
- runs: typing.Union[mlrun.lists.RunList, list] = None,
38
- custom_html: str = None,
39
- alert: mlrun.common.schemas.AlertConfig = None,
40
- event_data: mlrun.common.schemas.Event = None,
37
+ runs: typing.Optional[typing.Union[mlrun.lists.RunList, list]] = None,
38
+ custom_html: typing.Optional[typing.Optional[str]] = None,
39
+ alert: typing.Optional[mlrun.common.schemas.AlertConfig] = None,
40
+ event_data: typing.Optional[mlrun.common.schemas.Event] = None,
41
41
  ):
42
42
  severity = self._resolve_severity(severity)
43
43
  print(f"[{severity}] {message}")
@@ -54,13 +54,13 @@ class GitNotification(NotificationBase):
54
54
  async def push(
55
55
  self,
56
56
  message: str,
57
- severity: typing.Union[
58
- mlrun.common.schemas.NotificationSeverity, str
57
+ severity: typing.Optional[
58
+ typing.Union[mlrun.common.schemas.NotificationSeverity, str]
59
59
  ] = mlrun.common.schemas.NotificationSeverity.INFO,
60
- runs: typing.Union[mlrun.lists.RunList, list] = None,
61
- custom_html: str = None,
62
- alert: mlrun.common.schemas.AlertConfig = None,
63
- event_data: mlrun.common.schemas.Event = None,
60
+ runs: typing.Optional[typing.Union[mlrun.lists.RunList, list]] = None,
61
+ custom_html: typing.Optional[typing.Optional[str]] = None,
62
+ alert: typing.Optional[mlrun.common.schemas.AlertConfig] = None,
63
+ event_data: typing.Optional[mlrun.common.schemas.Event] = None,
64
64
  ):
65
65
  git_repo = self.params.get("repo", None)
66
66
  git_issue = self.params.get("issue", None)
@@ -85,11 +85,11 @@ class GitNotification(NotificationBase):
85
85
  @staticmethod
86
86
  async def _pr_comment(
87
87
  message: str,
88
- repo: str = None,
89
- issue: int = None,
90
- merge_request: int = None,
91
- token: str = None,
92
- server: str = None,
88
+ repo: typing.Optional[str] = None,
89
+ issue: typing.Optional[int] = None,
90
+ merge_request: typing.Optional[int] = None,
91
+ token: typing.Optional[str] = None,
92
+ server: typing.Optional[str] = None,
93
93
  gitlab: bool = False,
94
94
  ) -> str:
95
95
  """push comment message to Git system PR/issue
@@ -28,10 +28,11 @@ class IPythonNotification(NotificationBase):
28
28
 
29
29
  def __init__(
30
30
  self,
31
- name: str = None,
32
- params: dict[str, str] = None,
31
+ name: typing.Optional[str] = None,
32
+ params: typing.Optional[dict[str, str]] = None,
33
+ default_params: typing.Optional[dict[str, str]] = None,
33
34
  ):
34
- super().__init__(name, params)
35
+ super().__init__(name, params, default_params)
35
36
  self._ipython = None
36
37
  try:
37
38
  import IPython
@@ -48,13 +49,13 @@ class IPythonNotification(NotificationBase):
48
49
  def push(
49
50
  self,
50
51
  message: str,
51
- severity: typing.Union[
52
- mlrun.common.schemas.NotificationSeverity, str
52
+ severity: typing.Optional[
53
+ typing.Union[mlrun.common.schemas.NotificationSeverity, str]
53
54
  ] = mlrun.common.schemas.NotificationSeverity.INFO,
54
- runs: typing.Union[mlrun.lists.RunList, list] = None,
55
- custom_html: str = None,
56
- alert: mlrun.common.schemas.AlertConfig = None,
57
- event_data: mlrun.common.schemas.Event = None,
55
+ runs: typing.Optional[typing.Union[mlrun.lists.RunList, list]] = None,
56
+ custom_html: typing.Optional[typing.Optional[str]] = None,
57
+ alert: typing.Optional[mlrun.common.schemas.AlertConfig] = None,
58
+ event_data: typing.Optional[mlrun.common.schemas.Event] = None,
58
59
  ):
59
60
  if not self._ipython:
60
61
  mlrun.utils.helpers.logger.debug(