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.
- mlrun/__init__.py +24 -3
- mlrun/__main__.py +0 -4
- mlrun/artifacts/dataset.py +2 -2
- mlrun/artifacts/document.py +6 -1
- mlrun/artifacts/llm_prompt.py +21 -15
- mlrun/artifacts/model.py +3 -3
- 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 +14 -0
- mlrun/common/model_monitoring/helpers.py +123 -0
- mlrun/common/runtimes/constants.py +28 -0
- mlrun/common/schemas/__init__.py +14 -3
- mlrun/common/schemas/alert.py +2 -2
- mlrun/common/schemas/api_gateway.py +3 -0
- mlrun/common/schemas/auth.py +12 -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 +34 -0
- mlrun/common/schemas/hub.py +33 -20
- mlrun/common/schemas/model_monitoring/__init__.py +2 -1
- mlrun/common/schemas/model_monitoring/constants.py +12 -15
- mlrun/common/schemas/model_monitoring/functions.py +13 -4
- mlrun/common/schemas/model_monitoring/model_endpoints.py +11 -0
- mlrun/common/schemas/pipeline.py +1 -1
- mlrun/common/schemas/secret.py +17 -2
- mlrun/common/secrets.py +95 -1
- mlrun/common/types.py +10 -10
- mlrun/config.py +69 -19
- mlrun/data_types/infer.py +2 -2
- mlrun/datastore/__init__.py +12 -5
- mlrun/datastore/azure_blob.py +162 -47
- mlrun/datastore/base.py +274 -10
- mlrun/datastore/datastore.py +7 -2
- mlrun/datastore/datastore_profile.py +84 -22
- mlrun/datastore/model_provider/huggingface_provider.py +225 -41
- mlrun/datastore/model_provider/mock_model_provider.py +87 -0
- mlrun/datastore/model_provider/model_provider.py +206 -74
- mlrun/datastore/model_provider/openai_provider.py +226 -66
- mlrun/datastore/s3.py +39 -18
- mlrun/datastore/sources.py +1 -1
- mlrun/datastore/store_resources.py +4 -4
- mlrun/datastore/storeytargets.py +17 -12
- mlrun/datastore/targets.py +1 -1
- mlrun/datastore/utils.py +25 -6
- mlrun/datastore/v3io.py +1 -1
- mlrun/db/base.py +63 -32
- mlrun/db/httpdb.py +373 -153
- mlrun/db/nopdb.py +54 -21
- mlrun/errors.py +4 -2
- mlrun/execution.py +66 -25
- 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 +52 -0
- mlrun/hub/base.py +142 -0
- mlrun/hub/module.py +172 -0
- mlrun/hub/step.py +113 -0
- mlrun/k8s_utils.py +105 -16
- mlrun/launcher/base.py +15 -7
- mlrun/launcher/local.py +4 -1
- mlrun/model.py +14 -4
- mlrun/model_monitoring/__init__.py +0 -1
- mlrun/model_monitoring/api.py +65 -28
- mlrun/model_monitoring/applications/__init__.py +1 -1
- mlrun/model_monitoring/applications/base.py +299 -128
- mlrun/model_monitoring/applications/context.py +2 -4
- mlrun/model_monitoring/controller.py +132 -58
- mlrun/model_monitoring/db/_schedules.py +38 -29
- mlrun/model_monitoring/db/_stats.py +6 -16
- mlrun/model_monitoring/db/tsdb/__init__.py +9 -7
- mlrun/model_monitoring/db/tsdb/base.py +29 -9
- 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 +20 -9
- mlrun/model_monitoring/db/tsdb/v3io/v3io_connector.py +235 -51
- mlrun/model_monitoring/features_drift_table.py +2 -1
- mlrun/model_monitoring/helpers.py +30 -6
- mlrun/model_monitoring/stream_processing.py +34 -28
- mlrun/model_monitoring/writer.py +224 -4
- mlrun/package/__init__.py +2 -1
- mlrun/platforms/__init__.py +0 -43
- mlrun/platforms/iguazio.py +8 -4
- mlrun/projects/operations.py +17 -11
- mlrun/projects/pipelines.py +2 -2
- mlrun/projects/project.py +187 -123
- mlrun/run.py +95 -21
- mlrun/runtimes/__init__.py +2 -186
- mlrun/runtimes/base.py +103 -25
- mlrun/runtimes/constants.py +225 -0
- mlrun/runtimes/daskjob.py +5 -2
- mlrun/runtimes/databricks_job/databricks_runtime.py +2 -1
- mlrun/runtimes/local.py +5 -2
- mlrun/runtimes/mounts.py +20 -2
- mlrun/runtimes/nuclio/__init__.py +12 -7
- mlrun/runtimes/nuclio/api_gateway.py +36 -6
- mlrun/runtimes/nuclio/application/application.py +339 -40
- mlrun/runtimes/nuclio/function.py +222 -72
- mlrun/runtimes/nuclio/serving.py +132 -42
- mlrun/runtimes/pod.py +213 -21
- mlrun/runtimes/utils.py +49 -9
- mlrun/secrets.py +99 -14
- mlrun/serving/__init__.py +2 -0
- mlrun/serving/remote.py +84 -11
- mlrun/serving/routers.py +26 -44
- mlrun/serving/server.py +138 -51
- mlrun/serving/serving_wrapper.py +6 -2
- mlrun/serving/states.py +997 -283
- mlrun/serving/steps.py +62 -0
- mlrun/serving/system_steps.py +149 -95
- mlrun/serving/v2_serving.py +9 -10
- mlrun/track/trackers/mlflow_tracker.py +29 -31
- mlrun/utils/helpers.py +292 -94
- mlrun/utils/http.py +9 -2
- mlrun/utils/notifications/notification/base.py +18 -0
- mlrun/utils/notifications/notification/git.py +3 -5
- mlrun/utils/notifications/notification/mail.py +39 -16
- mlrun/utils/notifications/notification/slack.py +2 -4
- mlrun/utils/notifications/notification/webhook.py +2 -5
- mlrun/utils/notifications/notification_pusher.py +3 -3
- mlrun/utils/version/version.json +2 -2
- mlrun/utils/version/version.py +3 -4
- {mlrun-1.10.0rc18.dist-info → mlrun-1.11.0rc16.dist-info}/METADATA +63 -74
- {mlrun-1.10.0rc18.dist-info → mlrun-1.11.0rc16.dist-info}/RECORD +161 -143
- mlrun/api/schemas/__init__.py +0 -259
- mlrun/db/auth_utils.py +0 -152
- mlrun/model_monitoring/db/tsdb/tdengine/schemas.py +0 -344
- 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 -1266
- {mlrun-1.10.0rc18.dist-info → mlrun-1.11.0rc16.dist-info}/WHEEL +0 -0
- {mlrun-1.10.0rc18.dist-info → mlrun-1.11.0rc16.dist-info}/entry_points.txt +0 -0
- {mlrun-1.10.0rc18.dist-info → mlrun-1.11.0rc16.dist-info}/licenses/LICENSE +0 -0
- {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 ==
|
|
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
|
|
171
|
-
elif state
|
|
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(
|
|
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(
|
|
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:
|
|
447
|
-
|
|
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
|
|
17
|
-
from
|
|
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,
|
|
193
|
-
|
|
205
|
+
if isinstance(secret_provider, dict | SecretsStore):
|
|
206
|
+
secret_value = secret_provider.get(key)
|
|
194
207
|
else:
|
|
195
|
-
|
|
196
|
-
if
|
|
197
|
-
return
|
|
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
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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,
|
|
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
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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,
|
|
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
|