mlrun 1.10.0rc11__py3-none-any.whl → 1.10.0rc12__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 +2 -1
- mlrun/__main__.py +7 -1
- mlrun/artifacts/base.py +9 -3
- mlrun/artifacts/dataset.py +2 -1
- mlrun/artifacts/llm_prompt.py +1 -1
- mlrun/artifacts/model.py +2 -2
- mlrun/common/constants.py +1 -0
- mlrun/common/runtimes/constants.py +10 -1
- mlrun/config.py +19 -2
- mlrun/datastore/__init__.py +3 -1
- mlrun/datastore/alibaba_oss.py +1 -1
- mlrun/datastore/azure_blob.py +1 -1
- mlrun/datastore/base.py +6 -31
- mlrun/datastore/datastore.py +109 -33
- mlrun/datastore/datastore_profile.py +31 -0
- mlrun/datastore/dbfs_store.py +1 -1
- mlrun/datastore/google_cloud_storage.py +2 -2
- mlrun/datastore/model_provider/__init__.py +13 -0
- mlrun/datastore/model_provider/model_provider.py +82 -0
- mlrun/datastore/model_provider/openai_provider.py +120 -0
- mlrun/datastore/remote_client.py +54 -0
- mlrun/datastore/s3.py +1 -1
- mlrun/datastore/storeytargets.py +1 -1
- mlrun/datastore/utils.py +22 -0
- mlrun/datastore/v3io.py +1 -1
- mlrun/db/base.py +1 -1
- mlrun/db/httpdb.py +9 -4
- mlrun/db/nopdb.py +1 -1
- mlrun/execution.py +23 -7
- mlrun/launcher/base.py +23 -13
- mlrun/launcher/local.py +3 -1
- mlrun/launcher/remote.py +4 -2
- mlrun/model.py +65 -0
- mlrun/package/packagers_manager.py +2 -0
- mlrun/projects/operations.py +8 -1
- mlrun/projects/project.py +23 -5
- mlrun/run.py +17 -0
- mlrun/runtimes/__init__.py +6 -0
- mlrun/runtimes/base.py +24 -6
- mlrun/runtimes/daskjob.py +1 -0
- mlrun/runtimes/databricks_job/databricks_runtime.py +1 -0
- mlrun/runtimes/local.py +1 -6
- mlrun/serving/server.py +0 -2
- mlrun/serving/states.py +30 -5
- mlrun/serving/system_steps.py +22 -28
- mlrun/utils/helpers.py +13 -2
- mlrun/utils/notifications/notification_pusher.py +15 -0
- mlrun/utils/version/version.json +2 -2
- {mlrun-1.10.0rc11.dist-info → mlrun-1.10.0rc12.dist-info}/METADATA +2 -2
- {mlrun-1.10.0rc11.dist-info → mlrun-1.10.0rc12.dist-info}/RECORD +54 -50
- {mlrun-1.10.0rc11.dist-info → mlrun-1.10.0rc12.dist-info}/WHEEL +0 -0
- {mlrun-1.10.0rc11.dist-info → mlrun-1.10.0rc12.dist-info}/entry_points.txt +0 -0
- {mlrun-1.10.0rc11.dist-info → mlrun-1.10.0rc12.dist-info}/licenses/LICENSE +0 -0
- {mlrun-1.10.0rc11.dist-info → mlrun-1.10.0rc12.dist-info}/top_level.txt +0 -0
mlrun/__init__.py
CHANGED
|
@@ -32,7 +32,7 @@ from typing import Optional
|
|
|
32
32
|
import dotenv
|
|
33
33
|
|
|
34
34
|
from .config import config as mlconf
|
|
35
|
-
from .datastore import DataItem, store_manager
|
|
35
|
+
from .datastore import DataItem, ModelProvider, store_manager
|
|
36
36
|
from .db import get_run_db
|
|
37
37
|
from .errors import MLRunInvalidArgumentError, MLRunNotFoundError
|
|
38
38
|
from .execution import MLClientCtx
|
|
@@ -55,6 +55,7 @@ from .run import (
|
|
|
55
55
|
code_to_function,
|
|
56
56
|
function_to_module,
|
|
57
57
|
get_dataitem,
|
|
58
|
+
get_model_provider,
|
|
58
59
|
get_object,
|
|
59
60
|
get_or_create_ctx,
|
|
60
61
|
get_pipeline,
|
mlrun/__main__.py
CHANGED
|
@@ -261,7 +261,13 @@ def run(
|
|
|
261
261
|
config = environ.get("MLRUN_EXEC_CONFIG")
|
|
262
262
|
if from_env and config:
|
|
263
263
|
config = json.loads(config)
|
|
264
|
-
|
|
264
|
+
# If run is a retry we need to maintain the run status therefore using RunObject instead of RunTemplate
|
|
265
|
+
retry_count = config.get("status", {}).get("retry_count")
|
|
266
|
+
if retry_count:
|
|
267
|
+
logger.info(f"Retrying run - attempt: {retry_count + 1}")
|
|
268
|
+
runobj = mlrun.RunObject.from_dict(config)
|
|
269
|
+
else:
|
|
270
|
+
runobj = RunTemplate.from_dict(config)
|
|
265
271
|
elif task:
|
|
266
272
|
obj = get_object(task)
|
|
267
273
|
task = yaml.load(obj, Loader=yaml.FullLoader)
|
mlrun/artifacts/base.py
CHANGED
|
@@ -839,9 +839,7 @@ def get_artifact_meta(artifact):
|
|
|
839
839
|
artifact = artifact.artifact_url
|
|
840
840
|
|
|
841
841
|
if mlrun.datastore.is_store_uri(artifact):
|
|
842
|
-
artifact_spec,
|
|
843
|
-
artifact
|
|
844
|
-
)
|
|
842
|
+
artifact_spec, _ = mlrun.datastore.store_manager.get_store_artifact(artifact)
|
|
845
843
|
|
|
846
844
|
elif artifact.lower().endswith(".yaml"):
|
|
847
845
|
data = mlrun.datastore.store_manager.object(url=artifact).get()
|
|
@@ -942,3 +940,11 @@ def fill_artifact_object_hash(object_dict, iteration=None, producer_id=None):
|
|
|
942
940
|
object_dict["spec"][key] = value
|
|
943
941
|
|
|
944
942
|
return uid
|
|
943
|
+
|
|
944
|
+
|
|
945
|
+
def verify_target_path(artifact: Artifact):
|
|
946
|
+
if not artifact.get_target_path():
|
|
947
|
+
raise mlrun.errors.MLRunInvalidArgumentError(
|
|
948
|
+
f"artifact {artifact.uri} "
|
|
949
|
+
f"does not have a valid/persistent offline target"
|
|
950
|
+
)
|
mlrun/artifacts/dataset.py
CHANGED
|
@@ -26,7 +26,7 @@ import mlrun.datastore
|
|
|
26
26
|
import mlrun.utils.helpers
|
|
27
27
|
from mlrun.config import config as mlconf
|
|
28
28
|
|
|
29
|
-
from .base import Artifact, ArtifactSpec, StorePrefix
|
|
29
|
+
from .base import Artifact, ArtifactSpec, StorePrefix, verify_target_path
|
|
30
30
|
|
|
31
31
|
default_preview_rows_length = 20
|
|
32
32
|
max_preview_columns = mlconf.artifacts.datasets.max_preview_columns
|
|
@@ -424,6 +424,7 @@ def update_dataset_meta(
|
|
|
424
424
|
artifact_spec = artifact
|
|
425
425
|
elif mlrun.datastore.is_store_uri(artifact):
|
|
426
426
|
artifact_spec, _ = mlrun.datastore.store_manager.get_store_artifact(artifact)
|
|
427
|
+
verify_target_path(artifact_spec)
|
|
427
428
|
else:
|
|
428
429
|
raise ValueError("model path must be a model store object/URL/DataItem")
|
|
429
430
|
|
mlrun/artifacts/llm_prompt.py
CHANGED
|
@@ -127,7 +127,7 @@ class LLMPromptArtifact(Artifact):
|
|
|
127
127
|
if self.spec._model_artifact:
|
|
128
128
|
return self.spec._model_artifact
|
|
129
129
|
if self.spec.model_uri:
|
|
130
|
-
self.spec._model_artifact,
|
|
130
|
+
self.spec._model_artifact, _ = (
|
|
131
131
|
mlrun.datastore.store_manager.get_store_artifact(self.spec.model_uri)
|
|
132
132
|
)
|
|
133
133
|
return self.spec._model_artifact
|
mlrun/artifacts/model.py
CHANGED
|
@@ -26,7 +26,7 @@ from ..data_types import InferOptions, get_infer_interface
|
|
|
26
26
|
from ..features import Feature
|
|
27
27
|
from ..model import ObjectList
|
|
28
28
|
from ..utils import StorePrefix, is_relative_path
|
|
29
|
-
from .base import Artifact, ArtifactSpec, upload_extra_data
|
|
29
|
+
from .base import Artifact, ArtifactSpec, upload_extra_data, verify_target_path
|
|
30
30
|
|
|
31
31
|
model_spec_filename = "model_spec.yaml"
|
|
32
32
|
MODEL_OPTIONAL_SUFFIXES = [".tar.gz", ".pkl", ".bin", ".pickle"]
|
|
@@ -493,7 +493,6 @@ def get_model(
|
|
|
493
493
|
:returns: model filename, model artifact object, extra data dict
|
|
494
494
|
|
|
495
495
|
"""
|
|
496
|
-
# TODO support LLMPromptArtifact
|
|
497
496
|
model_file = ""
|
|
498
497
|
model_spec = None
|
|
499
498
|
extra_dataitems = {}
|
|
@@ -518,6 +517,7 @@ def get_model(
|
|
|
518
517
|
model_spec, target = mlrun.datastore.store_manager.get_store_artifact(
|
|
519
518
|
model_dir
|
|
520
519
|
)
|
|
520
|
+
verify_target_path(model_spec)
|
|
521
521
|
else:
|
|
522
522
|
model_spec, target = model_dir, model_dir.get_target_path()
|
|
523
523
|
if not model_spec or model_spec.kind != "model":
|
mlrun/common/constants.py
CHANGED
|
@@ -66,6 +66,7 @@ class MLRunInternalLabels:
|
|
|
66
66
|
scrape_metrics = f"{MLRUN_LABEL_PREFIX}scrape-metrics"
|
|
67
67
|
tag = f"{MLRUN_LABEL_PREFIX}tag"
|
|
68
68
|
uid = f"{MLRUN_LABEL_PREFIX}uid"
|
|
69
|
+
retry = f"{MLRUN_LABEL_PREFIX}retry-attempt"
|
|
69
70
|
username = f"{MLRUN_LABEL_PREFIX}username"
|
|
70
71
|
username_domain = f"{MLRUN_LABEL_PREFIX}username_domain"
|
|
71
72
|
task_name = f"{MLRUN_LABEL_PREFIX}task-name"
|
|
@@ -139,6 +139,7 @@ class RunStates:
|
|
|
139
139
|
aborted = "aborted"
|
|
140
140
|
aborting = "aborting"
|
|
141
141
|
skipped = "skipped"
|
|
142
|
+
pending_retry = "pendingRetry"
|
|
142
143
|
|
|
143
144
|
@staticmethod
|
|
144
145
|
def all():
|
|
@@ -152,6 +153,7 @@ class RunStates:
|
|
|
152
153
|
RunStates.aborted,
|
|
153
154
|
RunStates.aborting,
|
|
154
155
|
RunStates.skipped,
|
|
156
|
+
RunStates.pending_retry,
|
|
155
157
|
]
|
|
156
158
|
|
|
157
159
|
@staticmethod
|
|
@@ -168,6 +170,7 @@ class RunStates:
|
|
|
168
170
|
return [
|
|
169
171
|
RunStates.error,
|
|
170
172
|
RunStates.aborted,
|
|
173
|
+
RunStates.pending_retry,
|
|
171
174
|
]
|
|
172
175
|
|
|
173
176
|
@staticmethod
|
|
@@ -185,12 +188,18 @@ class RunStates:
|
|
|
185
188
|
def non_terminal_states():
|
|
186
189
|
return list(set(RunStates.all()) - set(RunStates.terminal_states()))
|
|
187
190
|
|
|
191
|
+
@staticmethod
|
|
192
|
+
def terminal_or_error_states():
|
|
193
|
+
return list(
|
|
194
|
+
set(RunStates.terminal_states())
|
|
195
|
+
| set(RunStates.error_and_abortion_states())
|
|
196
|
+
)
|
|
197
|
+
|
|
188
198
|
@staticmethod
|
|
189
199
|
def not_allowed_for_deletion_states():
|
|
190
200
|
return [
|
|
191
201
|
RunStates.running,
|
|
192
202
|
RunStates.pending,
|
|
193
|
-
# TODO: add aborting state once we have it
|
|
194
203
|
]
|
|
195
204
|
|
|
196
205
|
@staticmethod
|
mlrun/config.py
CHANGED
|
@@ -120,6 +120,12 @@ default_config = {
|
|
|
120
120
|
# max number of parallel abort run jobs in runs monitoring
|
|
121
121
|
"concurrent_abort_stale_runs_workers": 10,
|
|
122
122
|
"list_runs_time_period_in_days": 7, # days
|
|
123
|
+
"retry": {
|
|
124
|
+
# periodic job for triggering retries interval in seconds
|
|
125
|
+
"interval": "30",
|
|
126
|
+
# runs limit to fetch for retrying
|
|
127
|
+
"fetch_runs_limit": 1000,
|
|
128
|
+
},
|
|
123
129
|
},
|
|
124
130
|
"projects": {
|
|
125
131
|
"summaries": {
|
|
@@ -184,6 +190,9 @@ default_config = {
|
|
|
184
190
|
"url": "",
|
|
185
191
|
},
|
|
186
192
|
"v3io_framesd": "http://framesd:8080",
|
|
193
|
+
"model_providers": {
|
|
194
|
+
"openai_default_model": "gpt-4",
|
|
195
|
+
},
|
|
187
196
|
# default node selector to be applied to all functions - json string base64 encoded format
|
|
188
197
|
"default_function_node_selector": "e30=",
|
|
189
198
|
# default priority class to be applied to functions running on k8s cluster
|
|
@@ -270,6 +279,12 @@ default_config = {
|
|
|
270
279
|
"executing": "24h",
|
|
271
280
|
}
|
|
272
281
|
},
|
|
282
|
+
"retry": {
|
|
283
|
+
"backoff": {
|
|
284
|
+
"default_base_delay": "30s",
|
|
285
|
+
"min_base_delay": "30s",
|
|
286
|
+
},
|
|
287
|
+
},
|
|
273
288
|
# When the module is reloaded, the maximum depth recursion configuration for the recursive reload
|
|
274
289
|
# function is used to prevent infinite loop
|
|
275
290
|
"reload_max_recursion_depth": 100,
|
|
@@ -316,6 +331,7 @@ default_config = {
|
|
|
316
331
|
"project_summaries": "enabled",
|
|
317
332
|
"start_logs": "enabled",
|
|
318
333
|
"stop_logs": "enabled",
|
|
334
|
+
"retry_jobs": "enabled",
|
|
319
335
|
},
|
|
320
336
|
},
|
|
321
337
|
"worker": {
|
|
@@ -539,7 +555,7 @@ default_config = {
|
|
|
539
555
|
},
|
|
540
556
|
"v3io_api": "",
|
|
541
557
|
"v3io_framesd": "",
|
|
542
|
-
# If running from sdk and MLRUN_DBPATH is not set, the db will fallback to a nop db which will not
|
|
558
|
+
# If running from sdk and MLRUN_DBPATH is not set, the db will fallback to a nop db which will not perform any
|
|
543
559
|
# run db operations.
|
|
544
560
|
"nop_db": {
|
|
545
561
|
# if set to true, will raise an error for trying to use run db functionality
|
|
@@ -641,7 +657,7 @@ default_config = {
|
|
|
641
657
|
"offline_storage_path": "model-endpoints/{kind}",
|
|
642
658
|
"parquet_batching_max_events": 10_000,
|
|
643
659
|
"parquet_batching_timeout_secs": timedelta(minutes=1).total_seconds(),
|
|
644
|
-
"model_endpoint_creation_check_period":
|
|
660
|
+
"model_endpoint_creation_check_period": 15,
|
|
645
661
|
},
|
|
646
662
|
"secret_stores": {
|
|
647
663
|
# Use only in testing scenarios (such as integration tests) to avoid using k8s for secrets (will use in-memory
|
|
@@ -1219,6 +1235,7 @@ class Config:
|
|
|
1219
1235
|
"""
|
|
1220
1236
|
Get the default value for the ssl_redirect configuration.
|
|
1221
1237
|
In Iguazio we always want to redirect to HTTPS, in other cases we don't.
|
|
1238
|
+
|
|
1222
1239
|
:return: True if we should redirect to HTTPS, False otherwise.
|
|
1223
1240
|
"""
|
|
1224
1241
|
return self.is_running_on_iguazio()
|
mlrun/datastore/__init__.py
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
__all__ = [
|
|
16
16
|
"DataItem",
|
|
17
|
+
"ModelProvider",
|
|
17
18
|
"get_store_resource",
|
|
18
19
|
"ParquetTarget",
|
|
19
20
|
"CSVTarget",
|
|
@@ -32,12 +33,12 @@ __all__ = [
|
|
|
32
33
|
"get_stream_pusher",
|
|
33
34
|
"ConfigProfile",
|
|
34
35
|
"VectorStoreCollection",
|
|
36
|
+
"store_manager",
|
|
35
37
|
]
|
|
36
38
|
|
|
37
39
|
from urllib.parse import urlparse
|
|
38
40
|
|
|
39
41
|
import fsspec
|
|
40
|
-
from mergedeep import merge
|
|
41
42
|
|
|
42
43
|
import mlrun.datastore.wasbfs
|
|
43
44
|
from mlrun.datastore.datastore_profile import (
|
|
@@ -45,6 +46,7 @@ from mlrun.datastore.datastore_profile import (
|
|
|
45
46
|
DatastoreProfileKafkaTarget,
|
|
46
47
|
DatastoreProfileV3io,
|
|
47
48
|
)
|
|
49
|
+
from mlrun.datastore.model_provider.model_provider import ModelProvider
|
|
48
50
|
from mlrun.platforms.iguazio import (
|
|
49
51
|
HTTPOutputStream,
|
|
50
52
|
KafkaOutputStream,
|
mlrun/datastore/alibaba_oss.py
CHANGED
|
@@ -69,7 +69,7 @@ class OSSStore(DataStore):
|
|
|
69
69
|
key=self._get_secret_or_env("ALIBABA_ACCESS_KEY_ID"),
|
|
70
70
|
secret=self._get_secret_or_env("ALIBABA_SECRET_ACCESS_KEY"),
|
|
71
71
|
)
|
|
72
|
-
return self.
|
|
72
|
+
return self._sanitize_options(res)
|
|
73
73
|
|
|
74
74
|
def get_bucket_and_key(self, key):
|
|
75
75
|
path = self._join(key)[1:]
|
mlrun/datastore/azure_blob.py
CHANGED
|
@@ -67,7 +67,7 @@ class AzureBlobStore(DataStore):
|
|
|
67
67
|
or self._get_secret_or_env("AZURE_STORAGE_SAS_TOKEN"),
|
|
68
68
|
credential=self._get_secret_or_env("credential"),
|
|
69
69
|
)
|
|
70
|
-
self._storage_options = self.
|
|
70
|
+
self._storage_options = self._sanitize_options(res)
|
|
71
71
|
return self._storage_options
|
|
72
72
|
|
|
73
73
|
@property
|
mlrun/datastore/base.py
CHANGED
|
@@ -28,6 +28,7 @@ import requests
|
|
|
28
28
|
|
|
29
29
|
import mlrun.config
|
|
30
30
|
import mlrun.errors
|
|
31
|
+
from mlrun.datastore.remote_client import BaseRemoteClient
|
|
31
32
|
from mlrun.errors import err_to_str
|
|
32
33
|
from mlrun.utils import StorePrefix, is_jupyter, logger
|
|
33
34
|
|
|
@@ -45,22 +46,19 @@ class FileStats:
|
|
|
45
46
|
return f"FileStats(size={self.size}, modified={self.modified}, type={self.content_type})"
|
|
46
47
|
|
|
47
48
|
|
|
48
|
-
class DataStore:
|
|
49
|
+
class DataStore(BaseRemoteClient):
|
|
49
50
|
using_bucket = False
|
|
50
51
|
|
|
51
52
|
def __init__(
|
|
52
53
|
self, parent, name, kind, endpoint="", secrets: Optional[dict] = None, **kwargs
|
|
53
54
|
):
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
self.endpoint = endpoint
|
|
55
|
+
super().__init__(
|
|
56
|
+
parent=parent, kind=kind, name=name, endpoint=endpoint, secrets=secrets
|
|
57
|
+
)
|
|
58
58
|
self.subpath = ""
|
|
59
|
-
self.secret_pfx = ""
|
|
60
59
|
self.options = {}
|
|
61
60
|
self.from_spec = False
|
|
62
61
|
self._filesystem = None
|
|
63
|
-
self._secrets = secrets or {}
|
|
64
62
|
|
|
65
63
|
@property
|
|
66
64
|
def is_structured(self):
|
|
@@ -70,13 +68,6 @@ class DataStore:
|
|
|
70
68
|
def is_unstructured(self):
|
|
71
69
|
return True
|
|
72
70
|
|
|
73
|
-
@staticmethod
|
|
74
|
-
def _sanitize_storage_options(options):
|
|
75
|
-
if not options:
|
|
76
|
-
return {}
|
|
77
|
-
options = {k: v for k, v in options.items() if v is not None and v != ""}
|
|
78
|
-
return options
|
|
79
|
-
|
|
80
71
|
@staticmethod
|
|
81
72
|
def _sanitize_url(url):
|
|
82
73
|
"""
|
|
@@ -106,15 +97,9 @@ class DataStore:
|
|
|
106
97
|
"""Whether the data store supports isdir"""
|
|
107
98
|
return True
|
|
108
99
|
|
|
109
|
-
def _get_secret_or_env(self, key, default=None):
|
|
110
|
-
# Project-secrets are mounted as env variables whose name can be retrieved from SecretsStore
|
|
111
|
-
return mlrun.get_secret_or_env(
|
|
112
|
-
key, secret_provider=self._get_secret, default=default
|
|
113
|
-
)
|
|
114
|
-
|
|
115
100
|
def get_storage_options(self):
|
|
116
101
|
"""get fsspec storage options"""
|
|
117
|
-
return self.
|
|
102
|
+
return self._sanitize_options(None)
|
|
118
103
|
|
|
119
104
|
def open(self, filepath, mode):
|
|
120
105
|
file_system = self.filesystem
|
|
@@ -125,16 +110,6 @@ class DataStore:
|
|
|
125
110
|
return f"{self.subpath}/{key}"
|
|
126
111
|
return key
|
|
127
112
|
|
|
128
|
-
def _get_parent_secret(self, key):
|
|
129
|
-
return self._parent.secret(self.secret_pfx + key)
|
|
130
|
-
|
|
131
|
-
def _get_secret(self, key: str, default=None):
|
|
132
|
-
return self._secrets.get(key, default) or self._get_parent_secret(key)
|
|
133
|
-
|
|
134
|
-
@property
|
|
135
|
-
def url(self):
|
|
136
|
-
return f"{self.kind}://{self.endpoint}"
|
|
137
|
-
|
|
138
113
|
@property
|
|
139
114
|
def spark_url(self):
|
|
140
115
|
return self.url
|
mlrun/datastore/datastore.py
CHANGED
|
@@ -11,50 +11,40 @@
|
|
|
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
15
|
import warnings
|
|
16
|
+
from functools import partial
|
|
15
17
|
from typing import Optional
|
|
16
|
-
from urllib.parse import urlparse
|
|
17
18
|
|
|
18
19
|
from mergedeep import merge
|
|
19
20
|
|
|
20
21
|
import mlrun
|
|
21
22
|
import mlrun.errors
|
|
23
|
+
from mlrun.artifacts.llm_prompt import LLMPromptArtifact
|
|
24
|
+
from mlrun.artifacts.model import ModelArtifact
|
|
22
25
|
from mlrun.datastore.datastore_profile import datastore_profile_read
|
|
26
|
+
from mlrun.datastore.model_provider.model_provider import (
|
|
27
|
+
ModelProvider,
|
|
28
|
+
)
|
|
29
|
+
from mlrun.datastore.remote_client import BaseRemoteClient
|
|
30
|
+
from mlrun.datastore.utils import (
|
|
31
|
+
parse_url,
|
|
32
|
+
)
|
|
23
33
|
from mlrun.errors import err_to_str
|
|
24
34
|
from mlrun.utils.helpers import get_local_file_schema
|
|
25
35
|
|
|
36
|
+
from ..artifacts.base import verify_target_path
|
|
26
37
|
from ..utils import DB_SCHEMA, RunKeys
|
|
27
38
|
from .base import DataItem, DataStore, HttpStore
|
|
28
39
|
from .filestore import FileStore
|
|
29
40
|
from .inmem import InMemoryStore
|
|
41
|
+
from .model_provider.openai_provider import OpenAIProvider
|
|
30
42
|
from .store_resources import get_store_resource, is_store_uri
|
|
31
43
|
from .v3io import V3ioStore
|
|
32
44
|
|
|
33
45
|
in_memory_store = InMemoryStore()
|
|
34
46
|
|
|
35
47
|
|
|
36
|
-
def parse_url(url):
|
|
37
|
-
if url and url.startswith("v3io://") and not url.startswith("v3io:///"):
|
|
38
|
-
url = url.replace("v3io://", "v3io:///", 1)
|
|
39
|
-
parsed_url = urlparse(url)
|
|
40
|
-
schema = parsed_url.scheme.lower()
|
|
41
|
-
endpoint = parsed_url.hostname
|
|
42
|
-
if endpoint:
|
|
43
|
-
# HACK - urlparse returns the hostname after in lower case - we want the original case:
|
|
44
|
-
# the hostname is a substring of the netloc, in which it's the original case, so we find the indexes of the
|
|
45
|
-
# hostname in the netloc and take it from there
|
|
46
|
-
lower_hostname = parsed_url.hostname
|
|
47
|
-
netloc = str(parsed_url.netloc)
|
|
48
|
-
lower_netloc = netloc.lower()
|
|
49
|
-
hostname_index_in_netloc = lower_netloc.index(str(lower_hostname))
|
|
50
|
-
endpoint = netloc[
|
|
51
|
-
hostname_index_in_netloc : hostname_index_in_netloc + len(lower_hostname)
|
|
52
|
-
]
|
|
53
|
-
if parsed_url.port:
|
|
54
|
-
endpoint += f":{parsed_url.port}"
|
|
55
|
-
return schema, endpoint, parsed_url
|
|
56
|
-
|
|
57
|
-
|
|
58
48
|
def schema_to_store(schema) -> DataStore.__subclasses__():
|
|
59
49
|
# import store classes inside to enable making their dependencies optional (package extras)
|
|
60
50
|
|
|
@@ -109,6 +99,20 @@ def schema_to_store(schema) -> DataStore.__subclasses__():
|
|
|
109
99
|
raise ValueError(f"unsupported store scheme ({schema})")
|
|
110
100
|
|
|
111
101
|
|
|
102
|
+
def schema_to_model_provider(
|
|
103
|
+
schema: str, raise_missing_schema_exception=True
|
|
104
|
+
) -> type[ModelProvider]:
|
|
105
|
+
# TODO add hugging face and http
|
|
106
|
+
schema_dict = {"openai": OpenAIProvider}
|
|
107
|
+
provider_class = schema_dict.get(schema, None)
|
|
108
|
+
if not provider_class:
|
|
109
|
+
if raise_missing_schema_exception:
|
|
110
|
+
raise ValueError(f"unsupported model provider schema ({schema})")
|
|
111
|
+
else:
|
|
112
|
+
warnings.warn(f"unsupported model provider schema: {schema}")
|
|
113
|
+
return provider_class
|
|
114
|
+
|
|
115
|
+
|
|
112
116
|
def uri_to_ipython(link):
|
|
113
117
|
schema, endpoint, parsed_url = parse_url(link)
|
|
114
118
|
if schema in [DB_SCHEMA, "memory", "ds"]:
|
|
@@ -159,7 +163,11 @@ class StoreManager:
|
|
|
159
163
|
self._stores[store.name] = store
|
|
160
164
|
|
|
161
165
|
def get_store_artifact(
|
|
162
|
-
self,
|
|
166
|
+
self,
|
|
167
|
+
url,
|
|
168
|
+
project="",
|
|
169
|
+
allow_empty_resources=None,
|
|
170
|
+
secrets=None,
|
|
163
171
|
):
|
|
164
172
|
"""
|
|
165
173
|
This is expected to be run only on client side. server is not expected to load artifacts.
|
|
@@ -175,12 +183,21 @@ class StoreManager:
|
|
|
175
183
|
except Exception as exc:
|
|
176
184
|
raise OSError(f"artifact {url} not found, {err_to_str(exc)}")
|
|
177
185
|
target = resource.get_target_path()
|
|
186
|
+
|
|
178
187
|
# the allow_empty.. flag allows us to have functions which dont depend on having targets e.g. a function
|
|
179
188
|
# which accepts a feature vector uri and generate the offline vector (parquet) for it if it doesnt exist
|
|
180
|
-
if not
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
189
|
+
if not allow_empty_resources:
|
|
190
|
+
if isinstance(resource, LLMPromptArtifact):
|
|
191
|
+
if not resource.spec.model_uri:
|
|
192
|
+
raise mlrun.errors.MLRunInvalidArgumentError(
|
|
193
|
+
f"LLMPromptArtifact {url} does not contain model artifact uri"
|
|
194
|
+
)
|
|
195
|
+
elif not target and not (
|
|
196
|
+
isinstance(resource, ModelArtifact) and resource.model_url
|
|
197
|
+
):
|
|
198
|
+
raise mlrun.errors.MLRunInvalidArgumentError(
|
|
199
|
+
f"Resource {url} does not have a valid/persistent offline target or model_url"
|
|
200
|
+
)
|
|
184
201
|
return resource, target or ""
|
|
185
202
|
|
|
186
203
|
def object(
|
|
@@ -190,6 +207,7 @@ class StoreManager:
|
|
|
190
207
|
project="",
|
|
191
208
|
allow_empty_resources=None,
|
|
192
209
|
secrets: Optional[dict] = None,
|
|
210
|
+
**kwargs,
|
|
193
211
|
) -> DataItem:
|
|
194
212
|
meta = artifact_url = None
|
|
195
213
|
if is_store_uri(url):
|
|
@@ -197,6 +215,8 @@ class StoreManager:
|
|
|
197
215
|
meta, url = self.get_store_artifact(
|
|
198
216
|
url, project, allow_empty_resources, secrets
|
|
199
217
|
)
|
|
218
|
+
if not allow_empty_resources:
|
|
219
|
+
verify_target_path(meta)
|
|
200
220
|
|
|
201
221
|
store, subpath, url = self.get_or_create_store(
|
|
202
222
|
url, secrets=secrets, project_name=project
|
|
@@ -218,7 +238,7 @@ class StoreManager:
|
|
|
218
238
|
cache: Optional[dict] = None,
|
|
219
239
|
schema_to_class: callable = schema_to_store,
|
|
220
240
|
**kwargs,
|
|
221
|
-
) -> (
|
|
241
|
+
) -> (BaseRemoteClient, str, str):
|
|
222
242
|
# The cache can be an empty dictionary ({}), even if it is a _stores object
|
|
223
243
|
cache = cache if cache is not None else {}
|
|
224
244
|
schema, endpoint, parsed_url = parse_url(url)
|
|
@@ -227,10 +247,7 @@ class StoreManager:
|
|
|
227
247
|
|
|
228
248
|
if schema == "ds":
|
|
229
249
|
datastore_profile = datastore_profile_read(url, project_name, secrets)
|
|
230
|
-
|
|
231
|
-
secrets = merge(secrets, datastore_profile.secrets())
|
|
232
|
-
else:
|
|
233
|
-
secrets = secrets or datastore_profile.secrets()
|
|
250
|
+
secrets = merge(secrets or {}, datastore_profile.secrets() or {})
|
|
234
251
|
url = datastore_profile.url(subpath)
|
|
235
252
|
schema, endpoint, parsed_url = parse_url(url)
|
|
236
253
|
subpath = parsed_url.path
|
|
@@ -260,6 +277,9 @@ class StoreManager:
|
|
|
260
277
|
remote_client_class = schema_to_class(schema)
|
|
261
278
|
remote_client = None
|
|
262
279
|
if remote_client_class:
|
|
280
|
+
endpoint, subpath = remote_client_class.parse_endpoint_and_path(
|
|
281
|
+
endpoint, subpath
|
|
282
|
+
)
|
|
263
283
|
remote_client = remote_client_class(
|
|
264
284
|
self, schema, cache_key, parsed_url.netloc, secrets=secrets, **kwargs
|
|
265
285
|
)
|
|
@@ -288,5 +308,61 @@ class StoreManager:
|
|
|
288
308
|
)
|
|
289
309
|
return datastore, sub_path, url
|
|
290
310
|
|
|
311
|
+
def get_or_create_model_provider(
|
|
312
|
+
self,
|
|
313
|
+
url,
|
|
314
|
+
secrets: Optional[dict] = None,
|
|
315
|
+
project_name="",
|
|
316
|
+
default_invoke_kwargs: Optional[dict] = None,
|
|
317
|
+
raise_missing_schema_exception=True,
|
|
318
|
+
) -> ModelProvider:
|
|
319
|
+
schema_to_provider_with_raise = partial(
|
|
320
|
+
schema_to_model_provider,
|
|
321
|
+
raise_missing_schema_exception=raise_missing_schema_exception,
|
|
322
|
+
)
|
|
323
|
+
model_provider, _, _ = self._get_or_create_remote_client(
|
|
324
|
+
url=url,
|
|
325
|
+
secrets=secrets,
|
|
326
|
+
project_name=project_name,
|
|
327
|
+
schema_to_class=schema_to_provider_with_raise,
|
|
328
|
+
default_invoke_kwargs=default_invoke_kwargs,
|
|
329
|
+
)
|
|
330
|
+
if model_provider and not isinstance(model_provider, ModelProvider):
|
|
331
|
+
raise mlrun.errors.MLRunInvalidArgumentError(
|
|
332
|
+
"remote client by url is not model_provider"
|
|
333
|
+
)
|
|
334
|
+
return model_provider
|
|
335
|
+
|
|
291
336
|
def reset_secrets(self):
|
|
292
337
|
self._secrets = {}
|
|
338
|
+
|
|
339
|
+
def model_provider_object(
|
|
340
|
+
self,
|
|
341
|
+
url,
|
|
342
|
+
project="",
|
|
343
|
+
allow_empty_resources=None,
|
|
344
|
+
secrets: Optional[dict] = None,
|
|
345
|
+
default_invoke_kwargs: Optional[dict] = None,
|
|
346
|
+
raise_missing_schema_exception=True,
|
|
347
|
+
) -> ModelProvider:
|
|
348
|
+
if mlrun.datastore.is_store_uri(url):
|
|
349
|
+
resource = self.get_store_artifact(
|
|
350
|
+
url,
|
|
351
|
+
project,
|
|
352
|
+
allow_empty_resources,
|
|
353
|
+
secrets,
|
|
354
|
+
)
|
|
355
|
+
if not isinstance(resource, ModelArtifact) or not resource.model_url:
|
|
356
|
+
raise mlrun.errors.MLRunInvalidArgumentError(
|
|
357
|
+
"unable to create the model provider from the given resource URI"
|
|
358
|
+
)
|
|
359
|
+
url = resource.model_url
|
|
360
|
+
default_invoke_kwargs = default_invoke_kwargs or resource.default_config
|
|
361
|
+
model_provider = self.get_or_create_model_provider(
|
|
362
|
+
url,
|
|
363
|
+
secrets=secrets,
|
|
364
|
+
project_name=project,
|
|
365
|
+
default_invoke_kwargs=default_invoke_kwargs,
|
|
366
|
+
raise_missing_schema_exception=raise_missing_schema_exception,
|
|
367
|
+
)
|
|
368
|
+
return model_provider
|
|
@@ -456,6 +456,36 @@ class DatastoreProfileTDEngine(DatastoreProfile):
|
|
|
456
456
|
)
|
|
457
457
|
|
|
458
458
|
|
|
459
|
+
class OpenAIProfile(DatastoreProfile):
|
|
460
|
+
type: str = pydantic.v1.Field("openai")
|
|
461
|
+
_private_attributes = "api_key"
|
|
462
|
+
api_key: typing.Optional[str] = None
|
|
463
|
+
organization: typing.Optional[str] = None
|
|
464
|
+
project: typing.Optional[str] = None
|
|
465
|
+
base_url: typing.Optional[str] = None
|
|
466
|
+
timeout: typing.Optional[float] = None
|
|
467
|
+
max_retries: typing.Optional[int] = None
|
|
468
|
+
|
|
469
|
+
def secrets(self) -> dict:
|
|
470
|
+
res = {}
|
|
471
|
+
if self.api_key:
|
|
472
|
+
res["OPENAI_API_KEY"] = self.api_key
|
|
473
|
+
if self.organization:
|
|
474
|
+
res["OPENAI_ORG_ID"] = self.organization
|
|
475
|
+
if self.project:
|
|
476
|
+
res["OPENAI_PROJECT_ID"] = self.project
|
|
477
|
+
if self.base_url:
|
|
478
|
+
res["OPENAI_BASE_URL"] = self.base_url
|
|
479
|
+
if self.timeout:
|
|
480
|
+
res["OPENAI_TIMEOUT"] = self.timeout
|
|
481
|
+
if self.max_retries:
|
|
482
|
+
res["OPENAI_MAX_RETRIES"] = self.max_retries
|
|
483
|
+
return res
|
|
484
|
+
|
|
485
|
+
def url(self, subpath):
|
|
486
|
+
return f"{self.type}://{subpath.lstrip('/')}"
|
|
487
|
+
|
|
488
|
+
|
|
459
489
|
_DATASTORE_TYPE_TO_PROFILE_CLASS: dict[str, type[DatastoreProfile]] = {
|
|
460
490
|
"v3io": DatastoreProfileV3io,
|
|
461
491
|
"s3": DatastoreProfileS3,
|
|
@@ -469,6 +499,7 @@ _DATASTORE_TYPE_TO_PROFILE_CLASS: dict[str, type[DatastoreProfile]] = {
|
|
|
469
499
|
"hdfs": DatastoreProfileHdfs,
|
|
470
500
|
"taosws": DatastoreProfileTDEngine,
|
|
471
501
|
"config": ConfigProfile,
|
|
502
|
+
"openai": OpenAIProfile,
|
|
472
503
|
}
|
|
473
504
|
|
|
474
505
|
|
mlrun/datastore/dbfs_store.py
CHANGED
|
@@ -104,7 +104,7 @@ class DBFSStore(DataStore):
|
|
|
104
104
|
token=self._get_secret_or_env("DATABRICKS_TOKEN"),
|
|
105
105
|
instance=self._get_secret_or_env("DATABRICKS_HOST"),
|
|
106
106
|
)
|
|
107
|
-
return self.
|
|
107
|
+
return self._sanitize_options(res)
|
|
108
108
|
|
|
109
109
|
def _verify_filesystem_and_key(self, key: str):
|
|
110
110
|
if not self.filesystem:
|
|
@@ -105,12 +105,12 @@ class GoogleCloudStorageStore(DataStore):
|
|
|
105
105
|
except json.JSONDecodeError:
|
|
106
106
|
# If it's not json, handle it as a filename
|
|
107
107
|
token = credentials
|
|
108
|
-
return self.
|
|
108
|
+
return self._sanitize_options(dict(token=token))
|
|
109
109
|
else:
|
|
110
110
|
logger.info(
|
|
111
111
|
"No GCS credentials available - auth will rely on auto-discovery of credentials"
|
|
112
112
|
)
|
|
113
|
-
return self.
|
|
113
|
+
return self._sanitize_options(None)
|
|
114
114
|
|
|
115
115
|
def get_storage_options(self):
|
|
116
116
|
return self.storage_options
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Copyright 2023 Iguazio
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|