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.
- mlrun/__init__.py +3 -2
- mlrun/__main__.py +0 -4
- mlrun/artifacts/dataset.py +2 -2
- mlrun/artifacts/plots.py +1 -1
- mlrun/{model_monitoring/db/tsdb/tdengine → auth}/__init__.py +2 -3
- mlrun/auth/nuclio.py +89 -0
- mlrun/auth/providers.py +429 -0
- mlrun/auth/utils.py +415 -0
- mlrun/common/constants.py +7 -0
- mlrun/common/model_monitoring/helpers.py +41 -4
- mlrun/common/runtimes/constants.py +28 -0
- mlrun/common/schemas/__init__.py +13 -3
- mlrun/common/schemas/alert.py +2 -2
- mlrun/common/schemas/api_gateway.py +3 -0
- mlrun/common/schemas/auth.py +10 -10
- mlrun/common/schemas/client_spec.py +4 -0
- mlrun/common/schemas/constants.py +25 -0
- mlrun/common/schemas/frontend_spec.py +1 -8
- mlrun/common/schemas/function.py +24 -0
- mlrun/common/schemas/hub.py +3 -2
- mlrun/common/schemas/model_monitoring/__init__.py +1 -1
- mlrun/common/schemas/model_monitoring/constants.py +2 -2
- mlrun/common/schemas/secret.py +17 -2
- mlrun/common/secrets.py +95 -1
- mlrun/common/types.py +10 -10
- mlrun/config.py +53 -15
- mlrun/data_types/infer.py +2 -2
- mlrun/datastore/__init__.py +2 -3
- mlrun/datastore/base.py +274 -10
- mlrun/datastore/datastore.py +1 -1
- mlrun/datastore/datastore_profile.py +49 -17
- mlrun/datastore/model_provider/huggingface_provider.py +6 -2
- mlrun/datastore/model_provider/model_provider.py +2 -2
- mlrun/datastore/model_provider/openai_provider.py +2 -2
- mlrun/datastore/s3.py +15 -16
- mlrun/datastore/sources.py +1 -1
- mlrun/datastore/store_resources.py +4 -4
- mlrun/datastore/storeytargets.py +16 -10
- mlrun/datastore/targets.py +1 -1
- mlrun/datastore/utils.py +16 -3
- mlrun/datastore/v3io.py +1 -1
- mlrun/db/base.py +36 -12
- mlrun/db/httpdb.py +316 -101
- mlrun/db/nopdb.py +29 -11
- mlrun/errors.py +4 -2
- mlrun/execution.py +11 -12
- mlrun/feature_store/api.py +1 -1
- mlrun/feature_store/common.py +1 -1
- mlrun/feature_store/feature_vector_utils.py +1 -1
- mlrun/feature_store/steps.py +8 -6
- mlrun/frameworks/_common/utils.py +3 -3
- mlrun/frameworks/_dl_common/loggers/logger.py +1 -1
- mlrun/frameworks/_dl_common/loggers/tensorboard_logger.py +2 -1
- mlrun/frameworks/_ml_common/loggers/mlrun_logger.py +1 -1
- mlrun/frameworks/_ml_common/utils.py +2 -1
- mlrun/frameworks/auto_mlrun/auto_mlrun.py +4 -3
- mlrun/frameworks/lgbm/mlrun_interfaces/mlrun_interface.py +2 -1
- mlrun/frameworks/onnx/dataset.py +2 -1
- mlrun/frameworks/onnx/mlrun_interface.py +2 -1
- mlrun/frameworks/pytorch/callbacks/logging_callback.py +5 -4
- mlrun/frameworks/pytorch/callbacks/mlrun_logging_callback.py +2 -1
- mlrun/frameworks/pytorch/callbacks/tensorboard_logging_callback.py +2 -1
- mlrun/frameworks/pytorch/utils.py +2 -1
- mlrun/frameworks/sklearn/metric.py +2 -1
- mlrun/frameworks/tf_keras/callbacks/logging_callback.py +5 -4
- mlrun/frameworks/tf_keras/callbacks/mlrun_logging_callback.py +2 -1
- mlrun/frameworks/tf_keras/callbacks/tensorboard_logging_callback.py +2 -1
- mlrun/hub/__init__.py +37 -0
- mlrun/hub/base.py +142 -0
- mlrun/hub/module.py +67 -76
- mlrun/hub/step.py +113 -0
- mlrun/launcher/base.py +2 -1
- mlrun/launcher/local.py +2 -1
- mlrun/model.py +12 -2
- mlrun/model_monitoring/__init__.py +0 -1
- mlrun/model_monitoring/api.py +2 -2
- mlrun/model_monitoring/applications/base.py +20 -6
- mlrun/model_monitoring/applications/context.py +1 -0
- mlrun/model_monitoring/controller.py +7 -17
- mlrun/model_monitoring/db/_schedules.py +2 -16
- mlrun/model_monitoring/db/_stats.py +2 -13
- mlrun/model_monitoring/db/tsdb/__init__.py +9 -7
- mlrun/model_monitoring/db/tsdb/base.py +2 -4
- mlrun/model_monitoring/db/tsdb/preaggregate.py +234 -0
- mlrun/model_monitoring/db/tsdb/stream_graph_steps.py +63 -0
- mlrun/model_monitoring/db/tsdb/timescaledb/queries/timescaledb_metrics_queries.py +414 -0
- mlrun/model_monitoring/db/tsdb/timescaledb/queries/timescaledb_predictions_queries.py +376 -0
- mlrun/model_monitoring/db/tsdb/timescaledb/queries/timescaledb_results_queries.py +590 -0
- mlrun/model_monitoring/db/tsdb/timescaledb/timescaledb_connection.py +434 -0
- mlrun/model_monitoring/db/tsdb/timescaledb/timescaledb_connector.py +541 -0
- mlrun/model_monitoring/db/tsdb/timescaledb/timescaledb_operations.py +808 -0
- mlrun/model_monitoring/db/tsdb/timescaledb/timescaledb_schema.py +502 -0
- mlrun/model_monitoring/db/tsdb/timescaledb/timescaledb_stream.py +163 -0
- mlrun/model_monitoring/db/tsdb/timescaledb/timescaledb_stream_graph_steps.py +60 -0
- mlrun/model_monitoring/db/tsdb/timescaledb/utils/timescaledb_dataframe_processor.py +141 -0
- mlrun/model_monitoring/db/tsdb/timescaledb/utils/timescaledb_query_builder.py +585 -0
- mlrun/model_monitoring/db/tsdb/timescaledb/writer_graph_steps.py +73 -0
- mlrun/model_monitoring/db/tsdb/v3io/stream_graph_steps.py +4 -6
- mlrun/model_monitoring/db/tsdb/v3io/v3io_connector.py +147 -79
- mlrun/model_monitoring/features_drift_table.py +2 -1
- mlrun/model_monitoring/helpers.py +2 -1
- mlrun/model_monitoring/stream_processing.py +18 -16
- mlrun/model_monitoring/writer.py +4 -3
- mlrun/package/__init__.py +2 -1
- mlrun/platforms/__init__.py +0 -44
- mlrun/platforms/iguazio.py +1 -1
- mlrun/projects/operations.py +11 -10
- mlrun/projects/project.py +81 -82
- mlrun/run.py +4 -7
- mlrun/runtimes/__init__.py +2 -204
- mlrun/runtimes/base.py +89 -21
- mlrun/runtimes/constants.py +225 -0
- mlrun/runtimes/daskjob.py +4 -2
- mlrun/runtimes/databricks_job/databricks_runtime.py +2 -1
- mlrun/runtimes/mounts.py +5 -0
- mlrun/runtimes/nuclio/__init__.py +12 -8
- mlrun/runtimes/nuclio/api_gateway.py +36 -6
- mlrun/runtimes/nuclio/application/application.py +200 -32
- mlrun/runtimes/nuclio/function.py +154 -49
- mlrun/runtimes/nuclio/serving.py +55 -42
- mlrun/runtimes/pod.py +59 -10
- mlrun/secrets.py +46 -2
- mlrun/serving/__init__.py +2 -0
- mlrun/serving/remote.py +5 -5
- mlrun/serving/routers.py +3 -3
- mlrun/serving/server.py +46 -43
- mlrun/serving/serving_wrapper.py +6 -2
- mlrun/serving/states.py +554 -207
- mlrun/serving/steps.py +1 -1
- mlrun/serving/system_steps.py +42 -33
- mlrun/track/trackers/mlflow_tracker.py +29 -31
- mlrun/utils/helpers.py +89 -16
- mlrun/utils/http.py +9 -2
- mlrun/utils/notifications/notification/git.py +1 -1
- mlrun/utils/notifications/notification/mail.py +39 -16
- mlrun/utils/notifications/notification_pusher.py +2 -2
- mlrun/utils/version/version.json +2 -2
- mlrun/utils/version/version.py +3 -4
- {mlrun-1.10.0rc40.dist-info → mlrun-1.11.0rc16.dist-info}/METADATA +39 -49
- {mlrun-1.10.0rc40.dist-info → mlrun-1.11.0rc16.dist-info}/RECORD +144 -130
- mlrun/db/auth_utils.py +0 -152
- mlrun/model_monitoring/db/tsdb/tdengine/schemas.py +0 -343
- mlrun/model_monitoring/db/tsdb/tdengine/stream_graph_steps.py +0 -75
- mlrun/model_monitoring/db/tsdb/tdengine/tdengine_connection.py +0 -281
- mlrun/model_monitoring/db/tsdb/tdengine/tdengine_connector.py +0 -1368
- mlrun/model_monitoring/db/tsdb/tdengine/writer_graph_steps.py +0 -51
- {mlrun-1.10.0rc40.dist-info → mlrun-1.11.0rc16.dist-info}/WHEEL +0 -0
- {mlrun-1.10.0rc40.dist-info → mlrun-1.11.0rc16.dist-info}/entry_points.txt +0 -0
- {mlrun-1.10.0rc40.dist-info → mlrun-1.11.0rc16.dist-info}/licenses/LICENSE +0 -0
- {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(
|
|
712
|
-
|
|
713
|
-
|
|
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=
|
|
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(
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
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
|
|
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,
|
|
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
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
|
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,
|
|
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,
|
|
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=
|
|
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=
|
|
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
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
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
|
-
|
|
794
|
-
#
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
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
|
|
mlrun/serving/serving_wrapper.py
CHANGED
|
@@ -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
|
-
|
|
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
|