mlrun 1.10.0rc18__py3-none-any.whl → 1.11.0rc16__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 (167) hide show
  1. mlrun/__init__.py +24 -3
  2. mlrun/__main__.py +0 -4
  3. mlrun/artifacts/dataset.py +2 -2
  4. mlrun/artifacts/document.py +6 -1
  5. mlrun/artifacts/llm_prompt.py +21 -15
  6. mlrun/artifacts/model.py +3 -3
  7. mlrun/artifacts/plots.py +1 -1
  8. mlrun/{model_monitoring/db/tsdb/tdengine → auth}/__init__.py +2 -3
  9. mlrun/auth/nuclio.py +89 -0
  10. mlrun/auth/providers.py +429 -0
  11. mlrun/auth/utils.py +415 -0
  12. mlrun/common/constants.py +14 -0
  13. mlrun/common/model_monitoring/helpers.py +123 -0
  14. mlrun/common/runtimes/constants.py +28 -0
  15. mlrun/common/schemas/__init__.py +14 -3
  16. mlrun/common/schemas/alert.py +2 -2
  17. mlrun/common/schemas/api_gateway.py +3 -0
  18. mlrun/common/schemas/auth.py +12 -10
  19. mlrun/common/schemas/client_spec.py +4 -0
  20. mlrun/common/schemas/constants.py +25 -0
  21. mlrun/common/schemas/frontend_spec.py +1 -8
  22. mlrun/common/schemas/function.py +34 -0
  23. mlrun/common/schemas/hub.py +33 -20
  24. mlrun/common/schemas/model_monitoring/__init__.py +2 -1
  25. mlrun/common/schemas/model_monitoring/constants.py +12 -15
  26. mlrun/common/schemas/model_monitoring/functions.py +13 -4
  27. mlrun/common/schemas/model_monitoring/model_endpoints.py +11 -0
  28. mlrun/common/schemas/pipeline.py +1 -1
  29. mlrun/common/schemas/secret.py +17 -2
  30. mlrun/common/secrets.py +95 -1
  31. mlrun/common/types.py +10 -10
  32. mlrun/config.py +69 -19
  33. mlrun/data_types/infer.py +2 -2
  34. mlrun/datastore/__init__.py +12 -5
  35. mlrun/datastore/azure_blob.py +162 -47
  36. mlrun/datastore/base.py +274 -10
  37. mlrun/datastore/datastore.py +7 -2
  38. mlrun/datastore/datastore_profile.py +84 -22
  39. mlrun/datastore/model_provider/huggingface_provider.py +225 -41
  40. mlrun/datastore/model_provider/mock_model_provider.py +87 -0
  41. mlrun/datastore/model_provider/model_provider.py +206 -74
  42. mlrun/datastore/model_provider/openai_provider.py +226 -66
  43. mlrun/datastore/s3.py +39 -18
  44. mlrun/datastore/sources.py +1 -1
  45. mlrun/datastore/store_resources.py +4 -4
  46. mlrun/datastore/storeytargets.py +17 -12
  47. mlrun/datastore/targets.py +1 -1
  48. mlrun/datastore/utils.py +25 -6
  49. mlrun/datastore/v3io.py +1 -1
  50. mlrun/db/base.py +63 -32
  51. mlrun/db/httpdb.py +373 -153
  52. mlrun/db/nopdb.py +54 -21
  53. mlrun/errors.py +4 -2
  54. mlrun/execution.py +66 -25
  55. mlrun/feature_store/api.py +1 -1
  56. mlrun/feature_store/common.py +1 -1
  57. mlrun/feature_store/feature_vector_utils.py +1 -1
  58. mlrun/feature_store/steps.py +8 -6
  59. mlrun/frameworks/_common/utils.py +3 -3
  60. mlrun/frameworks/_dl_common/loggers/logger.py +1 -1
  61. mlrun/frameworks/_dl_common/loggers/tensorboard_logger.py +2 -1
  62. mlrun/frameworks/_ml_common/loggers/mlrun_logger.py +1 -1
  63. mlrun/frameworks/_ml_common/utils.py +2 -1
  64. mlrun/frameworks/auto_mlrun/auto_mlrun.py +4 -3
  65. mlrun/frameworks/lgbm/mlrun_interfaces/mlrun_interface.py +2 -1
  66. mlrun/frameworks/onnx/dataset.py +2 -1
  67. mlrun/frameworks/onnx/mlrun_interface.py +2 -1
  68. mlrun/frameworks/pytorch/callbacks/logging_callback.py +5 -4
  69. mlrun/frameworks/pytorch/callbacks/mlrun_logging_callback.py +2 -1
  70. mlrun/frameworks/pytorch/callbacks/tensorboard_logging_callback.py +2 -1
  71. mlrun/frameworks/pytorch/utils.py +2 -1
  72. mlrun/frameworks/sklearn/metric.py +2 -1
  73. mlrun/frameworks/tf_keras/callbacks/logging_callback.py +5 -4
  74. mlrun/frameworks/tf_keras/callbacks/mlrun_logging_callback.py +2 -1
  75. mlrun/frameworks/tf_keras/callbacks/tensorboard_logging_callback.py +2 -1
  76. mlrun/hub/__init__.py +52 -0
  77. mlrun/hub/base.py +142 -0
  78. mlrun/hub/module.py +172 -0
  79. mlrun/hub/step.py +113 -0
  80. mlrun/k8s_utils.py +105 -16
  81. mlrun/launcher/base.py +15 -7
  82. mlrun/launcher/local.py +4 -1
  83. mlrun/model.py +14 -4
  84. mlrun/model_monitoring/__init__.py +0 -1
  85. mlrun/model_monitoring/api.py +65 -28
  86. mlrun/model_monitoring/applications/__init__.py +1 -1
  87. mlrun/model_monitoring/applications/base.py +299 -128
  88. mlrun/model_monitoring/applications/context.py +2 -4
  89. mlrun/model_monitoring/controller.py +132 -58
  90. mlrun/model_monitoring/db/_schedules.py +38 -29
  91. mlrun/model_monitoring/db/_stats.py +6 -16
  92. mlrun/model_monitoring/db/tsdb/__init__.py +9 -7
  93. mlrun/model_monitoring/db/tsdb/base.py +29 -9
  94. mlrun/model_monitoring/db/tsdb/preaggregate.py +234 -0
  95. mlrun/model_monitoring/db/tsdb/stream_graph_steps.py +63 -0
  96. mlrun/model_monitoring/db/tsdb/timescaledb/queries/timescaledb_metrics_queries.py +414 -0
  97. mlrun/model_monitoring/db/tsdb/timescaledb/queries/timescaledb_predictions_queries.py +376 -0
  98. mlrun/model_monitoring/db/tsdb/timescaledb/queries/timescaledb_results_queries.py +590 -0
  99. mlrun/model_monitoring/db/tsdb/timescaledb/timescaledb_connection.py +434 -0
  100. mlrun/model_monitoring/db/tsdb/timescaledb/timescaledb_connector.py +541 -0
  101. mlrun/model_monitoring/db/tsdb/timescaledb/timescaledb_operations.py +808 -0
  102. mlrun/model_monitoring/db/tsdb/timescaledb/timescaledb_schema.py +502 -0
  103. mlrun/model_monitoring/db/tsdb/timescaledb/timescaledb_stream.py +163 -0
  104. mlrun/model_monitoring/db/tsdb/timescaledb/timescaledb_stream_graph_steps.py +60 -0
  105. mlrun/model_monitoring/db/tsdb/timescaledb/utils/timescaledb_dataframe_processor.py +141 -0
  106. mlrun/model_monitoring/db/tsdb/timescaledb/utils/timescaledb_query_builder.py +585 -0
  107. mlrun/model_monitoring/db/tsdb/timescaledb/writer_graph_steps.py +73 -0
  108. mlrun/model_monitoring/db/tsdb/v3io/stream_graph_steps.py +20 -9
  109. mlrun/model_monitoring/db/tsdb/v3io/v3io_connector.py +235 -51
  110. mlrun/model_monitoring/features_drift_table.py +2 -1
  111. mlrun/model_monitoring/helpers.py +30 -6
  112. mlrun/model_monitoring/stream_processing.py +34 -28
  113. mlrun/model_monitoring/writer.py +224 -4
  114. mlrun/package/__init__.py +2 -1
  115. mlrun/platforms/__init__.py +0 -43
  116. mlrun/platforms/iguazio.py +8 -4
  117. mlrun/projects/operations.py +17 -11
  118. mlrun/projects/pipelines.py +2 -2
  119. mlrun/projects/project.py +187 -123
  120. mlrun/run.py +95 -21
  121. mlrun/runtimes/__init__.py +2 -186
  122. mlrun/runtimes/base.py +103 -25
  123. mlrun/runtimes/constants.py +225 -0
  124. mlrun/runtimes/daskjob.py +5 -2
  125. mlrun/runtimes/databricks_job/databricks_runtime.py +2 -1
  126. mlrun/runtimes/local.py +5 -2
  127. mlrun/runtimes/mounts.py +20 -2
  128. mlrun/runtimes/nuclio/__init__.py +12 -7
  129. mlrun/runtimes/nuclio/api_gateway.py +36 -6
  130. mlrun/runtimes/nuclio/application/application.py +339 -40
  131. mlrun/runtimes/nuclio/function.py +222 -72
  132. mlrun/runtimes/nuclio/serving.py +132 -42
  133. mlrun/runtimes/pod.py +213 -21
  134. mlrun/runtimes/utils.py +49 -9
  135. mlrun/secrets.py +99 -14
  136. mlrun/serving/__init__.py +2 -0
  137. mlrun/serving/remote.py +84 -11
  138. mlrun/serving/routers.py +26 -44
  139. mlrun/serving/server.py +138 -51
  140. mlrun/serving/serving_wrapper.py +6 -2
  141. mlrun/serving/states.py +997 -283
  142. mlrun/serving/steps.py +62 -0
  143. mlrun/serving/system_steps.py +149 -95
  144. mlrun/serving/v2_serving.py +9 -10
  145. mlrun/track/trackers/mlflow_tracker.py +29 -31
  146. mlrun/utils/helpers.py +292 -94
  147. mlrun/utils/http.py +9 -2
  148. mlrun/utils/notifications/notification/base.py +18 -0
  149. mlrun/utils/notifications/notification/git.py +3 -5
  150. mlrun/utils/notifications/notification/mail.py +39 -16
  151. mlrun/utils/notifications/notification/slack.py +2 -4
  152. mlrun/utils/notifications/notification/webhook.py +2 -5
  153. mlrun/utils/notifications/notification_pusher.py +3 -3
  154. mlrun/utils/version/version.json +2 -2
  155. mlrun/utils/version/version.py +3 -4
  156. {mlrun-1.10.0rc18.dist-info → mlrun-1.11.0rc16.dist-info}/METADATA +63 -74
  157. {mlrun-1.10.0rc18.dist-info → mlrun-1.11.0rc16.dist-info}/RECORD +161 -143
  158. mlrun/api/schemas/__init__.py +0 -259
  159. mlrun/db/auth_utils.py +0 -152
  160. mlrun/model_monitoring/db/tsdb/tdengine/schemas.py +0 -344
  161. mlrun/model_monitoring/db/tsdb/tdengine/stream_graph_steps.py +0 -75
  162. mlrun/model_monitoring/db/tsdb/tdengine/tdengine_connection.py +0 -281
  163. mlrun/model_monitoring/db/tsdb/tdengine/tdengine_connector.py +0 -1266
  164. {mlrun-1.10.0rc18.dist-info → mlrun-1.11.0rc16.dist-info}/WHEEL +0 -0
  165. {mlrun-1.10.0rc18.dist-info → mlrun-1.11.0rc16.dist-info}/entry_points.txt +0 -0
  166. {mlrun-1.10.0rc18.dist-info → mlrun-1.11.0rc16.dist-info}/licenses/LICENSE +0 -0
  167. {mlrun-1.10.0rc18.dist-info → mlrun-1.11.0rc16.dist-info}/top_level.txt +0 -0
mlrun/runtimes/utils.py CHANGED
@@ -26,6 +26,7 @@ import pandas as pd
26
26
  import mlrun
27
27
  import mlrun.common.constants
28
28
  import mlrun.common.constants as mlrun_constants
29
+ import mlrun.common.runtimes.constants
29
30
  import mlrun.common.schemas
30
31
  import mlrun.utils.regex
31
32
  from mlrun.artifacts import TableArtifact
@@ -153,6 +154,7 @@ def results_to_iter(results, runspec, execution):
153
154
 
154
155
  iter = []
155
156
  failed = 0
157
+ pending_retry = 0
156
158
  running = 0
157
159
  for task in results:
158
160
  if task:
@@ -164,17 +166,26 @@ def results_to_iter(results, runspec, execution):
164
166
  "state": state,
165
167
  "iter": id,
166
168
  }
167
- if state == "error":
169
+ if state == mlrun.common.runtimes.constants.RunStates.error:
168
170
  failed += 1
169
171
  err = get_in(task, ["status", "error"], "")
170
- logger.error(f"error in task {execution.uid}:{id} - {err_to_str(err)}")
171
- elif state != "completed":
172
+ logger.error(f"error in task {execution.uid}:{id} - {err_to_str(err)}")
173
+ elif state == mlrun.common.runtimes.constants.RunStates.pending_retry:
174
+ pending_retry += 1
175
+ err = get_in(task, ["status", "error"], "")
176
+ retry_count = get_in(task, ["status", "retry_count"], 0)
177
+ logger.warning(
178
+ f"pending retry in task {execution.uid}:{id} - {err_to_str(err)}. Retry count: {retry_count}"
179
+ )
180
+ elif state != mlrun.common.runtimes.constants.RunStates.completed:
172
181
  running += 1
173
182
 
174
183
  iter.append(struct)
175
184
 
176
185
  if not iter:
177
- execution.set_state("completed", commit=True)
186
+ execution.set_state(
187
+ mlrun.common.runtimes.constants.RunStates.completed, commit=True
188
+ )
178
189
  logger.warning("Warning!, zero iteration results")
179
190
  return
180
191
  if hasattr(pd, "json_normalize"):
@@ -204,8 +215,14 @@ def results_to_iter(results, runspec, execution):
204
215
  error=f"{failed} of {len(results)} tasks failed, check logs in db for details",
205
216
  commit=False,
206
217
  )
218
+ elif pending_retry:
219
+ execution.set_state(
220
+ mlrun.common.runtimes.constants.RunStates.pending_retry, commit=False
221
+ )
207
222
  elif running == 0:
208
- execution.set_state("completed", commit=False)
223
+ execution.set_state(
224
+ mlrun.common.runtimes.constants.RunStates.completed, commit=False
225
+ )
209
226
  execution.commit()
210
227
 
211
228
 
@@ -431,22 +448,45 @@ def enrich_function_from_dict(function, function_dict):
431
448
  return function
432
449
 
433
450
 
451
+ def resolve_owner(
452
+ labels: dict,
453
+ owner_to_enrich: Optional[str] = None,
454
+ ):
455
+ """
456
+ Resolve the owner label value
457
+ :param labels: The run labels dict
458
+ :param auth_username: The authenticated username
459
+ :return: The resolved owner label value
460
+ """
461
+
462
+ if owner_to_enrich and (
463
+ labels.get("job-type") == mlrun.common.constants.JOB_TYPE_WORKFLOW_RUNNER
464
+ or labels.get("job-type")
465
+ == mlrun.common.constants.JOB_TYPE_RERUN_WORKFLOW_RUNNER
466
+ ):
467
+ return owner_to_enrich
468
+ else:
469
+ return os.environ.get("V3IO_USERNAME") or getpass.getuser()
470
+
471
+
434
472
  def enrich_run_labels(
435
473
  labels: dict,
436
474
  labels_to_enrich: Optional[list[mlrun_constants.MLRunInternalLabels]] = None,
475
+ owner_to_enrich: Optional[str] = None,
437
476
  ):
438
477
  """
439
- Enrich the run labels with the internal labels and the labels enrichment extension
478
+ Enrich the run labels with the internal labels and the labels enrichment extension.
440
479
  :param labels: The run labels dict
441
480
  :param labels_to_enrich: The label keys to enrich from MLRunInternalLabels.default_run_labels_to_enrich
481
+ :param owner_to_enrich: Optional owner to enrich the labels with, if not provided will try to resolve it.
442
482
  :return: The enriched labels dict
443
483
  """
444
484
  # Merge the labels with the labels enrichment extension
445
485
  labels_enrichment = {
446
- mlrun_constants.MLRunInternalLabels.owner: os.environ.get("V3IO_USERNAME")
447
- or getpass.getuser(),
486
+ mlrun_constants.MLRunInternalLabels.owner: resolve_owner(
487
+ labels, owner_to_enrich
488
+ ),
448
489
  }
449
-
450
490
  # Resolve which label keys to enrich
451
491
  if labels_to_enrich is None:
452
492
  labels_to_enrich = (
mlrun/secrets.py CHANGED
@@ -11,10 +11,16 @@
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 json
15
+ import os
15
16
  from ast import literal_eval
16
- from os import environ, getenv
17
- from typing import Callable, Optional, Union
17
+ from collections.abc import Callable
18
+ from os import environ
19
+ from typing import Optional, Union
20
+
21
+ import mlrun.auth.utils
22
+ import mlrun.utils.helpers
23
+ from mlrun.config import is_running_as_api
18
24
 
19
25
  from .utils import AzureVaultStore, list2dict
20
26
 
@@ -48,6 +54,11 @@ class SecretsStore:
48
54
  self._secrets[prefix + k] = str(v)
49
55
 
50
56
  elif kind == "file":
57
+ # Ensure files cannot be open from inside the API
58
+ if is_running_as_api():
59
+ raise RuntimeError(
60
+ "add_source of kind 'file' is not allowed from the API"
61
+ )
51
62
  with open(source) as fp:
52
63
  lines = fp.read().splitlines()
53
64
  secrets_dict = list2dict(lines)
@@ -161,6 +172,9 @@ def get_secret_or_env(
161
172
  4. An MLRun-generated env. variable, mounted from a project secret (to be used in MLRun runtimes)
162
173
  5. The default value
163
174
 
175
+ Also supports discovering the value inside any environment variable that contains a JSON-encoded list
176
+ of dicts with fields: {'name': 'KEY', 'value': 'VAL', 'value_from': ...}. This fallback is applied
177
+ after checking normal environment variables and before returning the default.
164
178
  Example::
165
179
 
166
180
  secrets = {"KEY1": "VALUE1"}
@@ -187,18 +201,89 @@ def get_secret_or_env(
187
201
  if prefix:
188
202
  key = f"{prefix}_{key}"
189
203
 
190
- value = None
191
204
  if secret_provider:
192
- if isinstance(secret_provider, (dict, SecretsStore)):
193
- value = secret_provider.get(key)
205
+ if isinstance(secret_provider, dict | SecretsStore):
206
+ secret_value = secret_provider.get(key)
194
207
  else:
195
- value = secret_provider(key)
196
- if value:
197
- return value
208
+ secret_value = secret_provider(key)
209
+ if secret_value:
210
+ return secret_value
211
+
212
+ direct_environment_value = environ.get(key)
213
+ if direct_environment_value:
214
+ return direct_environment_value
215
+
216
+ json_list_value = _find_value_in_json_env_lists(key)
217
+ if json_list_value is not None:
218
+ return json_list_value
198
219
 
199
- return (
200
- value
201
- or getenv(key)
202
- or getenv(SecretsStore.k8s_env_variable_name_for_secret(key))
203
- or default
220
+ mlrun_env_key = SecretsStore.k8s_env_variable_name_for_secret(key)
221
+ mlrun_env_value = environ.get(mlrun_env_key)
222
+ if mlrun_env_value:
223
+ return mlrun_env_value
224
+
225
+ return default
226
+
227
+
228
+ def _find_value_in_json_env_lists(
229
+ secret_name: str,
230
+ ) -> Optional[str]:
231
+ """
232
+ Scan all environment variables. If any env var contains a JSON-encoded list
233
+ of dicts shaped like {'name': str, 'value': str|None, 'value_from': ...},
234
+ return the 'value' for the entry whose 'name' matches secret_name.
235
+ """
236
+ for environment_variable_value in environ.values():
237
+ if not environment_variable_value or not isinstance(
238
+ environment_variable_value, str
239
+ ):
240
+ continue
241
+ # Fast precheck to skip obvious non-JSON strings
242
+ first_char = environment_variable_value.lstrip()[:1]
243
+ if first_char not in ("[", "{"):
244
+ continue
245
+ try:
246
+ parsed_value = json.loads(environment_variable_value)
247
+ except ValueError:
248
+ continue
249
+ if isinstance(parsed_value, list):
250
+ for entry in parsed_value:
251
+ if isinstance(entry, dict) and entry.get("name") == secret_name:
252
+ value_in_entry = entry.get("value")
253
+ # Match original semantics: empty string is treated as "not found"
254
+ if value_in_entry:
255
+ return value_in_entry
256
+ return None
257
+
258
+
259
+ @mlrun.utils.iguazio_v4_only
260
+ def sync_secret_tokens() -> None:
261
+ """
262
+ Synchronize local secret tokens with the backend. Doesn't sync when running from a runtime.
263
+
264
+ This function:
265
+ 1. Reads the local token file (defaults to `mlrun.mlconf.auth_with_oauth_token.token_file` value).
266
+ 2. Validates its content and converts validated tokens into `SecretToken` objects.
267
+ 3. Uploads the tokens to the backend.
268
+ 4. Logs a warning if any tokens were updated on the backend due to newer
269
+ expiration times found locally.
270
+ """
271
+
272
+ # Do not sync tokens from the file when using the offline token environment variable.
273
+ # The offline token from the env var takes precedence over the file.
274
+ # Using the env var is not the recommended approach, and tokens from the env var
275
+ # will not be saved as secrets in the backend.
276
+ if os.getenv("MLRUN_AUTH_OFFLINE_TOKEN") or mlrun.utils.is_running_in_runtime():
277
+ return
278
+
279
+ # The import is needed here to prevent a circular import, since this method is called from the mlrun.db connection.
280
+ from mlrun.db import get_run_db
281
+
282
+ secret_tokens = mlrun.auth.utils.load_and_prepare_secret_tokens(
283
+ auth_user_id=get_run_db().token_provider.authenticated_user_id
204
284
  )
285
+
286
+ # The log_warning=False flag ensures the SDK doesn't log
287
+ # unnecessary warnings about local file updates, since
288
+ # this method reads from the file, not updates it.
289
+ get_run_db().store_secret_tokens(secret_tokens, log_warning=False)
mlrun/serving/__init__.py CHANGED
@@ -27,6 +27,7 @@ __all__ = [
27
27
  "ModelRunner",
28
28
  "Model",
29
29
  "ModelSelector",
30
+ "ModelRunnerSelector",
30
31
  "MonitoredStep",
31
32
  "LLModel",
32
33
  ]
@@ -47,6 +48,7 @@ from .states import (
47
48
  ModelRunner,
48
49
  Model,
49
50
  ModelSelector,
51
+ ModelRunnerSelector,
50
52
  MonitoredStep,
51
53
  LLModel,
52
54
  ) # noqa
mlrun/serving/remote.py CHANGED
@@ -23,10 +23,14 @@ import storey
23
23
  from storey.flow import _ConcurrentJobExecution
24
24
 
25
25
  import mlrun
26
+ import mlrun.common.schemas
26
27
  import mlrun.config
28
+ import mlrun.platforms
29
+ import mlrun.utils.async_http
27
30
  from mlrun.errors import err_to_str
28
- from mlrun.utils import logger
31
+ from mlrun.utils import dict_to_json, logger
29
32
 
33
+ from ..config import config
30
34
  from .utils import (
31
35
  _extract_input_data,
32
36
  _update_result_body,
@@ -73,7 +77,9 @@ class RemoteStep(storey.SendToHttp):
73
77
 
74
78
  :param url: http(s) url or function [project/]name to call
75
79
  :param subpath: path (which follows the url), use `$path` to use the event.path
76
- :param method: HTTP method (GET, POST, ..), default to POST
80
+ :param method: The HTTP method to use for the request (e.g., "GET", "POST", "PUT", "DELETE").
81
+ If not provided, the step will try to use `event.method` at runtime, and if that
82
+ is also missing, it defaults to `"POST"`.
77
83
  :param headers: dictionary with http header values
78
84
  :param url_expression: an expression for getting the url from the event, e.g. "event['url']"
79
85
  :param body_expression: an expression for getting the request body from the event, e.g. "event['data']"
@@ -150,8 +156,8 @@ class RemoteStep(storey.SendToHttp):
150
156
  async def _process_event(self, event):
151
157
  # async implementation (with storey)
152
158
  body = self._get_event_or_body(event)
153
- method, url, headers, body = self._generate_request(event, body)
154
- kwargs = {}
159
+ method, url, headers, body, kwargs = self._generate_request(event, body)
160
+ kwargs = kwargs or {}
155
161
  if self.timeout:
156
162
  kwargs["timeout"] = aiohttp.ClientTimeout(total=self.timeout)
157
163
  try:
@@ -162,7 +168,7 @@ class RemoteStep(storey.SendToHttp):
162
168
  text = await resp.text()
163
169
  raise RuntimeError(f"bad http response {resp.status}: {text}")
164
170
  return resp
165
- except asyncio.TimeoutError as exc:
171
+ except TimeoutError as exc:
166
172
  logger.error(f"http request to {url} timed out in RemoteStep {self.name}")
167
173
  raise exc
168
174
 
@@ -191,7 +197,7 @@ class RemoteStep(storey.SendToHttp):
191
197
  )
192
198
 
193
199
  body = _extract_input_data(self._input_path, event.body)
194
- method, url, headers, body = self._generate_request(event, body)
200
+ method, url, headers, body, kwargs = self._generate_request(event, body)
195
201
  try:
196
202
  resp = self._session.request(
197
203
  method,
@@ -200,6 +206,7 @@ class RemoteStep(storey.SendToHttp):
200
206
  headers=headers,
201
207
  data=body,
202
208
  timeout=self.timeout,
209
+ **kwargs,
203
210
  )
204
211
  except requests.exceptions.ReadTimeout as err:
205
212
  raise requests.exceptions.ReadTimeout(
@@ -234,19 +241,19 @@ class RemoteStep(storey.SendToHttp):
234
241
  headers[event_id_key] = event.id
235
242
  if method == "GET":
236
243
  body = None
237
- elif body is not None and not isinstance(body, (str, bytes)):
244
+ elif body is not None and not isinstance(body, str | bytes):
238
245
  if self._body_function_handler:
239
246
  body = self._body_function_handler(body)
240
247
  body = json.dumps(body)
241
248
  headers["Content-Type"] = "application/json"
242
249
 
243
- return method, url, headers, body
250
+ return method, url, headers, body, {}
244
251
 
245
252
  def _get_data(self, data, headers):
246
253
  if (
247
254
  self.return_json
248
255
  or headers.get("content-type", "").lower() == "application/json"
249
- ) and isinstance(data, (str, bytes)):
256
+ ) and isinstance(data, str | bytes):
250
257
  data = json.loads(data)
251
258
  return data
252
259
 
@@ -383,7 +390,7 @@ class BatchHttpRequests(_ConcurrentJobExecution):
383
390
 
384
391
  if is_get:
385
392
  body = None
386
- elif body is not None and not isinstance(body, (str, bytes)):
393
+ elif body is not None and not isinstance(body, str | bytes):
387
394
  if self._body_function_handler:
388
395
  body = self._body_function_handler(body)
389
396
  body = json.dumps(body)
@@ -451,6 +458,72 @@ class BatchHttpRequests(_ConcurrentJobExecution):
451
458
  if (
452
459
  self.return_json
453
460
  or headers.get("content-type", "").lower() == "application/json"
454
- ) and isinstance(data, (str, bytes)):
461
+ ) and isinstance(data, str | bytes):
455
462
  data = json.loads(data)
456
463
  return data
464
+
465
+
466
+ class MLRunAPIRemoteStep(RemoteStep):
467
+ def __init__(
468
+ self, method: str, path: str, fill_placeholders: Optional[bool] = None, **kwargs
469
+ ):
470
+ """
471
+ Graph step implementation for calling MLRun API endpoints
472
+
473
+ :param method: The HTTP method to use for the request (e.g., "GET", "POST", "PUT", "DELETE").
474
+ If not provided, the step will try to use `event.method` at runtime, and if that
475
+ is also missing, it defaults to `"POST"`.
476
+ :param path: API path (e.g. /api/projects)
477
+ :param fill_placeholders: if True, fill placeholders in the path using event fields (default to False)
478
+ :param kwargs: other arguments passed to RemoteStep
479
+ """
480
+ super().__init__(url="", method=method, **kwargs)
481
+ self.rundb = None
482
+ self.path = path
483
+ self.fill_placeholders = fill_placeholders
484
+
485
+ def _generate_request(self, event, body):
486
+ method = self.method or event.method or "POST"
487
+ kw = {
488
+ key: value
489
+ for key, value in (
490
+ ("params", body.get("params")),
491
+ ("json", body.get("json")),
492
+ )
493
+ if value is not None
494
+ }
495
+
496
+ headers = self.headers or {}
497
+ headers.update(body.get("headers", {}))
498
+
499
+ if self.rundb.user:
500
+ kw["auth"] = (self.rundb.user, self.rundb.password)
501
+ elif self.rundb.token_provider:
502
+ token = self.rundb.token_provider.get_token()
503
+ if token:
504
+ # Iguazio auth doesn't support passing token through bearer, so use cookie instead
505
+ if self.rundb.token_provider.is_iguazio_session():
506
+ session_cookie = f'session=j:{{"sid": "{token}"}}'
507
+ headers["cookie"] = session_cookie
508
+ else:
509
+ if "Authorization" not in kw.setdefault("headers", {}):
510
+ headers.update({"Authorization": "Bearer " + token})
511
+
512
+ if mlrun.common.schemas.HeaderNames.client_version not in headers:
513
+ headers.update(
514
+ {
515
+ mlrun.common.schemas.HeaderNames.client_version: self.rundb.client_version,
516
+ mlrun.common.schemas.HeaderNames.python_version: self.rundb.python_version,
517
+ "User-Agent": f"{requests.utils.default_user_agent()} mlrun/{config.version}",
518
+ }
519
+ )
520
+
521
+ url = self.url.format(**body) if self.fill_placeholders else self.url
522
+ headers["Content-Type"] = "application/json"
523
+ return method, url, headers, dict_to_json(body), kw
524
+
525
+ def post_init(self, mode="sync", **kwargs):
526
+ super().post_init(mode=mode, **kwargs)
527
+ self.fill_placeholders = self.fill_placeholders or False
528
+ self.rundb = mlrun.get_run_db()
529
+ self.url = self.rundb.get_base_api_url(self.path)
mlrun/serving/routers.py CHANGED
@@ -31,6 +31,9 @@ import mlrun.common.model_monitoring
31
31
  import mlrun.common.schemas.model_monitoring
32
32
  from mlrun.utils import logger, now_date
33
33
 
34
+ from ..common.model_monitoring.helpers import (
35
+ get_model_endpoints_creation_task_status,
36
+ )
34
37
  from .utils import RouterToDict, _extract_input_data, _update_result_body
35
38
  from .v2_serving import _ModelLogPusher
36
39
 
@@ -171,46 +174,6 @@ class BaseModelRouter(RouterToDict):
171
174
  """run tasks after processing the event"""
172
175
  return event
173
176
 
174
- def _get_background_task_status(
175
- self,
176
- ) -> mlrun.common.schemas.BackgroundTaskState:
177
- self._background_task_check_timestamp = now_date()
178
- server: mlrun.serving.GraphServer = getattr(
179
- self.context, "_server", None
180
- ) or getattr(self.context, "server", None)
181
- if not self.context.is_mock:
182
- if server.model_endpoint_creation_task_name:
183
- background_task = mlrun.get_run_db().get_project_background_task(
184
- server.project, server.model_endpoint_creation_task_name
185
- )
186
- logger.debug(
187
- "Checking model endpoint creation task status",
188
- task_name=server.model_endpoint_creation_task_name,
189
- )
190
- if (
191
- background_task.status.state
192
- in mlrun.common.schemas.BackgroundTaskState.terminal_states()
193
- ):
194
- logger.info(
195
- f"Model endpoint creation task completed with state {background_task.status.state}"
196
- )
197
- else: # in progress
198
- logger.info(
199
- f"Model endpoint creation task is still in progress with the current state: "
200
- f"{background_task.status.state}. Events will not be monitored for the next "
201
- f"{mlrun.mlconf.model_endpoint_monitoring.model_endpoint_creation_check_period} seconds",
202
- name=self.name,
203
- background_task_check_timestamp=self._background_task_check_timestamp.isoformat(),
204
- )
205
- return background_task.status.state
206
- else:
207
- logger.error(
208
- "Model endpoint creation task name not provided. This function is not being monitored.",
209
- )
210
- elif self.context.monitoring_mock:
211
- return mlrun.common.schemas.BackgroundTaskState.succeeded
212
- return mlrun.common.schemas.BackgroundTaskState.failed
213
-
214
177
  def _update_background_task_state(self, event):
215
178
  if not self.background_task_reached_terminal_state and (
216
179
  self._background_task_check_timestamp is None
@@ -219,7 +182,26 @@ class BaseModelRouter(RouterToDict):
219
182
  seconds=mlrun.mlconf.model_endpoint_monitoring.model_endpoint_creation_check_period
220
183
  )
221
184
  ):
222
- self._background_task_current_state = self._get_background_task_status()
185
+ server: mlrun.serving.GraphServer = getattr(
186
+ self.context, "_server", None
187
+ ) or getattr(self.context, "server", None)
188
+ if not self.context.is_mock:
189
+ (
190
+ self._background_task_current_state,
191
+ self._background_task_check_timestamp,
192
+ _,
193
+ ) = get_model_endpoints_creation_task_status(server)
194
+ elif self.context.monitoring_mock:
195
+ self._background_task_current_state = (
196
+ mlrun.common.schemas.BackgroundTaskState.succeeded
197
+ )
198
+ self._background_task_check_timestamp = mlrun.utils.now_date()
199
+ else:
200
+ self._background_task_current_state = (
201
+ mlrun.common.schemas.BackgroundTaskState.failed
202
+ )
203
+ self._background_task_check_timestamp = mlrun.utils.now_date()
204
+
223
205
  if event.body:
224
206
  event.body["background_task_state"] = (
225
207
  self._background_task_current_state
@@ -1004,7 +986,7 @@ class VotingEnsemble(ParallelRun):
1004
986
  List
1005
987
  The model's predictions
1006
988
  """
1007
- if isinstance(response, (list, numpy.ndarray)):
989
+ if isinstance(response, list | numpy.ndarray):
1008
990
  return response
1009
991
  try:
1010
992
  self.format_response_with_col_name_flag = True
@@ -1141,7 +1123,7 @@ class EnrichmentModelRouter(ModelRouter):
1141
1123
 
1142
1124
  def preprocess(self, event):
1143
1125
  """Turn an entity identifier (source) to a Feature Vector"""
1144
- if isinstance(event.body, (str, bytes)):
1126
+ if isinstance(event.body, str | bytes):
1145
1127
  event.body = json.loads(event.body)
1146
1128
  event.body["inputs"] = self._feature_service.get(
1147
1129
  event.body["inputs"], as_list=True
@@ -1293,7 +1275,7 @@ class EnrichmentVotingEnsemble(VotingEnsemble):
1293
1275
  """
1294
1276
  Turn an entity identifier (source) to a Feature Vector
1295
1277
  """
1296
- if isinstance(event.body, (str, bytes)):
1278
+ if isinstance(event.body, str | bytes):
1297
1279
  event.body = json.loads(event.body)
1298
1280
  event.body["inputs"] = self._feature_service.get(
1299
1281
  event.body["inputs"], as_list=True