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

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

Potentially problematic release.


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

Files changed (275) hide show
  1. mlrun/__init__.py +26 -22
  2. mlrun/__main__.py +15 -16
  3. mlrun/alerts/alert.py +150 -15
  4. mlrun/api/schemas/__init__.py +1 -9
  5. mlrun/artifacts/__init__.py +2 -3
  6. mlrun/artifacts/base.py +62 -19
  7. mlrun/artifacts/dataset.py +17 -17
  8. mlrun/artifacts/document.py +454 -0
  9. mlrun/artifacts/manager.py +28 -18
  10. mlrun/artifacts/model.py +91 -59
  11. mlrun/artifacts/plots.py +2 -2
  12. mlrun/common/constants.py +8 -0
  13. mlrun/common/formatters/__init__.py +1 -0
  14. mlrun/common/formatters/artifact.py +1 -1
  15. mlrun/common/formatters/feature_set.py +2 -0
  16. mlrun/common/formatters/function.py +1 -0
  17. mlrun/{model_monitoring/db/stores/v3io_kv/__init__.py → common/formatters/model_endpoint.py} +17 -0
  18. mlrun/common/formatters/pipeline.py +1 -2
  19. mlrun/common/formatters/project.py +9 -0
  20. mlrun/common/model_monitoring/__init__.py +0 -5
  21. mlrun/common/model_monitoring/helpers.py +12 -62
  22. mlrun/common/runtimes/constants.py +25 -4
  23. mlrun/common/schemas/__init__.py +9 -5
  24. mlrun/common/schemas/alert.py +114 -19
  25. mlrun/common/schemas/api_gateway.py +3 -3
  26. mlrun/common/schemas/artifact.py +22 -9
  27. mlrun/common/schemas/auth.py +8 -4
  28. mlrun/common/schemas/background_task.py +7 -7
  29. mlrun/common/schemas/client_spec.py +4 -4
  30. mlrun/common/schemas/clusterization_spec.py +2 -2
  31. mlrun/common/schemas/common.py +53 -3
  32. mlrun/common/schemas/constants.py +15 -0
  33. mlrun/common/schemas/datastore_profile.py +1 -1
  34. mlrun/common/schemas/feature_store.py +9 -9
  35. mlrun/common/schemas/frontend_spec.py +4 -4
  36. mlrun/common/schemas/function.py +10 -10
  37. mlrun/common/schemas/hub.py +1 -1
  38. mlrun/common/schemas/k8s.py +3 -3
  39. mlrun/common/schemas/memory_reports.py +3 -3
  40. mlrun/common/schemas/model_monitoring/__init__.py +4 -8
  41. mlrun/common/schemas/model_monitoring/constants.py +127 -46
  42. mlrun/common/schemas/model_monitoring/grafana.py +18 -12
  43. mlrun/common/schemas/model_monitoring/model_endpoints.py +154 -160
  44. mlrun/common/schemas/notification.py +24 -3
  45. mlrun/common/schemas/object.py +1 -1
  46. mlrun/common/schemas/pagination.py +4 -4
  47. mlrun/common/schemas/partition.py +142 -0
  48. mlrun/common/schemas/pipeline.py +3 -3
  49. mlrun/common/schemas/project.py +26 -18
  50. mlrun/common/schemas/runs.py +3 -3
  51. mlrun/common/schemas/runtime_resource.py +5 -5
  52. mlrun/common/schemas/schedule.py +1 -1
  53. mlrun/common/schemas/secret.py +1 -1
  54. mlrun/{model_monitoring/db/stores/sqldb/__init__.py → common/schemas/serving.py} +10 -1
  55. mlrun/common/schemas/tag.py +3 -3
  56. mlrun/common/schemas/workflow.py +6 -5
  57. mlrun/common/types.py +1 -0
  58. mlrun/config.py +157 -89
  59. mlrun/data_types/__init__.py +5 -3
  60. mlrun/data_types/infer.py +13 -3
  61. mlrun/data_types/spark.py +2 -1
  62. mlrun/datastore/__init__.py +59 -18
  63. mlrun/datastore/alibaba_oss.py +4 -1
  64. mlrun/datastore/azure_blob.py +4 -1
  65. mlrun/datastore/base.py +19 -24
  66. mlrun/datastore/datastore.py +10 -4
  67. mlrun/datastore/datastore_profile.py +178 -45
  68. mlrun/datastore/dbfs_store.py +4 -1
  69. mlrun/datastore/filestore.py +4 -1
  70. mlrun/datastore/google_cloud_storage.py +4 -1
  71. mlrun/datastore/hdfs.py +4 -1
  72. mlrun/datastore/inmem.py +4 -1
  73. mlrun/datastore/redis.py +4 -1
  74. mlrun/datastore/s3.py +14 -3
  75. mlrun/datastore/sources.py +89 -92
  76. mlrun/datastore/store_resources.py +7 -4
  77. mlrun/datastore/storeytargets.py +51 -16
  78. mlrun/datastore/targets.py +38 -31
  79. mlrun/datastore/utils.py +87 -4
  80. mlrun/datastore/v3io.py +4 -1
  81. mlrun/datastore/vectorstore.py +291 -0
  82. mlrun/datastore/wasbfs/fs.py +13 -12
  83. mlrun/db/base.py +286 -100
  84. mlrun/db/httpdb.py +1562 -490
  85. mlrun/db/nopdb.py +250 -83
  86. mlrun/errors.py +6 -2
  87. mlrun/execution.py +194 -50
  88. mlrun/feature_store/__init__.py +2 -10
  89. mlrun/feature_store/api.py +20 -458
  90. mlrun/feature_store/common.py +9 -9
  91. mlrun/feature_store/feature_set.py +20 -18
  92. mlrun/feature_store/feature_vector.py +105 -479
  93. mlrun/feature_store/feature_vector_utils.py +466 -0
  94. mlrun/feature_store/retrieval/base.py +15 -11
  95. mlrun/feature_store/retrieval/job.py +2 -1
  96. mlrun/feature_store/retrieval/storey_merger.py +1 -1
  97. mlrun/feature_store/steps.py +3 -3
  98. mlrun/features.py +30 -13
  99. mlrun/frameworks/__init__.py +1 -2
  100. mlrun/frameworks/_common/__init__.py +1 -2
  101. mlrun/frameworks/_common/artifacts_library.py +2 -2
  102. mlrun/frameworks/_common/mlrun_interface.py +10 -6
  103. mlrun/frameworks/_common/model_handler.py +31 -31
  104. mlrun/frameworks/_common/producer.py +3 -1
  105. mlrun/frameworks/_dl_common/__init__.py +1 -2
  106. mlrun/frameworks/_dl_common/loggers/__init__.py +1 -2
  107. mlrun/frameworks/_dl_common/loggers/mlrun_logger.py +4 -4
  108. mlrun/frameworks/_dl_common/loggers/tensorboard_logger.py +3 -3
  109. mlrun/frameworks/_ml_common/__init__.py +1 -2
  110. mlrun/frameworks/_ml_common/loggers/__init__.py +1 -2
  111. mlrun/frameworks/_ml_common/model_handler.py +21 -21
  112. mlrun/frameworks/_ml_common/plans/__init__.py +1 -2
  113. mlrun/frameworks/_ml_common/plans/confusion_matrix_plan.py +3 -1
  114. mlrun/frameworks/_ml_common/plans/dataset_plan.py +3 -3
  115. mlrun/frameworks/_ml_common/plans/roc_curve_plan.py +4 -4
  116. mlrun/frameworks/auto_mlrun/__init__.py +1 -2
  117. mlrun/frameworks/auto_mlrun/auto_mlrun.py +22 -15
  118. mlrun/frameworks/huggingface/__init__.py +1 -2
  119. mlrun/frameworks/huggingface/model_server.py +9 -9
  120. mlrun/frameworks/lgbm/__init__.py +47 -44
  121. mlrun/frameworks/lgbm/callbacks/__init__.py +1 -2
  122. mlrun/frameworks/lgbm/callbacks/logging_callback.py +4 -2
  123. mlrun/frameworks/lgbm/callbacks/mlrun_logging_callback.py +4 -2
  124. mlrun/frameworks/lgbm/mlrun_interfaces/__init__.py +1 -2
  125. mlrun/frameworks/lgbm/mlrun_interfaces/mlrun_interface.py +5 -5
  126. mlrun/frameworks/lgbm/model_handler.py +15 -11
  127. mlrun/frameworks/lgbm/model_server.py +11 -7
  128. mlrun/frameworks/lgbm/utils.py +2 -2
  129. mlrun/frameworks/onnx/__init__.py +1 -2
  130. mlrun/frameworks/onnx/dataset.py +3 -3
  131. mlrun/frameworks/onnx/mlrun_interface.py +2 -2
  132. mlrun/frameworks/onnx/model_handler.py +7 -5
  133. mlrun/frameworks/onnx/model_server.py +8 -6
  134. mlrun/frameworks/parallel_coordinates.py +11 -11
  135. mlrun/frameworks/pytorch/__init__.py +22 -23
  136. mlrun/frameworks/pytorch/callbacks/__init__.py +1 -2
  137. mlrun/frameworks/pytorch/callbacks/callback.py +2 -1
  138. mlrun/frameworks/pytorch/callbacks/logging_callback.py +15 -8
  139. mlrun/frameworks/pytorch/callbacks/mlrun_logging_callback.py +19 -12
  140. mlrun/frameworks/pytorch/callbacks/tensorboard_logging_callback.py +22 -15
  141. mlrun/frameworks/pytorch/callbacks_handler.py +36 -30
  142. mlrun/frameworks/pytorch/mlrun_interface.py +17 -17
  143. mlrun/frameworks/pytorch/model_handler.py +21 -17
  144. mlrun/frameworks/pytorch/model_server.py +13 -9
  145. mlrun/frameworks/sklearn/__init__.py +19 -18
  146. mlrun/frameworks/sklearn/estimator.py +2 -2
  147. mlrun/frameworks/sklearn/metric.py +3 -3
  148. mlrun/frameworks/sklearn/metrics_library.py +8 -6
  149. mlrun/frameworks/sklearn/mlrun_interface.py +3 -2
  150. mlrun/frameworks/sklearn/model_handler.py +4 -3
  151. mlrun/frameworks/tf_keras/__init__.py +11 -12
  152. mlrun/frameworks/tf_keras/callbacks/__init__.py +1 -2
  153. mlrun/frameworks/tf_keras/callbacks/logging_callback.py +17 -14
  154. mlrun/frameworks/tf_keras/callbacks/mlrun_logging_callback.py +15 -12
  155. mlrun/frameworks/tf_keras/callbacks/tensorboard_logging_callback.py +21 -18
  156. mlrun/frameworks/tf_keras/model_handler.py +17 -13
  157. mlrun/frameworks/tf_keras/model_server.py +12 -8
  158. mlrun/frameworks/xgboost/__init__.py +19 -18
  159. mlrun/frameworks/xgboost/model_handler.py +13 -9
  160. mlrun/k8s_utils.py +2 -5
  161. mlrun/launcher/base.py +3 -4
  162. mlrun/launcher/client.py +2 -2
  163. mlrun/launcher/local.py +6 -2
  164. mlrun/launcher/remote.py +1 -1
  165. mlrun/lists.py +8 -4
  166. mlrun/model.py +132 -46
  167. mlrun/model_monitoring/__init__.py +3 -5
  168. mlrun/model_monitoring/api.py +113 -98
  169. mlrun/model_monitoring/applications/__init__.py +0 -5
  170. mlrun/model_monitoring/applications/_application_steps.py +81 -50
  171. mlrun/model_monitoring/applications/base.py +467 -14
  172. mlrun/model_monitoring/applications/context.py +212 -134
  173. mlrun/model_monitoring/{db/stores/base → applications/evidently}/__init__.py +6 -2
  174. mlrun/model_monitoring/applications/evidently/base.py +146 -0
  175. mlrun/model_monitoring/applications/histogram_data_drift.py +89 -56
  176. mlrun/model_monitoring/applications/results.py +67 -15
  177. mlrun/model_monitoring/controller.py +701 -315
  178. mlrun/model_monitoring/db/__init__.py +0 -2
  179. mlrun/model_monitoring/db/_schedules.py +242 -0
  180. mlrun/model_monitoring/db/_stats.py +189 -0
  181. mlrun/model_monitoring/db/tsdb/__init__.py +33 -22
  182. mlrun/model_monitoring/db/tsdb/base.py +243 -49
  183. mlrun/model_monitoring/db/tsdb/tdengine/schemas.py +76 -36
  184. mlrun/model_monitoring/db/tsdb/tdengine/stream_graph_steps.py +33 -0
  185. mlrun/model_monitoring/db/tsdb/tdengine/tdengine_connection.py +213 -0
  186. mlrun/model_monitoring/db/tsdb/tdengine/tdengine_connector.py +534 -88
  187. mlrun/model_monitoring/db/tsdb/v3io/stream_graph_steps.py +1 -0
  188. mlrun/model_monitoring/db/tsdb/v3io/v3io_connector.py +436 -106
  189. mlrun/model_monitoring/helpers.py +356 -114
  190. mlrun/model_monitoring/stream_processing.py +190 -345
  191. mlrun/model_monitoring/tracking_policy.py +11 -4
  192. mlrun/model_monitoring/writer.py +49 -90
  193. mlrun/package/__init__.py +3 -6
  194. mlrun/package/context_handler.py +2 -2
  195. mlrun/package/packager.py +12 -9
  196. mlrun/package/packagers/__init__.py +0 -2
  197. mlrun/package/packagers/default_packager.py +14 -11
  198. mlrun/package/packagers/numpy_packagers.py +16 -7
  199. mlrun/package/packagers/pandas_packagers.py +18 -18
  200. mlrun/package/packagers/python_standard_library_packagers.py +25 -11
  201. mlrun/package/packagers_manager.py +35 -32
  202. mlrun/package/utils/__init__.py +0 -3
  203. mlrun/package/utils/_pickler.py +6 -6
  204. mlrun/platforms/__init__.py +47 -16
  205. mlrun/platforms/iguazio.py +4 -1
  206. mlrun/projects/operations.py +30 -30
  207. mlrun/projects/pipelines.py +116 -47
  208. mlrun/projects/project.py +1292 -329
  209. mlrun/render.py +5 -9
  210. mlrun/run.py +57 -14
  211. mlrun/runtimes/__init__.py +1 -3
  212. mlrun/runtimes/base.py +30 -22
  213. mlrun/runtimes/daskjob.py +9 -9
  214. mlrun/runtimes/databricks_job/databricks_runtime.py +6 -5
  215. mlrun/runtimes/function_reference.py +5 -2
  216. mlrun/runtimes/generators.py +3 -2
  217. mlrun/runtimes/kubejob.py +6 -7
  218. mlrun/runtimes/mounts.py +574 -0
  219. mlrun/runtimes/mpijob/__init__.py +0 -2
  220. mlrun/runtimes/mpijob/abstract.py +7 -6
  221. mlrun/runtimes/nuclio/api_gateway.py +7 -7
  222. mlrun/runtimes/nuclio/application/application.py +11 -13
  223. mlrun/runtimes/nuclio/application/reverse_proxy.go +66 -64
  224. mlrun/runtimes/nuclio/function.py +127 -70
  225. mlrun/runtimes/nuclio/serving.py +105 -37
  226. mlrun/runtimes/pod.py +159 -54
  227. mlrun/runtimes/remotesparkjob.py +3 -2
  228. mlrun/runtimes/sparkjob/__init__.py +0 -2
  229. mlrun/runtimes/sparkjob/spark3job.py +22 -12
  230. mlrun/runtimes/utils.py +7 -6
  231. mlrun/secrets.py +2 -2
  232. mlrun/serving/__init__.py +8 -0
  233. mlrun/serving/merger.py +7 -5
  234. mlrun/serving/remote.py +35 -22
  235. mlrun/serving/routers.py +186 -240
  236. mlrun/serving/server.py +41 -10
  237. mlrun/serving/states.py +432 -118
  238. mlrun/serving/utils.py +13 -2
  239. mlrun/serving/v1_serving.py +3 -2
  240. mlrun/serving/v2_serving.py +161 -203
  241. mlrun/track/__init__.py +1 -1
  242. mlrun/track/tracker.py +2 -2
  243. mlrun/track/trackers/mlflow_tracker.py +6 -5
  244. mlrun/utils/async_http.py +35 -22
  245. mlrun/utils/clones.py +7 -4
  246. mlrun/utils/helpers.py +511 -58
  247. mlrun/utils/logger.py +119 -13
  248. mlrun/utils/notifications/notification/__init__.py +22 -19
  249. mlrun/utils/notifications/notification/base.py +39 -15
  250. mlrun/utils/notifications/notification/console.py +6 -6
  251. mlrun/utils/notifications/notification/git.py +11 -11
  252. mlrun/utils/notifications/notification/ipython.py +10 -9
  253. mlrun/utils/notifications/notification/mail.py +176 -0
  254. mlrun/utils/notifications/notification/slack.py +16 -8
  255. mlrun/utils/notifications/notification/webhook.py +24 -8
  256. mlrun/utils/notifications/notification_pusher.py +191 -200
  257. mlrun/utils/regex.py +12 -2
  258. mlrun/utils/version/version.json +2 -2
  259. {mlrun-1.7.2rc4.dist-info → mlrun-1.8.0.dist-info}/METADATA +69 -54
  260. mlrun-1.8.0.dist-info/RECORD +351 -0
  261. {mlrun-1.7.2rc4.dist-info → mlrun-1.8.0.dist-info}/WHEEL +1 -1
  262. mlrun/model_monitoring/applications/evidently_base.py +0 -137
  263. mlrun/model_monitoring/db/stores/__init__.py +0 -136
  264. mlrun/model_monitoring/db/stores/base/store.py +0 -213
  265. mlrun/model_monitoring/db/stores/sqldb/models/__init__.py +0 -71
  266. mlrun/model_monitoring/db/stores/sqldb/models/base.py +0 -190
  267. mlrun/model_monitoring/db/stores/sqldb/models/mysql.py +0 -103
  268. mlrun/model_monitoring/db/stores/sqldb/models/sqlite.py +0 -40
  269. mlrun/model_monitoring/db/stores/sqldb/sql_store.py +0 -659
  270. mlrun/model_monitoring/db/stores/v3io_kv/kv_store.py +0 -726
  271. mlrun/model_monitoring/model_endpoint.py +0 -118
  272. mlrun-1.7.2rc4.dist-info/RECORD +0 -351
  273. {mlrun-1.7.2rc4.dist-info → mlrun-1.8.0.dist-info}/entry_points.txt +0 -0
  274. {mlrun-1.7.2rc4.dist-info → mlrun-1.8.0.dist-info/licenses}/LICENSE +0 -0
  275. {mlrun-1.7.2rc4.dist-info → mlrun-1.8.0.dist-info}/top_level.txt +0 -0
mlrun/utils/helpers.py CHANGED
@@ -13,8 +13,10 @@
13
13
  # limitations under the License.
14
14
 
15
15
  import asyncio
16
+ import base64
16
17
  import enum
17
18
  import functools
19
+ import gzip
18
20
  import hashlib
19
21
  import inspect
20
22
  import itertools
@@ -23,35 +25,43 @@ import os
23
25
  import re
24
26
  import string
25
27
  import sys
28
+ import traceback
26
29
  import typing
27
30
  import uuid
28
31
  import warnings
29
- from datetime import datetime, timezone
32
+ from datetime import datetime, timedelta, timezone
30
33
  from importlib import import_module, reload
31
34
  from os import path
32
35
  from types import ModuleType
33
36
  from typing import Any, Optional
37
+ from urllib.parse import urlparse
34
38
 
35
39
  import git
36
40
  import inflection
37
41
  import numpy as np
38
42
  import packaging.version
39
43
  import pandas
44
+ import pytz
40
45
  import semver
41
46
  import yaml
42
47
  from dateutil import parser
43
- from mlrun_pipelines.models import PipelineRun
44
48
  from pandas import Timedelta, Timestamp
45
49
  from yaml.representer import RepresenterError
46
50
 
47
51
  import mlrun
52
+ import mlrun.common.constants as mlrun_constants
48
53
  import mlrun.common.helpers
54
+ import mlrun.common.runtimes.constants as runtimes_constants
49
55
  import mlrun.common.schemas
50
56
  import mlrun.errors
51
57
  import mlrun.utils.regex
52
58
  import mlrun.utils.version.version
59
+ import mlrun_pipelines.common.constants
60
+ import mlrun_pipelines.models
61
+ import mlrun_pipelines.utils
53
62
  from mlrun.common.constants import MYSQL_MEDIUMBLOB_SIZE_BYTES
54
63
  from mlrun.config import config
64
+ from mlrun_pipelines.models import PipelineRun
55
65
 
56
66
  from .logger import create_logger
57
67
  from .retryer import ( # noqa: F401
@@ -85,14 +95,19 @@ class StorePrefix:
85
95
  Artifact = "artifacts"
86
96
  Model = "models"
87
97
  Dataset = "datasets"
98
+ Document = "documents"
88
99
 
89
100
  @classmethod
90
101
  def is_artifact(cls, prefix):
91
- return prefix in [cls.Artifact, cls.Model, cls.Dataset]
102
+ return prefix in [cls.Artifact, cls.Model, cls.Dataset, cls.Document]
92
103
 
93
104
  @classmethod
94
105
  def kind_to_prefix(cls, kind):
95
- kind_map = {"model": cls.Model, "dataset": cls.Dataset}
106
+ kind_map = {
107
+ "model": cls.Model,
108
+ "dataset": cls.Dataset,
109
+ "document": cls.Document,
110
+ }
96
111
  return kind_map.get(kind, cls.Artifact)
97
112
 
98
113
  @classmethod
@@ -103,6 +118,7 @@ class StorePrefix:
103
118
  cls.Dataset,
104
119
  cls.FeatureSet,
105
120
  cls.FeatureVector,
121
+ cls.Document,
106
122
  ]
107
123
 
108
124
 
@@ -111,21 +127,27 @@ def get_artifact_target(item: dict, project=None):
111
127
  project_str = project or item["metadata"].get("project")
112
128
  tree = item["metadata"].get("tree")
113
129
  tag = item["metadata"].get("tag")
130
+ iter = item["metadata"].get("iter")
114
131
  kind = item.get("kind")
132
+ uid = item["metadata"].get("uid")
115
133
 
116
134
  if kind in {"dataset", "model", "artifact"} and db_key:
117
135
  target = (
118
136
  f"{DB_SCHEMA}://{StorePrefix.kind_to_prefix(kind)}/{project_str}/{db_key}"
119
137
  )
138
+ if iter:
139
+ target = f"{target}#{iter}"
120
140
  target += f":{tag}" if tag else ":latest"
121
141
  if tree:
122
142
  target += f"@{tree}"
143
+ if uid:
144
+ target += f"^{uid}"
123
145
  return target
124
146
 
125
147
  return item["spec"].get("target_path")
126
148
 
127
149
 
128
- # TODO: left for migrations testing purposes. Remove in 1.8.0.
150
+ # TODO: Remove once data migration v5 is obsolete
129
151
  def is_legacy_artifact(artifact):
130
152
  if isinstance(artifact, dict):
131
153
  return "metadata" not in artifact
@@ -167,6 +189,7 @@ class RunKeys:
167
189
  inputs = "inputs"
168
190
  returns = "returns"
169
191
  artifacts = "artifacts"
192
+ artifact_uris = "artifact_uris"
170
193
  outputs = "outputs"
171
194
  data_stores = "data_stores"
172
195
  secrets = "secret_sources"
@@ -220,7 +243,7 @@ def verify_field_regex(
220
243
 
221
244
 
222
245
  def validate_builder_source(
223
- source: str, pull_at_runtime: bool = False, workdir: str = None
246
+ source: str, pull_at_runtime: bool = False, workdir: Optional[str] = None
224
247
  ):
225
248
  if pull_at_runtime or not source:
226
249
  return
@@ -268,12 +291,14 @@ def validate_tag_name(
268
291
  def validate_artifact_key_name(
269
292
  artifact_key: str, field_name: str, raise_on_failure: bool = True
270
293
  ) -> bool:
294
+ field_type = "key" if field_name == "artifact.key" else "db_key"
271
295
  return mlrun.utils.helpers.verify_field_regex(
272
296
  field_name,
273
297
  artifact_key,
274
298
  mlrun.utils.regex.artifact_key,
275
299
  raise_on_failure=raise_on_failure,
276
- log_message="Slashes are not permitted in the artifact key (both \\ and /)",
300
+ log_message=f"Artifact {field_type} must start and end with an alphanumeric character, and may only contain "
301
+ "letters, numbers, hyphens, underscores, and dots.",
277
302
  )
278
303
 
279
304
 
@@ -354,8 +379,8 @@ def verify_field_list_of_type(
354
379
  def verify_dict_items_type(
355
380
  name: str,
356
381
  dictionary: dict,
357
- expected_keys_types: list = None,
358
- expected_values_types: list = None,
382
+ expected_keys_types: Optional[list] = None,
383
+ expected_values_types: Optional[list] = None,
359
384
  ):
360
385
  if dictionary:
361
386
  if not isinstance(dictionary, dict):
@@ -372,7 +397,7 @@ def verify_dict_items_type(
372
397
  ) from exc
373
398
 
374
399
 
375
- def verify_list_items_type(list_, expected_types: list = None):
400
+ def verify_list_items_type(list_, expected_types: Optional[list] = None):
376
401
  if list_ and expected_types:
377
402
  list_items_types = set(map(type, list_))
378
403
  expected_types = set(expected_types)
@@ -396,6 +421,32 @@ def now_date(tz: timezone = timezone.utc) -> datetime:
396
421
  return datetime.now(tz=tz)
397
422
 
398
423
 
424
+ def datetime_to_mysql_ts(datetime_object: datetime) -> datetime:
425
+ """
426
+ Convert a Python datetime object to a MySQL-compatible timestamp string,
427
+ rounded to the nearest millisecond.
428
+ Example: 2024-12-18T16:36:05.235687+00:00 -> 2024-12-18T16:36:05.236000
429
+
430
+ :param datetime_object: A Python datetime object.
431
+
432
+ :return: A MySQL-compatible timestamp string with millisecond precision.
433
+ """
434
+ if not datetime_object.tzinfo:
435
+ datetime_object = datetime_object.replace(tzinfo=timezone.utc)
436
+
437
+ # Round to the nearest millisecond
438
+ ms = round(datetime_object.microsecond / 1000) * 1000
439
+ if ms == 1000000:
440
+ datetime_object += timedelta(seconds=1)
441
+ ms = 0
442
+
443
+ return datetime_object.replace(microsecond=ms)
444
+
445
+
446
+ def datetime_min(tz: timezone = timezone.utc) -> datetime:
447
+ return datetime(1970, 1, 1, tzinfo=tz)
448
+
449
+
399
450
  datetime_now = now_date
400
451
 
401
452
 
@@ -448,7 +499,6 @@ def get_in(obj, keys, default=None):
448
499
  """
449
500
  if isinstance(keys, str):
450
501
  keys = keys.split(".")
451
-
452
502
  for key in keys:
453
503
  if not obj or key not in obj:
454
504
  return default
@@ -663,8 +713,8 @@ def dict_to_json(struct):
663
713
 
664
714
  def parse_artifact_uri(uri, default_project=""):
665
715
  """
666
- Parse artifact URI into project, key, tag, iter, tree
667
- URI format: [<project>/]<key>[#<iter>][:<tag>][@<tree>]
716
+ Parse artifact URI into project, key, tag, iter, tree, uid
717
+ URI format: [<project>/]<key>[#<iter>][:<tag>][@<tree>][^<uid>]
668
718
 
669
719
  :param uri: uri to parse
670
720
  :param default_project: default project name if not in URI
@@ -674,6 +724,7 @@ def parse_artifact_uri(uri, default_project=""):
674
724
  [2] = iteration
675
725
  [3] = tag
676
726
  [4] = tree
727
+ [5] = uid
677
728
  """
678
729
  uri_pattern = mlrun.utils.regex.artifact_uri_pattern
679
730
  match = re.match(uri_pattern, uri)
@@ -698,6 +749,7 @@ def parse_artifact_uri(uri, default_project=""):
698
749
  iteration,
699
750
  group_dict["tag"],
700
751
  group_dict["tree"],
752
+ group_dict["uid"],
701
753
  )
702
754
 
703
755
 
@@ -712,7 +764,9 @@ def generate_object_uri(project, name, tag=None, hash_key=None):
712
764
  return uri
713
765
 
714
766
 
715
- def generate_artifact_uri(project, key, tag=None, iter=None, tree=None):
767
+ def generate_artifact_uri(
768
+ project, key, tag=None, iter=None, tree=None, uid=None
769
+ ) -> str:
716
770
  artifact_uri = f"{project}/{key}"
717
771
  if iter is not None:
718
772
  artifact_uri = f"{artifact_uri}#{iter}"
@@ -720,6 +774,8 @@ def generate_artifact_uri(project, key, tag=None, iter=None, tree=None):
720
774
  artifact_uri = f"{artifact_uri}:{tag}"
721
775
  if tree is not None:
722
776
  artifact_uri = f"{artifact_uri}@{tree}"
777
+ if uid is not None:
778
+ artifact_uri = f"{artifact_uri}^{uid}"
723
779
  return artifact_uri
724
780
 
725
781
 
@@ -816,7 +872,9 @@ def _convert_python_package_version_to_image_tag(version: typing.Optional[str]):
816
872
 
817
873
 
818
874
  def enrich_image_url(
819
- image_url: str, client_version: str = None, client_python_version: str = None
875
+ image_url: str,
876
+ client_version: Optional[str] = None,
877
+ client_python_version: Optional[str] = None,
820
878
  ) -> str:
821
879
  client_version = _convert_python_package_version_to_image_tag(client_version)
822
880
  server_version = _convert_python_package_version_to_image_tag(
@@ -856,7 +914,7 @@ def enrich_image_url(
856
914
 
857
915
 
858
916
  def resolve_image_tag_suffix(
859
- mlrun_version: str = None, python_version: str = None
917
+ mlrun_version: Optional[str] = None, python_version: Optional[str] = None
860
918
  ) -> str:
861
919
  """
862
920
  resolves what suffix should be appended to the image tag
@@ -989,49 +1047,165 @@ async def retry_until_successful_async(
989
1047
  ).run()
990
1048
 
991
1049
 
992
- def get_ui_url(project, uid=None):
993
- url = ""
1050
+ def get_project_url(project: str) -> str:
1051
+ """
1052
+ Generate the base URL for a given project.
1053
+
1054
+ :param project: The project name.
1055
+ :return: The base URL for the project, or an empty string if the base URL is not resolved.
1056
+ """
994
1057
  if mlrun.mlconf.resolve_ui_url():
995
- url = f"{mlrun.mlconf.resolve_ui_url()}/{mlrun.mlconf.ui.projects_prefix}/{project}/jobs"
996
- if uid:
997
- url += f"/monitor/{uid}/overview"
1058
+ return f"{mlrun.mlconf.resolve_ui_url()}/{mlrun.mlconf.ui.projects_prefix}/{project}"
1059
+ return ""
1060
+
1061
+
1062
+ def get_run_url(project: str, uid: str, name: str) -> str:
1063
+ """
1064
+ Generate the URL for a specific run.
1065
+
1066
+ :param project: The project name.
1067
+ :param uid: The run UID.
1068
+ :param name: The run name.
1069
+ :return: The URL for the run, or an empty string if the base URL is not resolved.
1070
+ """
1071
+ runs_url = get_runs_url(project)
1072
+ if not runs_url:
1073
+ return ""
1074
+ return f"{runs_url}/monitor-jobs/{name}/{uid}/overview"
1075
+
1076
+
1077
+ def get_runs_url(project: str) -> str:
1078
+ """
1079
+ Generate the URL for the runs of a given project.
1080
+
1081
+ :param project: The project name.
1082
+ :return: The URL for the runs, or an empty string if the base URL is not resolved.
1083
+ """
1084
+ base_url = get_project_url(project)
1085
+ if not base_url:
1086
+ return ""
1087
+ return f"{base_url}/jobs"
1088
+
1089
+
1090
+ def get_model_endpoint_url(
1091
+ project: str,
1092
+ model_name: Optional[str] = None,
1093
+ model_endpoint_id: Optional[str] = None,
1094
+ ) -> str:
1095
+ """
1096
+ Generate the URL for a specific model endpoint.
1097
+
1098
+ :param project: The project name.
1099
+ :param model_name: The model name.
1100
+ :param model_endpoint_id: The model endpoint ID.
1101
+ :return: The URL for the model endpoint, or an empty string if the base URL is not resolved.
1102
+ """
1103
+ base_url = get_project_url(project)
1104
+ if not base_url:
1105
+ return ""
1106
+ url = f"{base_url}/models"
1107
+ if model_name and model_endpoint_id:
1108
+ url += f"/model-endpoints/{model_name}/{model_endpoint_id}/overview"
998
1109
  return url
999
1110
 
1000
1111
 
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"
1112
+ def get_workflow_url(
1113
+ project: str,
1114
+ id: Optional[str] = None,
1115
+ ) -> str:
1116
+ """
1117
+ Generate the URL for a specific workflow.
1118
+
1119
+ :param project: The project name.
1120
+ :param id: The workflow ID.
1121
+ :return: The URL for the workflow, or an empty string if the base URL is not resolved.
1122
+ """
1123
+ base_url = get_project_url(project)
1124
+ if not base_url:
1125
+ return ""
1126
+ url = f"{base_url}/jobs/monitor-workflows/workflow"
1127
+ if id:
1128
+ url += f"/{id}"
1007
1129
  return url
1008
1130
 
1009
1131
 
1010
- def get_workflow_url(project, id=None):
1011
- url = ""
1012
- if mlrun.mlconf.resolve_ui_url():
1013
- url = (
1014
- f"{mlrun.mlconf.resolve_ui_url()}/{mlrun.mlconf.ui.projects_prefix}"
1015
- f"/{project}/jobs/monitor-workflows/workflow/{id}"
1132
+ def get_kfp_list_runs_filter(
1133
+ project_name: Optional[str] = None,
1134
+ end_date: Optional[str] = None,
1135
+ start_date: Optional[str] = None,
1136
+ ) -> str:
1137
+ """
1138
+ Generates a filter for listing Kubeflow Pipelines (KFP) runs.
1139
+
1140
+ :param project_name: The name of the project. If "*", it won't filter by project.
1141
+ :param end_date: The latest creation date for filtering runs (ISO 8601 format).
1142
+ :param start_date: The earliest creation date for filtering runs (ISO 8601 format).
1143
+ :return: A JSON-formatted filter string for KFP.
1144
+ """
1145
+
1146
+ # KFP filter operation codes
1147
+ kfp_less_than_or_equal_op = 7 # '<='
1148
+ kfp_greater_than_or_equal_op = 5 # '>='
1149
+ kfp_substring_op = 9 # Substring match
1150
+
1151
+ filters = {"predicates": []}
1152
+
1153
+ if end_date:
1154
+ filters["predicates"].append(
1155
+ {
1156
+ "key": "created_at",
1157
+ "op": kfp_less_than_or_equal_op,
1158
+ "timestamp_value": end_date,
1159
+ }
1016
1160
  )
1017
- return url
1161
+
1162
+ if project_name and project_name != "*":
1163
+ filters["predicates"].append(
1164
+ {
1165
+ "key": "name",
1166
+ "op": kfp_substring_op,
1167
+ "string_value": project_name,
1168
+ }
1169
+ )
1170
+ if start_date:
1171
+ filters["predicates"].append(
1172
+ {
1173
+ "key": "created_at",
1174
+ "op": kfp_greater_than_or_equal_op,
1175
+ "timestamp_value": start_date,
1176
+ }
1177
+ )
1178
+ return json.dumps(filters)
1018
1179
 
1019
1180
 
1020
- def get_kfp_project_filter(project_name: str) -> str:
1181
+ def validate_and_convert_date(date_input: str) -> str:
1021
1182
  """
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.
1183
+ Converts any recognizable date string into a standardized RFC 3339 format.
1184
+ :param date_input: A date string in a recognizable format.
1027
1185
  """
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)
1186
+ try:
1187
+ dt_object = parser.parse(date_input)
1188
+ if dt_object.tzinfo is not None:
1189
+ # Convert to UTC if it's in a different timezone
1190
+ dt_object = dt_object.astimezone(pytz.utc)
1191
+ else:
1192
+ # If no timezone info is present, assume it's in local time
1193
+ local_tz = pytz.timezone("UTC")
1194
+ dt_object = local_tz.localize(dt_object)
1195
+
1196
+ # Convert the datetime object to an RFC 3339-compliant string.
1197
+ # RFC 3339 requires timestamps to be in ISO 8601 format with a 'Z' suffix for UTC time.
1198
+ # The isoformat() method adds a "+00:00" suffix for UTC by default,
1199
+ # so we replace it with "Z" to ensure compliance.
1200
+ formatted_date = dt_object.isoformat().replace("+00:00", "Z")
1201
+ formatted_date = formatted_date.rstrip("Z") + "Z"
1202
+
1203
+ return formatted_date
1204
+ except (ValueError, OverflowError) as e:
1205
+ raise ValueError(
1206
+ f"Invalid date format: {date_input}."
1207
+ f" Date format must adhere to the RFC 3339 standard (e.g., 'YYYY-MM-DDTHH:MM:SSZ' for UTC)."
1208
+ ) from e
1035
1209
 
1036
1210
 
1037
1211
  def are_strings_in_exception_chain_messages(
@@ -1175,7 +1349,7 @@ def get_function(function, namespaces, reload_modules: bool = False):
1175
1349
  def get_handler_extended(
1176
1350
  handler_path: str,
1177
1351
  context=None,
1178
- class_args: dict = None,
1352
+ class_args: Optional[dict] = None,
1179
1353
  namespaces=None,
1180
1354
  reload_modules: bool = False,
1181
1355
  ):
@@ -1217,7 +1391,11 @@ def get_handler_extended(
1217
1391
  def datetime_from_iso(time_str: str) -> Optional[datetime]:
1218
1392
  if not time_str:
1219
1393
  return
1220
- return parser.isoparse(time_str)
1394
+ dt = parser.isoparse(time_str)
1395
+ if dt.tzinfo is None:
1396
+ dt = dt.replace(tzinfo=timezone.utc)
1397
+ # ensure the datetime is in UTC, converting if necessary
1398
+ return dt.astimezone(timezone.utc)
1221
1399
 
1222
1400
 
1223
1401
  def datetime_to_iso(time_obj: Optional[datetime]) -> Optional[str]:
@@ -1256,6 +1434,21 @@ def has_timezone(timestamp):
1256
1434
  return False
1257
1435
 
1258
1436
 
1437
+ def format_datetime(dt: datetime, fmt: Optional[str] = None) -> str:
1438
+ if dt is None:
1439
+ return ""
1440
+
1441
+ # If the datetime is naive
1442
+ if dt.tzinfo is None:
1443
+ dt = dt.replace(tzinfo=timezone.utc)
1444
+
1445
+ # TODO: Once Python 3.12 is the minimal version, use %:z to format the timezone offset with a colon
1446
+ formatted_time = dt.strftime(fmt or "%Y-%m-%d %H:%M:%S.%f%z")
1447
+
1448
+ # For versions earlier than Python 3.12, we manually insert the colon in the timezone offset
1449
+ return formatted_time[:-2] + ":" + formatted_time[-2:]
1450
+
1451
+
1259
1452
  def as_list(element: Any) -> list[Any]:
1260
1453
  return element if isinstance(element, list) else [element]
1261
1454
 
@@ -1309,6 +1502,17 @@ def to_non_empty_values_dict(input_dict: dict) -> dict:
1309
1502
  return {key: value for key, value in input_dict.items() if value}
1310
1503
 
1311
1504
 
1505
+ def get_enriched_gpu_limits(function_limits: dict) -> dict[str, int]:
1506
+ """
1507
+ Creates new limits containing the GPU-related limits from the function's limits,
1508
+ mapping each to zero. This is used for pods like Kaniko and Argo pods, which inherit
1509
+ GPU-related selectors but do not require GPU resources. By setting these
1510
+ limits to zero, the pods receive the necessary tolerations from the cloud provider for scheduling,
1511
+ without actually consuming GPU resources.
1512
+ """
1513
+ return {resource: 0 for resource in function_limits if "/gpu" in resource.lower()}
1514
+
1515
+
1312
1516
  def str_to_timestamp(time_str: str, now_time: Timestamp = None):
1313
1517
  """convert fixed/relative time string to Pandas Timestamp
1314
1518
 
@@ -1347,6 +1551,16 @@ def str_to_timestamp(time_str: str, now_time: Timestamp = None):
1347
1551
  return Timestamp(time_str)
1348
1552
 
1349
1553
 
1554
+ def str_to_bool(value: str) -> bool:
1555
+ """Convert a string to a boolean value."""
1556
+ value = value.lower()
1557
+ if value in ("true", "1", "t", "y", "yes", "on"):
1558
+ return True
1559
+ if value in ("false", "0", "f", "n", "no", "off"):
1560
+ return False
1561
+ raise ValueError(f"invalid boolean value: {value}")
1562
+
1563
+
1350
1564
  def is_link_artifact(artifact):
1351
1565
  if isinstance(artifact, dict):
1352
1566
  return (
@@ -1625,7 +1839,9 @@ setting partitioned=False"""
1625
1839
 
1626
1840
  def is_ecr_url(registry: str) -> bool:
1627
1841
  # example URL: <aws_account_id>.dkr.ecr.<region>.amazonaws.com
1628
- return ".ecr." in registry and ".amazonaws.com" in registry
1842
+ parsed_url = urlparse(f"https://{registry}")
1843
+ hostname = parsed_url.hostname
1844
+ return hostname and ".ecr." in hostname and hostname.endswith(".amazonaws.com")
1629
1845
 
1630
1846
 
1631
1847
  def get_local_file_schema() -> list:
@@ -1660,7 +1876,14 @@ def get_serving_spec():
1660
1876
  raise mlrun.errors.MLRunInvalidArgumentError(
1661
1877
  "Failed to find serving spec in env var or config file"
1662
1878
  )
1663
- spec = json.loads(data)
1879
+ # Attempt to decode and decompress, or use as-is for backward compatibility
1880
+ try:
1881
+ decoded_data = base64.b64decode(data)
1882
+ decompressed_data = gzip.decompress(decoded_data)
1883
+ spec = json.loads(decompressed_data.decode("utf-8"))
1884
+ except (OSError, gzip.BadGzipFile, base64.binascii.Error, json.JSONDecodeError):
1885
+ spec = json.loads(data)
1886
+
1664
1887
  return spec
1665
1888
 
1666
1889
 
@@ -1694,17 +1917,22 @@ def merge_dicts_with_precedence(*dicts: dict) -> dict:
1694
1917
 
1695
1918
 
1696
1919
  def validate_component_version_compatibility(
1697
- component_name: typing.Literal["iguazio", "nuclio"], *min_versions: str
1920
+ component_name: typing.Literal["iguazio", "nuclio", "mlrun-client"],
1921
+ *min_versions: str,
1922
+ mlrun_client_version: Optional[str] = None,
1698
1923
  ):
1699
1924
  """
1700
1925
  :param component_name: Name of the component to validate compatibility for.
1701
1926
  :param min_versions: Valid minimum version(s) required, assuming no 2 versions has equal major and minor.
1927
+ :param mlrun_client_version: Client version to validate when component_name is "mlrun-client".
1702
1928
  """
1703
1929
  parsed_min_versions = [
1704
1930
  semver.VersionInfo.parse(min_version) for min_version in min_versions
1705
1931
  ]
1706
1932
  parsed_current_version = None
1707
1933
  component_current_version = None
1934
+ # For mlrun client we don't assume compatability if we fail to parse the client version
1935
+ assume_compatible = component_name not in ["mlrun-client"]
1708
1936
  try:
1709
1937
  if component_name == "iguazio":
1710
1938
  component_current_version = mlrun.mlconf.igz_version
@@ -1721,18 +1949,29 @@ def validate_component_version_compatibility(
1721
1949
  parsed_current_version = semver.VersionInfo.parse(
1722
1950
  mlrun.mlconf.nuclio_version
1723
1951
  )
1952
+ if component_name == "mlrun-client":
1953
+ # dev version, assume compatible
1954
+ if mlrun_client_version and (
1955
+ mlrun_client_version.startswith("0.0.0+")
1956
+ or "unstable" in mlrun_client_version
1957
+ ):
1958
+ return True
1959
+
1960
+ component_current_version = mlrun_client_version
1961
+ parsed_current_version = semver.Version.parse(mlrun_client_version)
1724
1962
  if not parsed_current_version:
1725
- return True
1963
+ return assume_compatible
1726
1964
  except ValueError:
1727
1965
  # only log when version is set but invalid
1728
1966
  if component_current_version:
1729
1967
  logger.warning(
1730
- "Unable to parse current version, assuming compatibility",
1968
+ "Unable to parse current version",
1731
1969
  component_name=component_name,
1732
1970
  current_version=component_current_version,
1733
1971
  min_versions=min_versions,
1972
+ assume_compatible=assume_compatible,
1734
1973
  )
1735
- return True
1974
+ return assume_compatible
1736
1975
 
1737
1976
  # Feature might have been back-ported e.g. nuclio node selection is supported from
1738
1977
  # 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
@@ -1797,9 +2036,8 @@ def _reload(module, max_recursion_depth):
1797
2036
  def run_with_retry(
1798
2037
  retry_count: int,
1799
2038
  func: typing.Callable,
1800
- retry_on_exceptions: typing.Union[
1801
- type[Exception],
1802
- tuple[type[Exception]],
2039
+ retry_on_exceptions: Optional[
2040
+ typing.Union[type[Exception], tuple[type[Exception]]]
1803
2041
  ] = None,
1804
2042
  *args,
1805
2043
  **kwargs,
@@ -1832,3 +2070,218 @@ def run_with_retry(
1832
2070
  if attempt == retry_count:
1833
2071
  raise
1834
2072
  raise last_exception
2073
+
2074
+
2075
+ def join_urls(base_url: Optional[str], path: Optional[str]) -> str:
2076
+ """
2077
+ Joins a base URL with a path, ensuring proper handling of slashes.
2078
+
2079
+ :param base_url: The base URL (e.g., "http://example.com").
2080
+ :param path: The path to append to the base URL (e.g., "/path/to/resource").
2081
+
2082
+ :return: A unified URL with exactly one slash between base_url and path.
2083
+ """
2084
+ if base_url is None:
2085
+ base_url = ""
2086
+ return f"{base_url.rstrip('/')}/{path.lstrip('/')}" if path else base_url
2087
+
2088
+
2089
+ class Workflow:
2090
+ @staticmethod
2091
+ def get_workflow_steps(
2092
+ db: "mlrun.db.RunDBInterface", workflow_id: str, project: str
2093
+ ) -> list:
2094
+ steps = []
2095
+
2096
+ def _add_run_step(_step: mlrun_pipelines.models.PipelineStep):
2097
+ # on kfp 1.8 argo sets the pod hostname differently than what we have with kfp 2.5
2098
+ # therefore, the heuristic needs to change. what we do here is first trying against 1.8 conventions
2099
+ # and if we can't find it then falling back to 2.5
2100
+ try:
2101
+ # runner_pod = x-y-N
2102
+ _runs = db.list_runs(
2103
+ project=project,
2104
+ labels=f"{mlrun_constants.MLRunInternalLabels.runner_pod}={_step.node_name}",
2105
+ )
2106
+ if not _runs:
2107
+ try:
2108
+ # x-y-N -> x-y, N
2109
+ node_name_initials, node_name_generated_id = (
2110
+ _step.node_name.rsplit("-", 1)
2111
+ )
2112
+
2113
+ except ValueError:
2114
+ # defensive programming, if the node name is not in the expected format
2115
+ node_name_initials = _step.node_name
2116
+ node_name_generated_id = ""
2117
+
2118
+ # compile the expected runner pod hostname as per kfp >= 2.4
2119
+ # x-y, Z, N -> runner_pod = x-y-Z-N
2120
+ runner_pod_value = "-".join(
2121
+ [
2122
+ node_name_initials,
2123
+ _step.display_name,
2124
+ node_name_generated_id,
2125
+ ]
2126
+ ).rstrip("-")
2127
+ logger.debug(
2128
+ "No run found for step, trying with different node name",
2129
+ step_node_name=runner_pod_value,
2130
+ )
2131
+ _runs = db.list_runs(
2132
+ project=project,
2133
+ labels=f"{mlrun_constants.MLRunInternalLabels.runner_pod}={runner_pod_value}",
2134
+ )
2135
+
2136
+ _run = _runs[0]
2137
+ except IndexError:
2138
+ logger.warning("No run found for step", step=_step.to_dict())
2139
+ _run = {
2140
+ "metadata": {
2141
+ "name": _step.display_name,
2142
+ "project": project,
2143
+ },
2144
+ "status": {},
2145
+ }
2146
+ _run["step_kind"] = _step.step_type
2147
+ if _step.skipped:
2148
+ _run.setdefault("status", {})["state"] = (
2149
+ runtimes_constants.RunStates.skipped
2150
+ )
2151
+ steps.append(_run)
2152
+
2153
+ def _add_deploy_function_step(_step: mlrun_pipelines.models.PipelineStep):
2154
+ project, name, hash_key = Workflow._extract_function_uri(
2155
+ _step.get_annotation("mlrun/function-uri")
2156
+ )
2157
+ if name:
2158
+ try:
2159
+ function = db.get_function(
2160
+ project=project, name=name, hash_key=hash_key
2161
+ )
2162
+ except mlrun.errors.MLRunNotFoundError:
2163
+ # If the function is not found (if build failed for example), we will create a dummy
2164
+ # function object for the notification to display the function name
2165
+ function = {
2166
+ "metadata": {
2167
+ "name": name,
2168
+ "project": project,
2169
+ "hash_key": hash_key,
2170
+ },
2171
+ }
2172
+ pod_phase = _step.phase
2173
+ if _step.skipped:
2174
+ state = mlrun.common.schemas.FunctionState.skipped
2175
+ else:
2176
+ state = runtimes_constants.PodPhases.pod_phase_to_run_state(
2177
+ pod_phase
2178
+ )
2179
+ function["status"] = {"state": state}
2180
+ if isinstance(function["metadata"].get("updated"), datetime):
2181
+ function["metadata"]["updated"] = function["metadata"][
2182
+ "updated"
2183
+ ].isoformat()
2184
+ function["step_kind"] = _step.step_type
2185
+ steps.append(function)
2186
+
2187
+ step_methods = {
2188
+ mlrun_pipelines.common.constants.PipelineRunType.run: _add_run_step,
2189
+ mlrun_pipelines.common.constants.PipelineRunType.build: _add_deploy_function_step,
2190
+ mlrun_pipelines.common.constants.PipelineRunType.deploy: _add_deploy_function_step,
2191
+ }
2192
+
2193
+ if not workflow_id:
2194
+ return steps
2195
+
2196
+ try:
2197
+ workflow_manifest = Workflow._get_workflow_manifest(workflow_id)
2198
+ except Exception:
2199
+ logger.warning(
2200
+ "Failed to extract workflow steps from workflow manifest, "
2201
+ "returning all runs with the workflow id label",
2202
+ workflow_id=workflow_id,
2203
+ traceback=traceback.format_exc(),
2204
+ )
2205
+ return db.list_runs(
2206
+ project=project,
2207
+ labels=f"workflow={workflow_id}",
2208
+ )
2209
+
2210
+ if not workflow_manifest:
2211
+ return steps
2212
+
2213
+ try:
2214
+ for step in workflow_manifest.get_steps():
2215
+ step_method = step_methods.get(step.step_type)
2216
+ if step_method:
2217
+ step_method(step)
2218
+ return steps
2219
+ except Exception:
2220
+ # If we fail to read the pipeline steps, we will return the list of runs that have the same workflow id
2221
+ logger.warning(
2222
+ "Failed to extract workflow steps from workflow manifest, "
2223
+ "returning all runs with the workflow id label",
2224
+ workflow_id=workflow_id,
2225
+ traceback=traceback.format_exc(),
2226
+ )
2227
+ return db.list_runs(
2228
+ project=project,
2229
+ labels=f"workflow={workflow_id}",
2230
+ )
2231
+
2232
+ @staticmethod
2233
+ def _extract_function_uri(function_uri: str) -> tuple[str, str, str]:
2234
+ """
2235
+ Extract the project, name, and hash key from a function uri.
2236
+ Examples:
2237
+ - "project/name@hash_key" returns project, name, hash_key
2238
+ - "project/name returns" project, name, ""
2239
+ """
2240
+ project, name, hash_key = None, None, None
2241
+ hashed_pattern = r"^(.+)/(.+)@(.+)$"
2242
+ pattern = r"^(.+)/(.+)$"
2243
+ match = re.match(hashed_pattern, function_uri)
2244
+ if match:
2245
+ project, name, hash_key = match.groups()
2246
+ else:
2247
+ match = re.match(pattern, function_uri)
2248
+ if match:
2249
+ project, name = match.groups()
2250
+ hash_key = ""
2251
+ return project, name, hash_key
2252
+
2253
+ @staticmethod
2254
+ def _get_workflow_manifest(
2255
+ workflow_id: str,
2256
+ ) -> typing.Optional[mlrun_pipelines.models.PipelineManifest]:
2257
+ kfp_client = mlrun_pipelines.utils.get_client(mlrun.mlconf.kfp_url)
2258
+
2259
+ # arbitrary timeout of 30 seconds, the workflow should be done by now, however sometimes kfp takes a few
2260
+ # seconds to update the workflow status
2261
+ kfp_run = kfp_client.wait_for_run_completion(workflow_id, 30)
2262
+ if not kfp_run:
2263
+ return None
2264
+
2265
+ kfp_run = mlrun_pipelines.models.PipelineRun(kfp_run)
2266
+ return kfp_run.workflow_manifest()
2267
+
2268
+
2269
+ def as_dict(data: typing.Union[dict, str]) -> dict:
2270
+ if isinstance(data, str):
2271
+ return json.loads(data)
2272
+ return data
2273
+
2274
+
2275
+ def encode_user_code(
2276
+ user_code: typing.Union[str, bytes], max_len_warning: typing.Optional[int] = None
2277
+ ) -> str:
2278
+ max_len_warning = max_len_warning or config.function.spec.source_code_max_bytes
2279
+ if isinstance(user_code, str):
2280
+ user_code = user_code.encode("utf-8")
2281
+ encoded = base64.b64encode(user_code).decode("utf-8")
2282
+ if len(encoded) > max_len_warning:
2283
+ logger.warning(
2284
+ f"User code exceeds the maximum allowed size of {max_len_warning} bytes for non remote source. "
2285
+ "Consider using `with_source_archive` to add user code as a remote source to the function."
2286
+ )
2287
+ return encoded