mlrun 1.10.0rc40__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 (150) hide show
  1. mlrun/__init__.py +3 -2
  2. mlrun/__main__.py +0 -4
  3. mlrun/artifacts/dataset.py +2 -2
  4. mlrun/artifacts/plots.py +1 -1
  5. mlrun/{model_monitoring/db/tsdb/tdengine → auth}/__init__.py +2 -3
  6. mlrun/auth/nuclio.py +89 -0
  7. mlrun/auth/providers.py +429 -0
  8. mlrun/auth/utils.py +415 -0
  9. mlrun/common/constants.py +7 -0
  10. mlrun/common/model_monitoring/helpers.py +41 -4
  11. mlrun/common/runtimes/constants.py +28 -0
  12. mlrun/common/schemas/__init__.py +13 -3
  13. mlrun/common/schemas/alert.py +2 -2
  14. mlrun/common/schemas/api_gateway.py +3 -0
  15. mlrun/common/schemas/auth.py +10 -10
  16. mlrun/common/schemas/client_spec.py +4 -0
  17. mlrun/common/schemas/constants.py +25 -0
  18. mlrun/common/schemas/frontend_spec.py +1 -8
  19. mlrun/common/schemas/function.py +24 -0
  20. mlrun/common/schemas/hub.py +3 -2
  21. mlrun/common/schemas/model_monitoring/__init__.py +1 -1
  22. mlrun/common/schemas/model_monitoring/constants.py +2 -2
  23. mlrun/common/schemas/secret.py +17 -2
  24. mlrun/common/secrets.py +95 -1
  25. mlrun/common/types.py +10 -10
  26. mlrun/config.py +53 -15
  27. mlrun/data_types/infer.py +2 -2
  28. mlrun/datastore/__init__.py +2 -3
  29. mlrun/datastore/base.py +274 -10
  30. mlrun/datastore/datastore.py +1 -1
  31. mlrun/datastore/datastore_profile.py +49 -17
  32. mlrun/datastore/model_provider/huggingface_provider.py +6 -2
  33. mlrun/datastore/model_provider/model_provider.py +2 -2
  34. mlrun/datastore/model_provider/openai_provider.py +2 -2
  35. mlrun/datastore/s3.py +15 -16
  36. mlrun/datastore/sources.py +1 -1
  37. mlrun/datastore/store_resources.py +4 -4
  38. mlrun/datastore/storeytargets.py +16 -10
  39. mlrun/datastore/targets.py +1 -1
  40. mlrun/datastore/utils.py +16 -3
  41. mlrun/datastore/v3io.py +1 -1
  42. mlrun/db/base.py +36 -12
  43. mlrun/db/httpdb.py +316 -101
  44. mlrun/db/nopdb.py +29 -11
  45. mlrun/errors.py +4 -2
  46. mlrun/execution.py +11 -12
  47. mlrun/feature_store/api.py +1 -1
  48. mlrun/feature_store/common.py +1 -1
  49. mlrun/feature_store/feature_vector_utils.py +1 -1
  50. mlrun/feature_store/steps.py +8 -6
  51. mlrun/frameworks/_common/utils.py +3 -3
  52. mlrun/frameworks/_dl_common/loggers/logger.py +1 -1
  53. mlrun/frameworks/_dl_common/loggers/tensorboard_logger.py +2 -1
  54. mlrun/frameworks/_ml_common/loggers/mlrun_logger.py +1 -1
  55. mlrun/frameworks/_ml_common/utils.py +2 -1
  56. mlrun/frameworks/auto_mlrun/auto_mlrun.py +4 -3
  57. mlrun/frameworks/lgbm/mlrun_interfaces/mlrun_interface.py +2 -1
  58. mlrun/frameworks/onnx/dataset.py +2 -1
  59. mlrun/frameworks/onnx/mlrun_interface.py +2 -1
  60. mlrun/frameworks/pytorch/callbacks/logging_callback.py +5 -4
  61. mlrun/frameworks/pytorch/callbacks/mlrun_logging_callback.py +2 -1
  62. mlrun/frameworks/pytorch/callbacks/tensorboard_logging_callback.py +2 -1
  63. mlrun/frameworks/pytorch/utils.py +2 -1
  64. mlrun/frameworks/sklearn/metric.py +2 -1
  65. mlrun/frameworks/tf_keras/callbacks/logging_callback.py +5 -4
  66. mlrun/frameworks/tf_keras/callbacks/mlrun_logging_callback.py +2 -1
  67. mlrun/frameworks/tf_keras/callbacks/tensorboard_logging_callback.py +2 -1
  68. mlrun/hub/__init__.py +37 -0
  69. mlrun/hub/base.py +142 -0
  70. mlrun/hub/module.py +67 -76
  71. mlrun/hub/step.py +113 -0
  72. mlrun/launcher/base.py +2 -1
  73. mlrun/launcher/local.py +2 -1
  74. mlrun/model.py +12 -2
  75. mlrun/model_monitoring/__init__.py +0 -1
  76. mlrun/model_monitoring/api.py +2 -2
  77. mlrun/model_monitoring/applications/base.py +20 -6
  78. mlrun/model_monitoring/applications/context.py +1 -0
  79. mlrun/model_monitoring/controller.py +7 -17
  80. mlrun/model_monitoring/db/_schedules.py +2 -16
  81. mlrun/model_monitoring/db/_stats.py +2 -13
  82. mlrun/model_monitoring/db/tsdb/__init__.py +9 -7
  83. mlrun/model_monitoring/db/tsdb/base.py +2 -4
  84. mlrun/model_monitoring/db/tsdb/preaggregate.py +234 -0
  85. mlrun/model_monitoring/db/tsdb/stream_graph_steps.py +63 -0
  86. mlrun/model_monitoring/db/tsdb/timescaledb/queries/timescaledb_metrics_queries.py +414 -0
  87. mlrun/model_monitoring/db/tsdb/timescaledb/queries/timescaledb_predictions_queries.py +376 -0
  88. mlrun/model_monitoring/db/tsdb/timescaledb/queries/timescaledb_results_queries.py +590 -0
  89. mlrun/model_monitoring/db/tsdb/timescaledb/timescaledb_connection.py +434 -0
  90. mlrun/model_monitoring/db/tsdb/timescaledb/timescaledb_connector.py +541 -0
  91. mlrun/model_monitoring/db/tsdb/timescaledb/timescaledb_operations.py +808 -0
  92. mlrun/model_monitoring/db/tsdb/timescaledb/timescaledb_schema.py +502 -0
  93. mlrun/model_monitoring/db/tsdb/timescaledb/timescaledb_stream.py +163 -0
  94. mlrun/model_monitoring/db/tsdb/timescaledb/timescaledb_stream_graph_steps.py +60 -0
  95. mlrun/model_monitoring/db/tsdb/timescaledb/utils/timescaledb_dataframe_processor.py +141 -0
  96. mlrun/model_monitoring/db/tsdb/timescaledb/utils/timescaledb_query_builder.py +585 -0
  97. mlrun/model_monitoring/db/tsdb/timescaledb/writer_graph_steps.py +73 -0
  98. mlrun/model_monitoring/db/tsdb/v3io/stream_graph_steps.py +4 -6
  99. mlrun/model_monitoring/db/tsdb/v3io/v3io_connector.py +147 -79
  100. mlrun/model_monitoring/features_drift_table.py +2 -1
  101. mlrun/model_monitoring/helpers.py +2 -1
  102. mlrun/model_monitoring/stream_processing.py +18 -16
  103. mlrun/model_monitoring/writer.py +4 -3
  104. mlrun/package/__init__.py +2 -1
  105. mlrun/platforms/__init__.py +0 -44
  106. mlrun/platforms/iguazio.py +1 -1
  107. mlrun/projects/operations.py +11 -10
  108. mlrun/projects/project.py +81 -82
  109. mlrun/run.py +4 -7
  110. mlrun/runtimes/__init__.py +2 -204
  111. mlrun/runtimes/base.py +89 -21
  112. mlrun/runtimes/constants.py +225 -0
  113. mlrun/runtimes/daskjob.py +4 -2
  114. mlrun/runtimes/databricks_job/databricks_runtime.py +2 -1
  115. mlrun/runtimes/mounts.py +5 -0
  116. mlrun/runtimes/nuclio/__init__.py +12 -8
  117. mlrun/runtimes/nuclio/api_gateway.py +36 -6
  118. mlrun/runtimes/nuclio/application/application.py +200 -32
  119. mlrun/runtimes/nuclio/function.py +154 -49
  120. mlrun/runtimes/nuclio/serving.py +55 -42
  121. mlrun/runtimes/pod.py +59 -10
  122. mlrun/secrets.py +46 -2
  123. mlrun/serving/__init__.py +2 -0
  124. mlrun/serving/remote.py +5 -5
  125. mlrun/serving/routers.py +3 -3
  126. mlrun/serving/server.py +46 -43
  127. mlrun/serving/serving_wrapper.py +6 -2
  128. mlrun/serving/states.py +554 -207
  129. mlrun/serving/steps.py +1 -1
  130. mlrun/serving/system_steps.py +42 -33
  131. mlrun/track/trackers/mlflow_tracker.py +29 -31
  132. mlrun/utils/helpers.py +89 -16
  133. mlrun/utils/http.py +9 -2
  134. mlrun/utils/notifications/notification/git.py +1 -1
  135. mlrun/utils/notifications/notification/mail.py +39 -16
  136. mlrun/utils/notifications/notification_pusher.py +2 -2
  137. mlrun/utils/version/version.json +2 -2
  138. mlrun/utils/version/version.py +3 -4
  139. {mlrun-1.10.0rc40.dist-info → mlrun-1.11.0rc16.dist-info}/METADATA +39 -49
  140. {mlrun-1.10.0rc40.dist-info → mlrun-1.11.0rc16.dist-info}/RECORD +144 -130
  141. mlrun/db/auth_utils.py +0 -152
  142. mlrun/model_monitoring/db/tsdb/tdengine/schemas.py +0 -343
  143. mlrun/model_monitoring/db/tsdb/tdengine/stream_graph_steps.py +0 -75
  144. mlrun/model_monitoring/db/tsdb/tdengine/tdengine_connection.py +0 -281
  145. mlrun/model_monitoring/db/tsdb/tdengine/tdengine_connector.py +0 -1368
  146. mlrun/model_monitoring/db/tsdb/tdengine/writer_graph_steps.py +0 -51
  147. {mlrun-1.10.0rc40.dist-info → mlrun-1.11.0rc16.dist-info}/WHEEL +0 -0
  148. {mlrun-1.10.0rc40.dist-info → mlrun-1.11.0rc16.dist-info}/entry_points.txt +0 -0
  149. {mlrun-1.10.0rc40.dist-info → mlrun-1.11.0rc16.dist-info}/licenses/LICENSE +0 -0
  150. {mlrun-1.10.0rc40.dist-info → mlrun-1.11.0rc16.dist-info}/top_level.txt +0 -0
mlrun/runtimes/pod.py CHANGED
@@ -20,12 +20,14 @@ import typing
20
20
  import warnings
21
21
  from collections.abc import Iterable
22
22
  from enum import Enum
23
+ from typing import Optional
23
24
 
24
25
  import dotenv
25
26
  import kubernetes.client as k8s_client
26
27
  from kubernetes.client import V1Volume, V1VolumeMount
27
28
 
28
29
  import mlrun.common.constants
30
+ import mlrun.common.secrets
29
31
  import mlrun.errors
30
32
  import mlrun.runtimes.mounts
31
33
  import mlrun.utils.regex
@@ -708,19 +710,45 @@ class KubeResource(BaseRuntime):
708
710
  def spec(self, spec):
709
711
  self._spec = self._verify_dict(spec, "spec", KubeResourceSpec)
710
712
 
711
- def set_env_from_secret(self, name, secret=None, secret_key=None):
712
- """set pod environment var from secret"""
713
- secret_key = secret_key or name
713
+ def set_env_from_secret(
714
+ self,
715
+ name: str,
716
+ secret: Optional[str] = None,
717
+ secret_key: Optional[str] = None,
718
+ ):
719
+ """
720
+ Set an environment variable from a Kubernetes Secret.
721
+ Client-side guard forbids MLRun internal auth/project secrets; no-op on API.
722
+ """
723
+ mlrun.common.secrets.validate_not_forbidden_secret(secret)
724
+ key = secret_key or name
714
725
  value_from = k8s_client.V1EnvVarSource(
715
- secret_key_ref=k8s_client.V1SecretKeySelector(name=secret, key=secret_key)
726
+ secret_key_ref=k8s_client.V1SecretKeySelector(name=secret, key=key)
716
727
  )
717
- return self._set_env(name, value_from=value_from)
728
+ return self._set_env(name=name, value_from=value_from)
718
729
 
719
- def set_env(self, name, value=None, value_from=None):
720
- """set pod environment var from value"""
721
- if value is not None:
722
- return self._set_env(name, value=str(value))
723
- return self._set_env(name, value_from=value_from)
730
+ def set_env(
731
+ self,
732
+ name: str,
733
+ value: Optional[str] = None,
734
+ value_from: Optional[typing.Any] = None,
735
+ ):
736
+ """
737
+ Set an environment variable.
738
+ If value comes from a Secret, validate on client-side only.
739
+ """
740
+ if value_from is not None:
741
+ secret_name = self._extract_secret_name_from_value_from(
742
+ value_from=value_from
743
+ )
744
+ if secret_name:
745
+ mlrun.common.secrets.validate_not_forbidden_secret(secret_name)
746
+ return self._set_env(name=name, value_from=value_from)
747
+
748
+ # Plain literal value path
749
+ return self._set_env(
750
+ name=name, value=(str(value) if value is not None else None)
751
+ )
724
752
 
725
753
  def with_annotations(self, annotations: dict):
726
754
  """set a key/value annotations in the metadata of the pod"""
@@ -1366,6 +1394,27 @@ class KubeResource(BaseRuntime):
1366
1394
 
1367
1395
  return self.status.state
1368
1396
 
1397
+ @staticmethod
1398
+ def _extract_secret_name_from_value_from(
1399
+ value_from: typing.Any,
1400
+ ) -> Optional[str]:
1401
+ """Extract secret name from a V1EnvVarSource or dict representation."""
1402
+ if isinstance(value_from, k8s_client.V1EnvVarSource):
1403
+ if value_from.secret_key_ref:
1404
+ return value_from.secret_key_ref.name
1405
+ elif isinstance(value_from, dict):
1406
+ value_from = (
1407
+ value_from.get("valueFrom")
1408
+ or value_from.get("value_from")
1409
+ or value_from
1410
+ )
1411
+ secret_key_ref = (value_from or {}).get("secretKeyRef") or (
1412
+ value_from or {}
1413
+ ).get("secret_key_ref")
1414
+ if isinstance(secret_key_ref, dict):
1415
+ return secret_key_ref.get("name")
1416
+ return None
1417
+
1369
1418
 
1370
1419
  def _resolve_if_type_sanitized(attribute_name, attribute):
1371
1420
  attribute_config = sanitized_attributes[attribute_name]
mlrun/secrets.py CHANGED
@@ -12,9 +12,15 @@
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
17
+ from collections.abc import Callable
16
18
  from os import environ
17
- from typing import Callable, Optional, Union
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)
@@ -191,7 +202,7 @@ def get_secret_or_env(
191
202
  key = f"{prefix}_{key}"
192
203
 
193
204
  if secret_provider:
194
- if isinstance(secret_provider, (dict, SecretsStore)):
205
+ if isinstance(secret_provider, dict | SecretsStore):
195
206
  secret_value = secret_provider.get(key)
196
207
  else:
197
208
  secret_value = secret_provider(key)
@@ -243,3 +254,36 @@ def _find_value_in_json_env_lists(
243
254
  if value_in_entry:
244
255
  return value_in_entry
245
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
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
@@ -168,7 +168,7 @@ class RemoteStep(storey.SendToHttp):
168
168
  text = await resp.text()
169
169
  raise RuntimeError(f"bad http response {resp.status}: {text}")
170
170
  return resp
171
- except asyncio.TimeoutError as exc:
171
+ except TimeoutError as exc:
172
172
  logger.error(f"http request to {url} timed out in RemoteStep {self.name}")
173
173
  raise exc
174
174
 
@@ -241,7 +241,7 @@ class RemoteStep(storey.SendToHttp):
241
241
  headers[event_id_key] = event.id
242
242
  if method == "GET":
243
243
  body = None
244
- elif body is not None and not isinstance(body, (str, bytes)):
244
+ elif body is not None and not isinstance(body, str | bytes):
245
245
  if self._body_function_handler:
246
246
  body = self._body_function_handler(body)
247
247
  body = json.dumps(body)
@@ -253,7 +253,7 @@ class RemoteStep(storey.SendToHttp):
253
253
  if (
254
254
  self.return_json
255
255
  or headers.get("content-type", "").lower() == "application/json"
256
- ) and isinstance(data, (str, bytes)):
256
+ ) and isinstance(data, str | bytes):
257
257
  data = json.loads(data)
258
258
  return data
259
259
 
@@ -390,7 +390,7 @@ class BatchHttpRequests(_ConcurrentJobExecution):
390
390
 
391
391
  if is_get:
392
392
  body = None
393
- elif body is not None and not isinstance(body, (str, bytes)):
393
+ elif body is not None and not isinstance(body, str | bytes):
394
394
  if self._body_function_handler:
395
395
  body = self._body_function_handler(body)
396
396
  body = json.dumps(body)
@@ -458,7 +458,7 @@ class BatchHttpRequests(_ConcurrentJobExecution):
458
458
  if (
459
459
  self.return_json
460
460
  or headers.get("content-type", "").lower() == "application/json"
461
- ) and isinstance(data, (str, bytes)):
461
+ ) and isinstance(data, str | bytes):
462
462
  data = json.loads(data)
463
463
  return data
464
464
 
mlrun/serving/routers.py CHANGED
@@ -986,7 +986,7 @@ class VotingEnsemble(ParallelRun):
986
986
  List
987
987
  The model's predictions
988
988
  """
989
- if isinstance(response, (list, numpy.ndarray)):
989
+ if isinstance(response, list | numpy.ndarray):
990
990
  return response
991
991
  try:
992
992
  self.format_response_with_col_name_flag = True
@@ -1123,7 +1123,7 @@ class EnrichmentModelRouter(ModelRouter):
1123
1123
 
1124
1124
  def preprocess(self, event):
1125
1125
  """Turn an entity identifier (source) to a Feature Vector"""
1126
- if isinstance(event.body, (str, bytes)):
1126
+ if isinstance(event.body, str | bytes):
1127
1127
  event.body = json.loads(event.body)
1128
1128
  event.body["inputs"] = self._feature_service.get(
1129
1129
  event.body["inputs"], as_list=True
@@ -1275,7 +1275,7 @@ class EnrichmentVotingEnsemble(VotingEnsemble):
1275
1275
  """
1276
1276
  Turn an entity identifier (source) to a Feature Vector
1277
1277
  """
1278
- if isinstance(event.body, (str, bytes)):
1278
+ if isinstance(event.body, str | bytes):
1279
1279
  event.body = json.loads(event.body)
1280
1280
  event.body["inputs"] = self._feature_service.get(
1281
1281
  event.body["inputs"], as_list=True
mlrun/serving/server.py CHANGED
@@ -24,7 +24,7 @@ import socket
24
24
  import traceback
25
25
  import uuid
26
26
  from collections import defaultdict
27
- from datetime import datetime, timezone
27
+ from datetime import UTC, datetime
28
28
  from typing import Any, Optional, Union
29
29
 
30
30
  import pandas as pd
@@ -303,7 +303,7 @@ class GraphServer(ModelObj):
303
303
  if event_path_key in event.headers:
304
304
  event.path = event.headers.get(event_path_key)
305
305
 
306
- if isinstance(event.body, (str, bytes)) and (
306
+ if isinstance(event.body, str | bytes) and (
307
307
  not event.content_type or event.content_type in ["json", "application/json"]
308
308
  ):
309
309
  # assume it is json and try to load
@@ -348,7 +348,7 @@ class GraphServer(ModelObj):
348
348
  ):
349
349
  return body
350
350
 
351
- if body and not isinstance(body, (str, bytes)):
351
+ if body and not isinstance(body, str | bytes):
352
352
  body = json.dumps(body)
353
353
  return context.Response(
354
354
  body=body, content_type="application/json", status_code=200
@@ -363,8 +363,6 @@ class GraphServer(ModelObj):
363
363
  def add_error_raiser_step(
364
364
  graph: RootFlowStep, monitored_steps: dict[str, MonitoredStep]
365
365
  ) -> RootFlowStep:
366
- monitored_steps_raisers = {}
367
- user_steps = list(graph.steps.values())
368
366
  for monitored_step in monitored_steps.values():
369
367
  error_step = graph.add_step(
370
368
  class_name="mlrun.serving.states.ModelRunnerErrorRaiser",
@@ -379,21 +377,7 @@ def add_error_raiser_step(
379
377
  if monitored_step.responder:
380
378
  monitored_step.responder = False
381
379
  error_step.respond()
382
- monitored_steps_raisers[monitored_step.name] = error_step.name
383
380
  error_step.on_error = monitored_step.on_error
384
- if monitored_steps_raisers:
385
- for step in user_steps:
386
- if step.after:
387
- if isinstance(step.after, list):
388
- for i in range(len(step.after)):
389
- if step.after[i] in monitored_steps_raisers:
390
- step.after[i] = monitored_steps_raisers[step.after[i]]
391
- else:
392
- if (
393
- isinstance(step.after, str)
394
- and step.after in monitored_steps_raisers
395
- ):
396
- step.after = monitored_steps_raisers[step.after]
397
381
  return graph
398
382
 
399
383
 
@@ -649,7 +633,7 @@ async def async_execute_graph(
649
633
 
650
634
  if df.empty:
651
635
  context.logger.warn("Job terminated due to empty inputs (0 rows)")
652
- return []
636
+ return
653
637
 
654
638
  track_models = spec.get("track_models")
655
639
 
@@ -676,7 +660,7 @@ async def async_execute_graph(
676
660
  start_time = end_time = df["timestamp"].iloc[0].isoformat()
677
661
  else:
678
662
  # end time will be set from clock time when the batch completes
679
- start_time = datetime.now(tz=timezone.utc).isoformat()
663
+ start_time = datetime.now(tz=UTC).isoformat()
680
664
 
681
665
  server.graph = add_system_steps_to_graph(
682
666
  server.project,
@@ -756,7 +740,7 @@ async def async_execute_graph(
756
740
  server = GraphServer.from_dict(spec)
757
741
  server.init_states(None, namespace)
758
742
 
759
- batch_completion_time = datetime.now(tz=timezone.utc).isoformat()
743
+ batch_completion_time = datetime.now(tz=UTC).isoformat()
760
744
 
761
745
  if not timestamp_column:
762
746
  end_time = batch_completion_time
@@ -779,30 +763,49 @@ async def async_execute_graph(
779
763
  model_endpoint_uids=model_endpoint_uids,
780
764
  )
781
765
 
782
- # log the results as artifacts
783
- num_of_meps_in_the_graph = len(server.graph.model_endpoints_names)
784
- artifact_path = None
785
- if (
786
- "{{run.uid}}" not in context.artifact_path
787
- ): # TODO: delete when IG-22841 is resolved
788
- artifact_path = "+/{{run.uid}}" # will be concatenated to the context's path in extend_artifact_path
789
- if num_of_meps_in_the_graph <= 1:
766
+ has_responder = False
767
+ for step in server.graph.steps.values():
768
+ if getattr(step, "responder", False):
769
+ has_responder = True
770
+ break
771
+
772
+ if has_responder:
773
+ # log the results as a dataset artifact
774
+ artifact_path = None
775
+ if (
776
+ "{{run.uid}}" not in context.artifact_path
777
+ ): # TODO: delete when IG-22841 is resolved
778
+ artifact_path = "+/{{run.uid}}" # will be concatenated to the context's path in extend_artifact_path
790
779
  context.log_dataset(
791
780
  "prediction", df=pd.DataFrame(responses), artifact_path=artifact_path
792
781
  )
793
- else:
794
- # turn this list of samples into a dict of lists, one per model endpoint
795
- grouped = defaultdict(list)
796
- for sample in responses:
797
- for model_name, features in sample.items():
798
- grouped[model_name].append(features)
799
- # create a dataframe per model endpoint and log it
800
- for model_name, features in grouped.items():
801
- context.log_dataset(
802
- f"prediction_{model_name}",
803
- df=pd.DataFrame(features),
804
- artifact_path=artifact_path,
805
- )
782
+
783
+ # if we got responses that appear to be in the right format, try to log per-model datasets too
784
+ if (
785
+ responses
786
+ and responses[0]
787
+ and isinstance(responses[0], dict)
788
+ and isinstance(next(iter(responses[0].values())), dict | list)
789
+ ):
790
+ try:
791
+ # turn this list of samples into a dict of lists, one per model endpoint
792
+ grouped = defaultdict(list)
793
+ for sample in responses:
794
+ for model_name, features in sample.items():
795
+ grouped[model_name].append(features)
796
+ # create a dataframe per model endpoint and log it
797
+ for model_name, features in grouped.items():
798
+ context.log_dataset(
799
+ f"prediction_{model_name}",
800
+ df=pd.DataFrame(features),
801
+ artifact_path=artifact_path,
802
+ )
803
+ except Exception as e:
804
+ context.logger.warning(
805
+ "Failed to log per-model prediction datasets",
806
+ error=err_to_str(e),
807
+ )
808
+
806
809
  context.log_result("num_rows", run_call_count)
807
810
 
808
811
 
@@ -11,6 +11,7 @@
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
+ import asyncio
14
15
 
15
16
  # serving runtime hooks, used in empty serving functions
16
17
  from mlrun.runtimes import nuclio_init_hook
@@ -20,5 +21,8 @@ def init_context(context):
20
21
  nuclio_init_hook(context, globals(), "serving_v2")
21
22
 
22
23
 
23
- def handler(context, event):
24
- return context.mlrun_handler(context, event)
24
+ async def handler(context, event):
25
+ result = context.mlrun_handler(context, event)
26
+ if asyncio.iscoroutine(result):
27
+ return await result
28
+ return result