mlrun 1.7.0rc4__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 (235) 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 -1
  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 +31 -4
  25. mlrun/common/schemas/alert.py +202 -0
  26. mlrun/common/schemas/api_gateway.py +196 -0
  27. mlrun/common/schemas/artifact.py +28 -1
  28. mlrun/common/schemas/auth.py +13 -2
  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 +233 -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 +387 -119
  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 +245 -20
  75. mlrun/db/factory.py +1 -4
  76. mlrun/db/httpdb.py +909 -231
  77. mlrun/db/nopdb.py +279 -14
  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 +1176 -406
  164. mlrun/render.py +28 -22
  165. mlrun/run.py +208 -181
  166. mlrun/runtimes/__init__.py +76 -11
  167. mlrun/runtimes/base.py +54 -24
  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/__init__.py +1 -0
  178. mlrun/runtimes/nuclio/api_gateway.py +769 -0
  179. mlrun/runtimes/nuclio/application/__init__.py +15 -0
  180. mlrun/runtimes/nuclio/application/application.py +758 -0
  181. mlrun/runtimes/nuclio/application/reverse_proxy.go +95 -0
  182. mlrun/runtimes/nuclio/function.py +188 -68
  183. mlrun/runtimes/nuclio/serving.py +57 -60
  184. mlrun/runtimes/pod.py +191 -58
  185. mlrun/runtimes/remotesparkjob.py +11 -8
  186. mlrun/runtimes/sparkjob/spark3job.py +17 -18
  187. mlrun/runtimes/utils.py +40 -73
  188. mlrun/secrets.py +6 -2
  189. mlrun/serving/__init__.py +8 -1
  190. mlrun/serving/remote.py +2 -3
  191. mlrun/serving/routers.py +89 -64
  192. mlrun/serving/server.py +54 -26
  193. mlrun/serving/states.py +187 -56
  194. mlrun/serving/utils.py +19 -11
  195. mlrun/serving/v2_serving.py +136 -63
  196. mlrun/track/tracker.py +2 -1
  197. mlrun/track/trackers/mlflow_tracker.py +5 -0
  198. mlrun/utils/async_http.py +26 -6
  199. mlrun/utils/db.py +18 -0
  200. mlrun/utils/helpers.py +375 -105
  201. mlrun/utils/http.py +2 -2
  202. mlrun/utils/logger.py +75 -9
  203. mlrun/utils/notifications/notification/__init__.py +14 -10
  204. mlrun/utils/notifications/notification/base.py +48 -0
  205. mlrun/utils/notifications/notification/console.py +2 -0
  206. mlrun/utils/notifications/notification/git.py +24 -1
  207. mlrun/utils/notifications/notification/ipython.py +2 -0
  208. mlrun/utils/notifications/notification/slack.py +96 -21
  209. mlrun/utils/notifications/notification/webhook.py +63 -2
  210. mlrun/utils/notifications/notification_pusher.py +146 -16
  211. mlrun/utils/regex.py +9 -0
  212. mlrun/utils/retryer.py +3 -2
  213. mlrun/utils/v3io_clients.py +2 -3
  214. mlrun/utils/version/version.json +2 -2
  215. mlrun-1.7.2.dist-info/METADATA +390 -0
  216. mlrun-1.7.2.dist-info/RECORD +351 -0
  217. {mlrun-1.7.0rc4.dist-info → mlrun-1.7.2.dist-info}/WHEEL +1 -1
  218. mlrun/feature_store/retrieval/conversion.py +0 -271
  219. mlrun/kfpops.py +0 -868
  220. mlrun/model_monitoring/application.py +0 -310
  221. mlrun/model_monitoring/batch.py +0 -974
  222. mlrun/model_monitoring/controller_handler.py +0 -37
  223. mlrun/model_monitoring/prometheus.py +0 -216
  224. mlrun/model_monitoring/stores/__init__.py +0 -111
  225. mlrun/model_monitoring/stores/kv_model_endpoint_store.py +0 -574
  226. mlrun/model_monitoring/stores/model_endpoint_store.py +0 -145
  227. mlrun/model_monitoring/stores/models/__init__.py +0 -27
  228. mlrun/model_monitoring/stores/models/base.py +0 -84
  229. mlrun/model_monitoring/stores/sql_model_endpoint_store.py +0 -382
  230. mlrun/platforms/other.py +0 -305
  231. mlrun-1.7.0rc4.dist-info/METADATA +0 -269
  232. mlrun-1.7.0rc4.dist-info/RECORD +0 -321
  233. {mlrun-1.7.0rc4.dist-info → mlrun-1.7.2.dist-info}/LICENSE +0 -0
  234. {mlrun-1.7.0rc4.dist-info → mlrun-1.7.2.dist-info}/entry_points.txt +0 -0
  235. {mlrun-1.7.0rc4.dist-info → mlrun-1.7.2.dist-info}/top_level.txt +0 -0
mlrun/utils/helpers.py CHANGED
@@ -24,9 +24,10 @@ import re
24
24
  import string
25
25
  import sys
26
26
  import typing
27
+ import uuid
27
28
  import warnings
28
29
  from datetime import datetime, timezone
29
- from importlib import import_module
30
+ from importlib import import_module, reload
30
31
  from os import path
31
32
  from types import ModuleType
32
33
  from typing import Any, Optional
@@ -39,8 +40,8 @@ import pandas
39
40
  import semver
40
41
  import yaml
41
42
  from dateutil import parser
42
- from deprecated import deprecated
43
- from pandas._libs.tslibs.timestamps import Timedelta, Timestamp
43
+ from mlrun_pipelines.models import PipelineRun
44
+ from pandas import Timedelta, Timestamp
44
45
  from yaml.representer import RepresenterError
45
46
 
46
47
  import mlrun
@@ -76,19 +77,6 @@ class OverwriteBuildParamsWarning(FutureWarning):
76
77
  pass
77
78
 
78
79
 
79
- # TODO: remove in 1.7.0
80
- @deprecated(
81
- version="1.5.0",
82
- reason="'parse_versioned_object_uri' will be removed from this file in 1.7.0, use "
83
- "'mlrun.common.helpers.parse_versioned_object_uri' instead",
84
- category=FutureWarning,
85
- )
86
- def parse_versioned_object_uri(uri: str, default_project: str = ""):
87
- return mlrun.common.helpers.parse_versioned_object_uri(
88
- uri=uri, default_project=default_project
89
- )
90
-
91
-
92
80
  class StorePrefix:
93
81
  """map mlrun store objects to prefixes"""
94
82
 
@@ -119,51 +107,61 @@ class StorePrefix:
119
107
 
120
108
 
121
109
  def get_artifact_target(item: dict, project=None):
122
- if is_legacy_artifact(item):
123
- db_key = item.get("db_key")
124
- project_str = project or item.get("project")
125
- tree = item.get("tree")
126
- else:
127
- db_key = item["spec"].get("db_key")
128
- project_str = project or item["metadata"].get("project")
129
- tree = item["metadata"].get("tree")
130
-
110
+ db_key = item["spec"].get("db_key")
111
+ project_str = project or item["metadata"].get("project")
112
+ tree = item["metadata"].get("tree")
113
+ tag = item["metadata"].get("tag")
131
114
  kind = item.get("kind")
132
- if kind in ["dataset", "model", "artifact"] and db_key:
133
- target = f"{DB_SCHEMA}://{StorePrefix.Artifact}/{project_str}/{db_key}"
115
+
116
+ if kind in {"dataset", "model", "artifact"} and db_key:
117
+ target = (
118
+ f"{DB_SCHEMA}://{StorePrefix.kind_to_prefix(kind)}/{project_str}/{db_key}"
119
+ )
120
+ target += f":{tag}" if tag else ":latest"
134
121
  if tree:
135
- target = f"{target}@{tree}"
122
+ target += f"@{tree}"
136
123
  return target
137
124
 
138
- return (
139
- item.get("target_path")
140
- if is_legacy_artifact(item)
141
- else item["spec"].get("target_path")
142
- )
125
+ return item["spec"].get("target_path")
126
+
127
+
128
+ # TODO: left for migrations testing purposes. Remove in 1.8.0.
129
+ def is_legacy_artifact(artifact):
130
+ if isinstance(artifact, dict):
131
+ return "metadata" not in artifact
132
+ else:
133
+ return not hasattr(artifact, "metadata")
143
134
 
144
135
 
145
136
  logger = create_logger(config.log_level, config.log_formatter, "mlrun", sys.stdout)
146
137
  missing = object()
147
138
 
148
- is_ipython = False
139
+ is_ipython = False # is IPython terminal, including Jupyter
140
+ is_jupyter = False # is Jupyter notebook/lab terminal
149
141
  try:
150
- import IPython
142
+ import IPython.core.getipython
151
143
 
152
- ipy = IPython.get_ipython()
153
- # if its IPython terminal ignore (cant show html)
154
- if ipy and "Terminal" not in str(type(ipy)):
155
- is_ipython = True
156
- except ImportError:
144
+ ipy = IPython.core.getipython.get_ipython()
145
+
146
+ is_ipython = ipy is not None
147
+ is_jupyter = (
148
+ is_ipython
149
+ # not IPython
150
+ and "Terminal" not in str(type(ipy))
151
+ )
152
+
153
+ del ipy
154
+ except ModuleNotFoundError:
157
155
  pass
158
156
 
159
- if is_ipython and config.nest_asyncio_enabled in ["1", "True"]:
157
+ if is_jupyter and config.nest_asyncio_enabled in ["1", "True"]:
160
158
  # bypass Jupyter asyncio bug
161
159
  import nest_asyncio
162
160
 
163
161
  nest_asyncio.apply()
164
162
 
165
163
 
166
- class run_keys:
164
+ class RunKeys:
167
165
  input_path = "input_path"
168
166
  output_path = "output_path"
169
167
  inputs = "inputs"
@@ -174,6 +172,10 @@ class run_keys:
174
172
  secrets = "secret_sources"
175
173
 
176
174
 
175
+ # for Backward compatibility
176
+ run_keys = RunKeys
177
+
178
+
177
179
  def verify_field_regex(
178
180
  field_name,
179
181
  field_value,
@@ -195,8 +197,12 @@ def verify_field_regex(
195
197
  )
196
198
  if mode == mlrun.common.schemas.RegexMatchModes.all:
197
199
  if raise_on_failure:
200
+ if len(field_name) > max_chars:
201
+ field_name = field_name[:max_chars] + "...truncated"
202
+ if len(field_value) > max_chars:
203
+ field_value = field_value[:max_chars] + "...truncated"
198
204
  raise mlrun.errors.MLRunInvalidArgumentError(
199
- f"Field '{field_name[:max_chars]}' is malformed. '{field_value[:max_chars]}' "
205
+ f"Field '{field_name}' is malformed. '{field_value}' "
200
206
  f"does not match required pattern: {pattern}"
201
207
  )
202
208
  return False
@@ -437,7 +443,7 @@ class LogBatchWriter:
437
443
 
438
444
  def get_in(obj, keys, default=None):
439
445
  """
440
- >>> get_in({'a': {'b': 1}}, 'a.b')
446
+ >>> get_in({"a": {"b": 1}}, "a.b")
441
447
  1
442
448
  """
443
449
  if isinstance(keys, str):
@@ -669,7 +675,7 @@ def parse_artifact_uri(uri, default_project=""):
669
675
  [3] = tag
670
676
  [4] = tree
671
677
  """
672
- uri_pattern = r"^((?P<project>.*)/)?(?P<key>.*?)(\#(?P<iteration>.*?))?(:(?P<tag>.*?))?(@(?P<tree>.*))?$"
678
+ uri_pattern = mlrun.utils.regex.artifact_uri_pattern
673
679
  match = re.match(uri_pattern, uri)
674
680
  if not match:
675
681
  raise ValueError(
@@ -684,6 +690,8 @@ def parse_artifact_uri(uri, default_project=""):
684
690
  raise ValueError(
685
691
  f"illegal store path '{uri}', iteration must be integer value"
686
692
  )
693
+ else:
694
+ iteration = 0
687
695
  return (
688
696
  group_dict["project"] or default_project,
689
697
  group_dict["key"],
@@ -801,34 +809,6 @@ def gen_html_table(header, rows=None):
801
809
  return style + '<table class="tg">\n' + out + "</table>\n\n"
802
810
 
803
811
 
804
- def new_pipe_metadata(
805
- artifact_path: str = None,
806
- cleanup_ttl: int = None,
807
- op_transformers: list[typing.Callable] = None,
808
- ):
809
- from kfp.dsl import PipelineConf
810
-
811
- def _set_artifact_path(task):
812
- from kubernetes import client as k8s_client
813
-
814
- task.add_env_variable(
815
- k8s_client.V1EnvVar(name="MLRUN_ARTIFACT_PATH", value=artifact_path)
816
- )
817
- return task
818
-
819
- conf = PipelineConf()
820
- cleanup_ttl = cleanup_ttl or int(config.kfp_ttl)
821
-
822
- if cleanup_ttl:
823
- conf.set_ttl_seconds_after_finished(cleanup_ttl)
824
- if artifact_path:
825
- conf.add_op_transformer(_set_artifact_path)
826
- if op_transformers:
827
- for op_transformer in op_transformers:
828
- conf.add_op_transformer(op_transformer)
829
- return conf
830
-
831
-
832
812
  def _convert_python_package_version_to_image_tag(version: typing.Optional[str]):
833
813
  return (
834
814
  version.replace("+", "-").replace("0.0.0-", "") if version is not None else None
@@ -848,7 +828,6 @@ def enrich_image_url(
848
828
  tag += resolve_image_tag_suffix(
849
829
  mlrun_version=mlrun_version, python_version=client_python_version
850
830
  )
851
- registry = config.images_registry
852
831
 
853
832
  # it's an mlrun image if the repository is mlrun
854
833
  is_mlrun_image = image_url.startswith("mlrun/") or "/mlrun/" in image_url
@@ -856,6 +835,10 @@ def enrich_image_url(
856
835
  if is_mlrun_image and tag and ":" not in image_url:
857
836
  image_url = f"{image_url}:{tag}"
858
837
 
838
+ registry = (
839
+ config.images_registry if is_mlrun_image else config.vendor_images_registry
840
+ )
841
+
859
842
  enrich_registry = False
860
843
  # enrich registry only if images_to_enrich_registry provided
861
844
  # example: "^mlrun/*" means enrich only if the image repository is mlrun and registry is not specified (in which
@@ -1015,17 +998,44 @@ def get_ui_url(project, uid=None):
1015
998
  return url
1016
999
 
1017
1000
 
1001
+ def get_model_endpoint_url(project, model_name, model_endpoint_id):
1002
+ url = ""
1003
+ if mlrun.mlconf.resolve_ui_url():
1004
+ url = f"{mlrun.mlconf.resolve_ui_url()}/{mlrun.mlconf.ui.projects_prefix}/{project}/models"
1005
+ if model_name:
1006
+ url += f"/model-endpoints/{model_name}/{model_endpoint_id}/overview"
1007
+ return url
1008
+
1009
+
1018
1010
  def get_workflow_url(project, id=None):
1019
1011
  url = ""
1020
1012
  if mlrun.mlconf.resolve_ui_url():
1021
- url = "{}/{}/{}/jobs/monitor-workflows/workflow/{}".format(
1022
- mlrun.mlconf.resolve_ui_url(), mlrun.mlconf.ui.projects_prefix, project, id
1013
+ url = (
1014
+ f"{mlrun.mlconf.resolve_ui_url()}/{mlrun.mlconf.ui.projects_prefix}"
1015
+ f"/{project}/jobs/monitor-workflows/workflow/{id}"
1023
1016
  )
1024
1017
  return url
1025
1018
 
1026
1019
 
1020
+ def get_kfp_project_filter(project_name: str) -> str:
1021
+ """
1022
+ Generates a filter string for KFP runs, using a substring predicate
1023
+ on the run's 'name' field. This is used as a heuristic to retrieve runs that are associated
1024
+ with a specific project. The 'op: 9' operator indicates that the filter checks if the
1025
+ project name appears as a substring in the run's name, ensuring that we can identify
1026
+ runs belonging to the desired project.
1027
+ """
1028
+ is_substring_op = 9
1029
+ project_name_filter = {
1030
+ "predicates": [
1031
+ {"key": "name", "op": is_substring_op, "string_value": project_name}
1032
+ ]
1033
+ }
1034
+ return json.dumps(project_name_filter)
1035
+
1036
+
1027
1037
  def are_strings_in_exception_chain_messages(
1028
- exception: Exception, strings_list=list[str]
1038
+ exception: Exception, strings_list: list[str]
1029
1039
  ) -> bool:
1030
1040
  while exception is not None:
1031
1041
  if any([string in str(exception) for string in strings_list]):
@@ -1047,16 +1057,35 @@ def create_class(pkg_class: str):
1047
1057
  return class_
1048
1058
 
1049
1059
 
1050
- def create_function(pkg_func: str):
1060
+ def create_function(pkg_func: str, reload_modules: bool = False):
1051
1061
  """Create a function from a package.module.function string
1052
1062
 
1053
1063
  :param pkg_func: full function location,
1054
1064
  e.g. "sklearn.feature_selection.f_classif"
1065
+ :param reload_modules: reload the function again.
1055
1066
  """
1056
1067
  splits = pkg_func.split(".")
1057
1068
  pkg_module = ".".join(splits[:-1])
1058
1069
  cb_fname = splits[-1]
1059
1070
  pkg_module = __import__(pkg_module, fromlist=[cb_fname])
1071
+
1072
+ if reload_modules:
1073
+ # Even though the function appears in the modules list, we need to reload
1074
+ # the code again because it may have changed
1075
+ try:
1076
+ logger.debug("Reloading module", module=pkg_func)
1077
+ _reload(
1078
+ pkg_module,
1079
+ max_recursion_depth=mlrun.mlconf.function.spec.reload_max_recursion_depth,
1080
+ )
1081
+ except Exception as exc:
1082
+ logger.warning(
1083
+ "Failed to reload module. Not all associated modules can be reloaded, import them manually."
1084
+ "Or, with Jupyter, restart the Python kernel.",
1085
+ module=pkg_func,
1086
+ err=mlrun.errors.err_to_str(exc),
1087
+ )
1088
+
1060
1089
  function_ = getattr(pkg_module, cb_fname)
1061
1090
  return function_
1062
1091
 
@@ -1114,8 +1143,14 @@ def get_class(class_name, namespace=None):
1114
1143
  return class_object
1115
1144
 
1116
1145
 
1117
- def get_function(function, namespace):
1118
- """return function callable object from function name string"""
1146
+ def get_function(function, namespaces, reload_modules: bool = False):
1147
+ """Return function callable object from function name string
1148
+
1149
+ :param function: path to the function ([class_name::]function)
1150
+ :param namespaces: one or list of namespaces/modules to search the function in
1151
+ :param reload_modules: reload the function again
1152
+ :return: function handler (callable)
1153
+ """
1119
1154
  if callable(function):
1120
1155
  return function
1121
1156
 
@@ -1124,12 +1159,12 @@ def get_function(function, namespace):
1124
1159
  if not function.endswith(")"):
1125
1160
  raise ValueError('function expression must start with "(" and end with ")"')
1126
1161
  return eval("lambda event: " + function[1:-1], {}, {})
1127
- function_object = _search_in_namespaces(function, namespace)
1162
+ function_object = _search_in_namespaces(function, namespaces)
1128
1163
  if function_object is not None:
1129
1164
  return function_object
1130
1165
 
1131
1166
  try:
1132
- function_object = create_function(function)
1167
+ function_object = create_function(function, reload_modules)
1133
1168
  except (ImportError, ValueError) as exc:
1134
1169
  raise ImportError(
1135
1170
  f"state/function init failed, handler '{function}' not found"
@@ -1138,18 +1173,24 @@ def get_function(function, namespace):
1138
1173
 
1139
1174
 
1140
1175
  def get_handler_extended(
1141
- handler_path: str, context=None, class_args: dict = {}, namespaces=None
1176
+ handler_path: str,
1177
+ context=None,
1178
+ class_args: dict = None,
1179
+ namespaces=None,
1180
+ reload_modules: bool = False,
1142
1181
  ):
1143
- """get function handler from [class_name::]handler string
1182
+ """Get function handler from [class_name::]handler string
1144
1183
 
1145
1184
  :param handler_path: path to the function ([class_name::]handler)
1146
1185
  :param context: MLRun function/job client context
1147
1186
  :param class_args: optional dict of class init kwargs
1148
1187
  :param namespaces: one or list of namespaces/modules to search the handler in
1188
+ :param reload_modules: reload the function again
1149
1189
  :return: function handler (callable)
1150
1190
  """
1191
+ class_args = class_args or {}
1151
1192
  if "::" not in handler_path:
1152
- return get_function(handler_path, namespaces)
1193
+ return get_function(handler_path, namespaces, reload_modules)
1153
1194
 
1154
1195
  splitted = handler_path.split("::")
1155
1196
  class_path = splitted[0].strip()
@@ -1185,14 +1226,24 @@ def datetime_to_iso(time_obj: Optional[datetime]) -> Optional[str]:
1185
1226
  return time_obj.isoformat()
1186
1227
 
1187
1228
 
1188
- def enrich_datetime_with_tz_info(timestamp_string):
1229
+ def enrich_datetime_with_tz_info(timestamp_string) -> Optional[datetime]:
1189
1230
  if not timestamp_string:
1190
1231
  return timestamp_string
1191
1232
 
1192
1233
  if timestamp_string and not mlrun.utils.helpers.has_timezone(timestamp_string):
1193
1234
  timestamp_string += datetime.now(timezone.utc).astimezone().strftime("%z")
1194
1235
 
1195
- return datetime.strptime(timestamp_string, "%Y-%m-%d %H:%M:%S.%f%z")
1236
+ for _format in [
1237
+ # e.g: 2021-08-25 12:00:00.000Z
1238
+ "%Y-%m-%d %H:%M:%S.%f%z",
1239
+ # e.g: 2024-11-11 07:44:56+0000
1240
+ "%Y-%m-%d %H:%M:%S%z",
1241
+ ]:
1242
+ try:
1243
+ return datetime.strptime(timestamp_string, _format)
1244
+ except ValueError as exc:
1245
+ last_exc = exc
1246
+ raise last_exc
1196
1247
 
1197
1248
 
1198
1249
  def has_timezone(timestamp):
@@ -1224,7 +1275,7 @@ def calculate_dataframe_hash(dataframe: pandas.DataFrame):
1224
1275
  return hashlib.sha1(pandas.util.hash_pandas_object(dataframe).values).hexdigest()
1225
1276
 
1226
1277
 
1227
- def template_artifact_path(artifact_path, project, run_uid="project"):
1278
+ def template_artifact_path(artifact_path, project, run_uid=None):
1228
1279
  """
1229
1280
  Replace {{run.uid}} with the run uid and {{project}} with the project name in the artifact path.
1230
1281
  If no run uid is provided, the word `project` will be used instead as it is assumed to be a project
@@ -1232,6 +1283,7 @@ def template_artifact_path(artifact_path, project, run_uid="project"):
1232
1283
  """
1233
1284
  if not artifact_path:
1234
1285
  return artifact_path
1286
+ run_uid = run_uid or "project"
1235
1287
  artifact_path = artifact_path.replace("{{run.uid}}", run_uid)
1236
1288
  artifact_path = _fill_project_path_template(artifact_path, project)
1237
1289
  return artifact_path
@@ -1253,6 +1305,10 @@ def _fill_project_path_template(artifact_path, project):
1253
1305
  return artifact_path
1254
1306
 
1255
1307
 
1308
+ def to_non_empty_values_dict(input_dict: dict) -> dict:
1309
+ return {key: value for key, value in input_dict.items() if value}
1310
+
1311
+
1256
1312
  def str_to_timestamp(time_str: str, now_time: Timestamp = None):
1257
1313
  """convert fixed/relative time string to Pandas Timestamp
1258
1314
 
@@ -1291,13 +1347,6 @@ def str_to_timestamp(time_str: str, now_time: Timestamp = None):
1291
1347
  return Timestamp(time_str)
1292
1348
 
1293
1349
 
1294
- def is_legacy_artifact(artifact):
1295
- if isinstance(artifact, dict):
1296
- return "metadata" not in artifact
1297
- else:
1298
- return not hasattr(artifact, "metadata")
1299
-
1300
-
1301
1350
  def is_link_artifact(artifact):
1302
1351
  if isinstance(artifact, dict):
1303
1352
  return (
@@ -1307,7 +1356,7 @@ def is_link_artifact(artifact):
1307
1356
  return artifact.kind == mlrun.common.schemas.ArtifactCategories.link.value
1308
1357
 
1309
1358
 
1310
- def format_run(run: dict, with_project=False) -> dict:
1359
+ def format_run(run: PipelineRun, with_project=False) -> dict:
1311
1360
  fields = [
1312
1361
  "id",
1313
1362
  "name",
@@ -1317,6 +1366,7 @@ def format_run(run: dict, with_project=False) -> dict:
1317
1366
  "scheduled_at",
1318
1367
  "finished_at",
1319
1368
  "description",
1369
+ "experiment_id",
1320
1370
  ]
1321
1371
 
1322
1372
  if with_project:
@@ -1344,17 +1394,17 @@ def format_run(run: dict, with_project=False) -> dict:
1344
1394
  # pipelines are yet to populate the status or workflow has failed
1345
1395
  # as observed https://jira.iguazeng.com/browse/ML-5195
1346
1396
  # set to unknown to ensure a status is returned
1347
- if run["status"] is None:
1348
- run["status"] = inflection.titleize(mlrun.runtimes.constants.RunStates.unknown)
1397
+ if run.get("status", None) is None:
1398
+ run["status"] = inflection.titleize(
1399
+ mlrun.common.runtimes.constants.RunStates.unknown
1400
+ )
1349
1401
 
1350
1402
  return run
1351
1403
 
1352
1404
 
1353
1405
  def get_in_artifact(artifact: dict, key, default=None, raise_on_missing=False):
1354
1406
  """artifact can be dict or Artifact object"""
1355
- if is_legacy_artifact(artifact):
1356
- return artifact.get(key, default)
1357
- elif key == "kind":
1407
+ if key == "kind":
1358
1408
  return artifact.get(key, default)
1359
1409
  else:
1360
1410
  for block in ["metadata", "spec", "status"]:
@@ -1391,11 +1441,27 @@ def is_running_in_jupyter_notebook() -> bool:
1391
1441
  Check if the code is running inside a Jupyter Notebook.
1392
1442
  :return: True if running inside a Jupyter Notebook, False otherwise.
1393
1443
  """
1394
- import IPython
1444
+ return is_jupyter
1445
+
1446
+
1447
+ def create_ipython_display():
1448
+ """
1449
+ Create an IPython display object and fill it with initial content.
1450
+ We can later use the returned display_id with the update_display method to update the content.
1451
+ If IPython is not installed, a warning will be logged and None will be returned.
1452
+ """
1453
+ if is_ipython:
1454
+ import IPython
1455
+
1456
+ display_id = uuid.uuid4().hex
1457
+ content = IPython.display.HTML(
1458
+ f'<div id="{display_id}">Temporary Display Content</div>'
1459
+ )
1460
+ IPython.display.display(content, display_id=display_id)
1461
+ return display_id
1395
1462
 
1396
- ipy = IPython.get_ipython()
1397
- # if its IPython terminal, it isn't a Jupyter ipython
1398
- return ipy and "Terminal" not in str(type(ipy))
1463
+ # returning None if IPython is not installed, this method shouldn't be called in that case but logging for sanity
1464
+ logger.debug("IPython is not installed, cannot create IPython display")
1399
1465
 
1400
1466
 
1401
1467
  def as_number(field_name, field_value):
@@ -1405,6 +1471,18 @@ def as_number(field_name, field_value):
1405
1471
 
1406
1472
 
1407
1473
  def filter_warnings(action, category):
1474
+ """
1475
+ Decorator to filter warnings
1476
+
1477
+ Example::
1478
+ @filter_warnings("ignore", FutureWarning)
1479
+ def my_function():
1480
+ pass
1481
+
1482
+ :param action: one of "error", "ignore", "always", "default", "module", or "once"
1483
+ :param category: a class that the warning must be a subclass of
1484
+ """
1485
+
1408
1486
  def decorator(function):
1409
1487
  def wrapper(*args, **kwargs):
1410
1488
  # context manager that copies and, upon exit, restores the warnings filter and the showwarning() function.
@@ -1562,3 +1640,195 @@ def is_safe_path(base, filepath, is_symlink=False):
1562
1640
  os.path.abspath(filepath) if not is_symlink else os.path.realpath(filepath)
1563
1641
  )
1564
1642
  return base == os.path.commonpath((base, resolved_filepath))
1643
+
1644
+
1645
+ def get_serving_spec():
1646
+ data = None
1647
+
1648
+ # we will have the serving spec in either mounted config map
1649
+ # or env depending on the size of the spec and configuration
1650
+
1651
+ try:
1652
+ with open(mlrun.common.constants.MLRUN_SERVING_SPEC_PATH) as f:
1653
+ data = f.read()
1654
+ except FileNotFoundError:
1655
+ pass
1656
+
1657
+ if data is None:
1658
+ data = os.environ.get("SERVING_SPEC_ENV", "")
1659
+ if not data:
1660
+ raise mlrun.errors.MLRunInvalidArgumentError(
1661
+ "Failed to find serving spec in env var or config file"
1662
+ )
1663
+ spec = json.loads(data)
1664
+ return spec
1665
+
1666
+
1667
+ def additional_filters_warning(additional_filters, class_name):
1668
+ if additional_filters and any(additional_filters):
1669
+ mlrun.utils.logger.warn(
1670
+ f"additional_filters parameter is not supported in {class_name},"
1671
+ f" parameter has been ignored."
1672
+ )
1673
+
1674
+
1675
+ def merge_dicts_with_precedence(*dicts: dict) -> dict:
1676
+ """
1677
+ Merge multiple dictionaries with precedence given to keys from later dictionaries.
1678
+
1679
+ This function merges an arbitrary number of dictionaries, where keys from dictionaries later
1680
+ in the argument list take precedence over keys from dictionaries earlier in the list. If all
1681
+ dictionaries contain the same key, the value from the last dictionary with that key will
1682
+ overwrite the values from earlier dictionaries.
1683
+
1684
+ Example:
1685
+ >>> first_dict = {"key1": "value1", "key2": "value2"}
1686
+ >>> second_dict = {"key2": "new_value2", "key3": "value3"}
1687
+ >>> third_dict = {"key3": "new_value3", "key4": "value4"}
1688
+ >>> merge_dicts_with_precedence(first_dict, second_dict, third_dict)
1689
+ {'key1': 'value1', 'key2': 'new_value2', 'key3': 'new_value3', 'key4': 'value4'}
1690
+
1691
+ - If no dictionaries are provided, the function returns an empty dictionary.
1692
+ """
1693
+ return {k: v for d in dicts if d for k, v in d.items()}
1694
+
1695
+
1696
+ def validate_component_version_compatibility(
1697
+ component_name: typing.Literal["iguazio", "nuclio"], *min_versions: str
1698
+ ):
1699
+ """
1700
+ :param component_name: Name of the component to validate compatibility for.
1701
+ :param min_versions: Valid minimum version(s) required, assuming no 2 versions has equal major and minor.
1702
+ """
1703
+ parsed_min_versions = [
1704
+ semver.VersionInfo.parse(min_version) for min_version in min_versions
1705
+ ]
1706
+ parsed_current_version = None
1707
+ component_current_version = None
1708
+ try:
1709
+ if component_name == "iguazio":
1710
+ component_current_version = mlrun.mlconf.igz_version
1711
+ parsed_current_version = mlrun.mlconf.get_parsed_igz_version()
1712
+
1713
+ if parsed_current_version:
1714
+ # ignore pre-release and build metadata, as iguazio version always has them, and we only care about the
1715
+ # major, minor, and patch versions
1716
+ parsed_current_version = semver.VersionInfo.parse(
1717
+ f"{parsed_current_version.major}.{parsed_current_version.minor}.{parsed_current_version.patch}"
1718
+ )
1719
+ if component_name == "nuclio":
1720
+ component_current_version = mlrun.mlconf.nuclio_version
1721
+ parsed_current_version = semver.VersionInfo.parse(
1722
+ mlrun.mlconf.nuclio_version
1723
+ )
1724
+ if not parsed_current_version:
1725
+ return True
1726
+ except ValueError:
1727
+ # only log when version is set but invalid
1728
+ if component_current_version:
1729
+ logger.warning(
1730
+ "Unable to parse current version, assuming compatibility",
1731
+ component_name=component_name,
1732
+ current_version=component_current_version,
1733
+ min_versions=min_versions,
1734
+ )
1735
+ return True
1736
+
1737
+ # Feature might have been back-ported e.g. nuclio node selection is supported from
1738
+ # 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
1739
+ # then against 1.5.x
1740
+ parsed_min_versions.sort(reverse=True)
1741
+ for parsed_min_version in parsed_min_versions:
1742
+ if (
1743
+ parsed_current_version.major == parsed_min_version.major
1744
+ and parsed_current_version.minor == parsed_min_version.minor
1745
+ and parsed_current_version.patch < parsed_min_version.patch
1746
+ ):
1747
+ return False
1748
+
1749
+ if parsed_current_version >= parsed_min_version:
1750
+ return True
1751
+ return False
1752
+
1753
+
1754
+ def format_alert_summary(
1755
+ alert: mlrun.common.schemas.AlertConfig, event_data: mlrun.common.schemas.Event
1756
+ ) -> str:
1757
+ result = alert.summary.replace("{{project}}", alert.project)
1758
+ result = result.replace("{{name}}", alert.name)
1759
+ result = result.replace("{{entity}}", event_data.entity.ids[0])
1760
+ return result
1761
+
1762
+
1763
+ def is_parquet_file(file_path, format_=None):
1764
+ return (file_path and file_path.endswith((".parquet", ".pq"))) or (
1765
+ format_ == "parquet"
1766
+ )
1767
+
1768
+
1769
+ def validate_single_def_handler(function_kind: str, code: str):
1770
+ # The name of MLRun's wrapper is 'handler', which is why the handler function name cannot be 'handler'
1771
+ # it would override MLRun's wrapper
1772
+ if function_kind == "mlrun":
1773
+ # Find all lines that start with "def handler("
1774
+ pattern = re.compile(r"^def handler\(", re.MULTILINE)
1775
+ matches = pattern.findall(code)
1776
+
1777
+ # Only MLRun's wrapper handler (footer) can be in the code
1778
+ if len(matches) > 1:
1779
+ raise mlrun.errors.MLRunInvalidArgumentError(
1780
+ "The code file contains a function named “handler“, which is reserved. "
1781
+ + "Use a different name for your function."
1782
+ )
1783
+
1784
+
1785
+ def _reload(module, max_recursion_depth):
1786
+ """Recursively reload modules."""
1787
+ if max_recursion_depth <= 0:
1788
+ return
1789
+
1790
+ reload(module)
1791
+ for attribute_name in dir(module):
1792
+ attribute = getattr(module, attribute_name)
1793
+ if type(attribute) is ModuleType:
1794
+ _reload(attribute, max_recursion_depth - 1)
1795
+
1796
+
1797
+ def run_with_retry(
1798
+ retry_count: int,
1799
+ func: typing.Callable,
1800
+ retry_on_exceptions: typing.Union[
1801
+ type[Exception],
1802
+ tuple[type[Exception]],
1803
+ ] = None,
1804
+ *args,
1805
+ **kwargs,
1806
+ ):
1807
+ """
1808
+ Executes a function with retry logic upon encountering specified exceptions.
1809
+
1810
+ :param retry_count: The number of times to retry the function execution.
1811
+ :param func: The function to execute.
1812
+ :param retry_on_exceptions: Exception(s) that trigger a retry. Can be a single exception or a tuple of exceptions.
1813
+ :param args: Positional arguments to pass to the function.
1814
+ :param kwargs: Keyword arguments to pass to the function.
1815
+ :return: The result of the function execution if successful.
1816
+ :raises Exception: Re-raises the last exception encountered after all retries are exhausted.
1817
+ """
1818
+ if retry_on_exceptions is None:
1819
+ retry_on_exceptions = (Exception,)
1820
+ elif isinstance(retry_on_exceptions, list):
1821
+ retry_on_exceptions = tuple(retry_on_exceptions)
1822
+
1823
+ last_exception = None
1824
+ for attempt in range(retry_count + 1):
1825
+ try:
1826
+ return func(*args, **kwargs)
1827
+ except retry_on_exceptions as exc:
1828
+ last_exception = exc
1829
+ logger.warning(
1830
+ f"Attempt {{{attempt}/ {retry_count}}} failed with exception: {exc}",
1831
+ )
1832
+ if attempt == retry_count:
1833
+ raise
1834
+ raise last_exception