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.

Files changed (54) hide show
  1. mlrun/__init__.py +2 -1
  2. mlrun/__main__.py +7 -1
  3. mlrun/artifacts/base.py +9 -3
  4. mlrun/artifacts/dataset.py +2 -1
  5. mlrun/artifacts/llm_prompt.py +1 -1
  6. mlrun/artifacts/model.py +2 -2
  7. mlrun/common/constants.py +1 -0
  8. mlrun/common/runtimes/constants.py +10 -1
  9. mlrun/config.py +19 -2
  10. mlrun/datastore/__init__.py +3 -1
  11. mlrun/datastore/alibaba_oss.py +1 -1
  12. mlrun/datastore/azure_blob.py +1 -1
  13. mlrun/datastore/base.py +6 -31
  14. mlrun/datastore/datastore.py +109 -33
  15. mlrun/datastore/datastore_profile.py +31 -0
  16. mlrun/datastore/dbfs_store.py +1 -1
  17. mlrun/datastore/google_cloud_storage.py +2 -2
  18. mlrun/datastore/model_provider/__init__.py +13 -0
  19. mlrun/datastore/model_provider/model_provider.py +82 -0
  20. mlrun/datastore/model_provider/openai_provider.py +120 -0
  21. mlrun/datastore/remote_client.py +54 -0
  22. mlrun/datastore/s3.py +1 -1
  23. mlrun/datastore/storeytargets.py +1 -1
  24. mlrun/datastore/utils.py +22 -0
  25. mlrun/datastore/v3io.py +1 -1
  26. mlrun/db/base.py +1 -1
  27. mlrun/db/httpdb.py +9 -4
  28. mlrun/db/nopdb.py +1 -1
  29. mlrun/execution.py +23 -7
  30. mlrun/launcher/base.py +23 -13
  31. mlrun/launcher/local.py +3 -1
  32. mlrun/launcher/remote.py +4 -2
  33. mlrun/model.py +65 -0
  34. mlrun/package/packagers_manager.py +2 -0
  35. mlrun/projects/operations.py +8 -1
  36. mlrun/projects/project.py +23 -5
  37. mlrun/run.py +17 -0
  38. mlrun/runtimes/__init__.py +6 -0
  39. mlrun/runtimes/base.py +24 -6
  40. mlrun/runtimes/daskjob.py +1 -0
  41. mlrun/runtimes/databricks_job/databricks_runtime.py +1 -0
  42. mlrun/runtimes/local.py +1 -6
  43. mlrun/serving/server.py +0 -2
  44. mlrun/serving/states.py +30 -5
  45. mlrun/serving/system_steps.py +22 -28
  46. mlrun/utils/helpers.py +13 -2
  47. mlrun/utils/notifications/notification_pusher.py +15 -0
  48. mlrun/utils/version/version.json +2 -2
  49. {mlrun-1.10.0rc11.dist-info → mlrun-1.10.0rc12.dist-info}/METADATA +2 -2
  50. {mlrun-1.10.0rc11.dist-info → mlrun-1.10.0rc12.dist-info}/RECORD +54 -50
  51. {mlrun-1.10.0rc11.dist-info → mlrun-1.10.0rc12.dist-info}/WHEEL +0 -0
  52. {mlrun-1.10.0rc11.dist-info → mlrun-1.10.0rc12.dist-info}/entry_points.txt +0 -0
  53. {mlrun-1.10.0rc11.dist-info → mlrun-1.10.0rc12.dist-info}/licenses/LICENSE +0 -0
  54. {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
- runobj = RunTemplate.from_dict(config)
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, target = mlrun.datastore.store_manager.get_store_artifact(
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
+ )
@@ -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
 
@@ -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, target = (
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 preform any
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": "15",
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()
@@ -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,
@@ -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._sanitize_storage_options(res)
72
+ return self._sanitize_options(res)
73
73
 
74
74
  def get_bucket_and_key(self, key):
75
75
  path = self._join(key)[1:]
@@ -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._sanitize_storage_options(res)
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
- self._parent = parent
55
- self.kind = kind
56
- self.name = name
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._sanitize_storage_options(None)
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
@@ -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, url, project="", allow_empty_resources=None, secrets=None
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 target and not allow_empty_resources:
181
- raise mlrun.errors.MLRunInvalidArgumentError(
182
- f"Resource {url} does not have a valid/persistent offline target"
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
- ) -> (DataStore, str, str):
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
- if secrets and datastore_profile.secrets():
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
 
@@ -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._sanitize_storage_options(res)
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._sanitize_storage_options(dict(token=token))
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._sanitize_storage_options(None)
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.