mlrun 1.6.2rc5__py3-none-any.whl → 1.6.3__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/artifacts/model.py CHANGED
@@ -13,8 +13,9 @@
13
13
  # limitations under the License.
14
14
  import tempfile
15
15
  from os import path
16
- from typing import List
16
+ from typing import Any
17
17
 
18
+ import pandas as pd
18
19
  import yaml
19
20
  from deprecated import deprecated
20
21
 
@@ -68,8 +69,8 @@ class ModelArtifactSpec(ArtifactSpec):
68
69
  model_file=None,
69
70
  metrics=None,
70
71
  paraemeters=None,
71
- inputs: List[Feature] = None,
72
- outputs: List[Feature] = None,
72
+ inputs: list[Feature] = None,
73
+ outputs: list[Feature] = None,
73
74
  framework=None,
74
75
  algorithm=None,
75
76
  feature_vector=None,
@@ -91,8 +92,8 @@ class ModelArtifactSpec(ArtifactSpec):
91
92
  self.model_file = model_file
92
93
  self.metrics = metrics or {}
93
94
  self.parameters = paraemeters or {}
94
- self.inputs: List[Feature] = inputs or []
95
- self.outputs: List[Feature] = outputs or []
95
+ self.inputs: list[Feature] = inputs or []
96
+ self.outputs: list[Feature] = outputs or []
96
97
  self.framework = framework
97
98
  self.algorithm = algorithm
98
99
  self.feature_vector = feature_vector
@@ -101,21 +102,21 @@ class ModelArtifactSpec(ArtifactSpec):
101
102
  self.model_target_file = model_target_file
102
103
 
103
104
  @property
104
- def inputs(self) -> List[Feature]:
105
+ def inputs(self) -> list[Feature]:
105
106
  """input feature list"""
106
107
  return self._inputs
107
108
 
108
109
  @inputs.setter
109
- def inputs(self, inputs: List[Feature]):
110
+ def inputs(self, inputs: list[Feature]):
110
111
  self._inputs = ObjectList.from_list(Feature, inputs)
111
112
 
112
113
  @property
113
- def outputs(self) -> List[Feature]:
114
+ def outputs(self) -> list[Feature]:
114
115
  """output feature list"""
115
116
  return self._outputs
116
117
 
117
118
  @outputs.setter
118
- def outputs(self, outputs: List[Feature]):
119
+ def outputs(self, outputs: list[Feature]):
119
120
  self._outputs = ObjectList.from_list(Feature, outputs)
120
121
 
121
122
 
@@ -175,22 +176,22 @@ class ModelArtifact(Artifact):
175
176
  self._spec = self._verify_dict(spec, "spec", ModelArtifactSpec)
176
177
 
177
178
  @property
178
- def inputs(self) -> List[Feature]:
179
+ def inputs(self) -> list[Feature]:
179
180
  """input feature list"""
180
181
  return self.spec.inputs
181
182
 
182
183
  @inputs.setter
183
- def inputs(self, inputs: List[Feature]):
184
+ def inputs(self, inputs: list[Feature]):
184
185
  """input feature list"""
185
186
  self.spec.inputs = inputs
186
187
 
187
188
  @property
188
- def outputs(self) -> List[Feature]:
189
+ def outputs(self) -> list[Feature]:
189
190
  """input feature list"""
190
191
  return self.spec.outputs
191
192
 
192
193
  @outputs.setter
193
- def outputs(self, outputs: List[Feature]):
194
+ def outputs(self, outputs: list[Feature]):
194
195
  """input feature list"""
195
196
  self.spec.outputs = outputs
196
197
 
@@ -260,6 +261,7 @@ class ModelArtifact(Artifact):
260
261
  """
261
262
  subset = df
262
263
  inferer = get_infer_interface(subset)
264
+ numeric_columns = self._extract_numeric_features(df)
263
265
  if label_columns:
264
266
  if not isinstance(label_columns, list):
265
267
  label_columns = [label_columns]
@@ -273,9 +275,13 @@ class ModelArtifact(Artifact):
273
275
  )
274
276
  if with_stats:
275
277
  self.spec.feature_stats = inferer.get_stats(
276
- df, options=InferOptions.Histogram, num_bins=num_bins
278
+ df[numeric_columns], options=InferOptions.Histogram, num_bins=num_bins
277
279
  )
278
280
 
281
+ @staticmethod
282
+ def _extract_numeric_features(df: pd.DataFrame) -> list[Any]:
283
+ return [col for col in df.columns if pd.api.types.is_numeric_dtype(df[col])]
284
+
279
285
  @property
280
286
  def is_dir(self):
281
287
  return True
@@ -445,8 +451,8 @@ class LegacyModelArtifact(LegacyArtifact):
445
451
  self.model_file = model_file
446
452
  self.parameters = parameters or {}
447
453
  self.metrics = metrics or {}
448
- self.inputs: List[Feature] = inputs or []
449
- self.outputs: List[Feature] = outputs or []
454
+ self.inputs: list[Feature] = inputs or []
455
+ self.outputs: list[Feature] = outputs or []
450
456
  self.extra_data = extra_data or {}
451
457
  self.framework = framework
452
458
  self.algorithm = algorithm
@@ -456,21 +462,21 @@ class LegacyModelArtifact(LegacyArtifact):
456
462
  self.model_target_file = model_target_file
457
463
 
458
464
  @property
459
- def inputs(self) -> List[Feature]:
465
+ def inputs(self) -> list[Feature]:
460
466
  """input feature list"""
461
467
  return self._inputs
462
468
 
463
469
  @inputs.setter
464
- def inputs(self, inputs: List[Feature]):
470
+ def inputs(self, inputs: list[Feature]):
465
471
  self._inputs = ObjectList.from_list(Feature, inputs)
466
472
 
467
473
  @property
468
- def outputs(self) -> List[Feature]:
474
+ def outputs(self) -> list[Feature]:
469
475
  """output feature list"""
470
476
  return self._outputs
471
477
 
472
478
  @outputs.setter
473
- def outputs(self, outputs: List[Feature]):
479
+ def outputs(self, outputs: list[Feature]):
474
480
  self._outputs = ObjectList.from_list(Feature, outputs)
475
481
 
476
482
  def infer_from_df(self, df, label_columns=None, with_stats=True, num_bins=None):
@@ -642,8 +648,8 @@ def update_model(
642
648
  parameters: dict = None,
643
649
  metrics: dict = None,
644
650
  extra_data: dict = None,
645
- inputs: List[Feature] = None,
646
- outputs: List[Feature] = None,
651
+ inputs: list[Feature] = None,
652
+ outputs: list[Feature] = None,
647
653
  feature_vector: str = None,
648
654
  feature_weights: list = None,
649
655
  key_prefix: str = "",
@@ -114,6 +114,7 @@ from .model_monitoring import (
114
114
  EventFieldType,
115
115
  EventKeyMetrics,
116
116
  Features,
117
+ FeatureSetFeatures,
117
118
  FeatureValues,
118
119
  GrafanaColumn,
119
120
  GrafanaDataPoint,
@@ -22,6 +22,7 @@ from .constants import (
22
22
  EventFieldType,
23
23
  EventKeyMetrics,
24
24
  EventLiveStats,
25
+ FeatureSetFeatures,
25
26
  FileTargetKind,
26
27
  FunctionURI,
27
28
  ModelEndpointTarget,
@@ -77,6 +77,26 @@ class EventFieldType:
77
77
  SAMPLE_PARQUET_PATH = "sample_parquet_path"
78
78
 
79
79
 
80
+ class MonitoringStrEnum(StrEnum):
81
+ @classmethod
82
+ def list(cls):
83
+ return list(map(lambda c: c.value, cls))
84
+
85
+
86
+ class FeatureSetFeatures(MonitoringStrEnum):
87
+ LATENCY = EventFieldType.LATENCY
88
+ ERROR_COUNT = EventFieldType.ERROR_COUNT
89
+ METRICS = EventFieldType.METRICS
90
+
91
+ @classmethod
92
+ def time_stamp(cls):
93
+ return EventFieldType.TIMESTAMP
94
+
95
+ @classmethod
96
+ def entity(cls):
97
+ return EventFieldType.ENDPOINT_ID
98
+
99
+
80
100
  class ApplicationEvent:
81
101
  APPLICATION_NAME = "application_name"
82
102
  CURRENT_STATS = "current_stats"
@@ -89,7 +109,7 @@ class ApplicationEvent:
89
109
  OUTPUT_STREAM_URI = "output_stream_uri"
90
110
 
91
111
 
92
- class WriterEvent(StrEnum):
112
+ class WriterEvent(MonitoringStrEnum):
93
113
  APPLICATION_NAME = "application_name"
94
114
  ENDPOINT_ID = "endpoint_id"
95
115
  START_INFER_TIME = "start_infer_time"
@@ -101,10 +121,6 @@ class WriterEvent(StrEnum):
101
121
  RESULT_EXTRA_DATA = "result_extra_data"
102
122
  CURRENT_STATS = "current_stats"
103
123
 
104
- @classmethod
105
- def list(cls):
106
- return list(map(lambda c: c.value, cls))
107
-
108
124
 
109
125
  class EventLiveStats:
110
126
  LATENCY_AVG_5M = "latency_avg_5m"
mlrun/config.py CHANGED
@@ -611,8 +611,9 @@ default_config = {
611
611
  },
612
612
  "workflows": {
613
613
  "default_workflow_runner_name": "workflow-runner-{}",
614
- # Default timeout seconds for retrieving workflow id after execution:
615
- "timeouts": {"local": 120, "kfp": 30, "remote": 90},
614
+ # Default timeout seconds for retrieving workflow id after execution
615
+ # Remote workflow timeout is the maximum between remote and the inner engine timeout
616
+ "timeouts": {"local": 120, "kfp": 60, "remote": 60 * 5},
616
617
  },
617
618
  "log_collector": {
618
619
  "address": "localhost:8282",
@@ -671,6 +672,10 @@ default_config = {
671
672
  "access_key": "",
672
673
  },
673
674
  "grafana_url": "",
675
+ "auth_with_client_id": {
676
+ "enabled": False,
677
+ "request_timeout": 5,
678
+ },
674
679
  }
675
680
 
676
681
  _is_running_as_api = None
@@ -1061,7 +1066,7 @@ class Config:
1061
1066
  target: str = "online",
1062
1067
  artifact_path: str = None,
1063
1068
  application_name: str = None,
1064
- ) -> str:
1069
+ ) -> typing.Union[str, list[str]]:
1065
1070
  """Get the full path from the configuration based on the provided project and kind.
1066
1071
 
1067
1072
  :param project: Project name.
@@ -1077,7 +1082,8 @@ class Config:
1077
1082
  relative artifact path will be taken from the global MLRun artifact path.
1078
1083
  :param application_name: Application name, None for model_monitoring_stream.
1079
1084
 
1080
- :return: Full configured path for the provided kind.
1085
+ :return: Full configured path for the provided kind. Can be either a single path
1086
+ or a list of paths in the case of the online model monitoring stream path.
1081
1087
  """
1082
1088
 
1083
1089
  if target != "offline":
@@ -1098,12 +1104,22 @@ class Config:
1098
1104
  if application_name is None
1099
1105
  else f"{kind}-{application_name.lower()}",
1100
1106
  )
1101
- return mlrun.mlconf.model_endpoint_monitoring.store_prefixes.default.format(
1102
- project=project,
1103
- kind=kind
1104
- if application_name is None
1105
- else f"{kind}-{application_name.lower()}",
1106
- )
1107
+ elif kind == "stream": # return list for mlrun<1.6.3 BC
1108
+ return [
1109
+ mlrun.mlconf.model_endpoint_monitoring.store_prefixes.default.format(
1110
+ project=project,
1111
+ kind=kind,
1112
+ ), # old stream uri (pipelines) for BC ML-6043
1113
+ mlrun.mlconf.model_endpoint_monitoring.store_prefixes.user_space.format(
1114
+ project=project,
1115
+ kind=kind,
1116
+ ), # new stream uri (projects)
1117
+ ]
1118
+ else:
1119
+ return mlrun.mlconf.model_endpoint_monitoring.store_prefixes.default.format(
1120
+ project=project,
1121
+ kind=kind,
1122
+ )
1107
1123
 
1108
1124
  # Get the current offline path from the configuration
1109
1125
  file_path = mlrun.mlconf.model_endpoint_monitoring.offline_storage_path.format(
@@ -1360,10 +1376,14 @@ def read_env(env=None, prefix=env_prefix):
1360
1376
  if log_formatter_name := config.get("log_formatter"):
1361
1377
  import mlrun.utils.logger
1362
1378
 
1363
- log_formatter = mlrun.utils.create_formatter_instance(
1379
+ log_formatter = mlrun.utils.resolve_formatter_by_kind(
1364
1380
  mlrun.utils.FormatterKinds(log_formatter_name)
1365
1381
  )
1366
- mlrun.utils.logger.get_handler("default").setFormatter(log_formatter)
1382
+ current_handler = mlrun.utils.logger.get_handler("default")
1383
+ current_formatter_name = current_handler.formatter.__class__.__name__
1384
+ desired_formatter_name = log_formatter.__name__
1385
+ if current_formatter_name != desired_formatter_name:
1386
+ current_handler.setFormatter(log_formatter())
1367
1387
 
1368
1388
  # The default function pod resource values are of type str; however, when reading from environment variable numbers,
1369
1389
  # it converts them to type int if contains only number, so we want to convert them to str.
@@ -41,6 +41,7 @@ class ValueType(str, Enum):
41
41
  BYTES = "bytes"
42
42
  STRING = "str"
43
43
  DATETIME = "datetime"
44
+ LIST = "List"
44
45
  BYTES_LIST = "List[bytes]"
45
46
  STRING_LIST = "List[string]"
46
47
  INT32_LIST = "List[int32]"
@@ -48,6 +49,7 @@ class ValueType(str, Enum):
48
49
  DOUBLE_LIST = "List[float]"
49
50
  FLOAT_LIST = "List[float32]"
50
51
  BOOL_LIST = "List[bool]"
52
+ Tuple = "Tuple"
51
53
 
52
54
 
53
55
  def pd_schema_to_value_type(value):
@@ -102,6 +104,8 @@ def python_type_to_value_type(value_type):
102
104
  "datetime64[ns]": ValueType.INT64,
103
105
  "datetime64[ns, tz]": ValueType.INT64,
104
106
  "category": ValueType.STRING,
107
+ "list": ValueType.LIST,
108
+ "tuple": ValueType.Tuple,
105
109
  }
106
110
 
107
111
  if type_name in type_map:
mlrun/datastore/v3io.py CHANGED
@@ -12,8 +12,6 @@
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
14
 
15
- import mmap
16
- import os
17
15
  import time
18
16
  from datetime import datetime
19
17
 
@@ -22,7 +20,6 @@ import v3io
22
20
  from v3io.dataplane.response import HttpResponseError
23
21
 
24
22
  import mlrun
25
- from mlrun.datastore.helpers import ONE_GB, ONE_MB
26
23
 
27
24
  from ..platforms.iguazio import parse_path, split_path
28
25
  from .base import (
@@ -32,6 +29,7 @@ from .base import (
32
29
  )
33
30
 
34
31
  V3IO_LOCAL_ROOT = "v3io"
32
+ V3IO_DEFAULT_UPLOAD_CHUNK_SIZE = 1024 * 1024 * 100
35
33
 
36
34
 
37
35
  class V3ioStore(DataStore):
@@ -94,46 +92,28 @@ class V3ioStore(DataStore):
94
92
  )
95
93
  return self._sanitize_storage_options(res)
96
94
 
97
- def _upload(self, key: str, src_path: str, max_chunk_size: int = ONE_GB):
95
+ def _upload(
96
+ self,
97
+ key: str,
98
+ src_path: str,
99
+ max_chunk_size: int = V3IO_DEFAULT_UPLOAD_CHUNK_SIZE,
100
+ ):
98
101
  """helper function for upload method, allows for controlling max_chunk_size in testing"""
99
102
  container, path = split_path(self._join(key))
100
- file_size = os.path.getsize(src_path) # in bytes
101
- if file_size <= ONE_MB:
102
- with open(src_path, "rb") as source_file:
103
- data = source_file.read()
104
- self._do_object_request(
105
- self.object.put,
106
- container=container,
107
- path=path,
108
- body=data,
109
- append=False,
110
- )
111
- return
112
- # chunk must be a multiple of the ALLOCATIONGRANULARITY
113
- # https://docs.python.org/3/library/mmap.html
114
- if residue := max_chunk_size % mmap.ALLOCATIONGRANULARITY:
115
- # round down to the nearest multiple of ALLOCATIONGRANULARITY
116
- max_chunk_size -= residue
117
-
118
103
  with open(src_path, "rb") as file_obj:
119
- file_offset = 0
120
- while file_offset < file_size:
121
- chunk_size = min(file_size - file_offset, max_chunk_size)
122
- with mmap.mmap(
123
- file_obj.fileno(),
124
- length=chunk_size,
125
- access=mmap.ACCESS_READ,
126
- offset=file_offset,
127
- ) as mmap_obj:
128
- append = file_offset != 0
129
- self._do_object_request(
130
- self.object.put,
131
- container=container,
132
- path=path,
133
- body=mmap_obj,
134
- append=append,
135
- )
136
- file_offset += chunk_size
104
+ append = False
105
+ while True:
106
+ data = memoryview(file_obj.read(max_chunk_size))
107
+ if not data:
108
+ break
109
+ self._do_object_request(
110
+ self.object.put,
111
+ container=container,
112
+ path=path,
113
+ body=data,
114
+ append=append,
115
+ )
116
+ append = True
137
117
 
138
118
  def upload(self, key, src_path):
139
119
  return self._upload(key, src_path)
@@ -148,19 +128,16 @@ class V3ioStore(DataStore):
148
128
  num_bytes=size,
149
129
  ).body
150
130
 
151
- def _put(self, key, data, append=False, max_chunk_size: int = ONE_GB):
131
+ def _put(
132
+ self,
133
+ key,
134
+ data,
135
+ append=False,
136
+ max_chunk_size: int = V3IO_DEFAULT_UPLOAD_CHUNK_SIZE,
137
+ ):
152
138
  """helper function for put method, allows for controlling max_chunk_size in testing"""
153
139
  container, path = split_path(self._join(key))
154
140
  buffer_size = len(data) # in bytes
155
- if buffer_size <= ONE_MB:
156
- self._do_object_request(
157
- self.object.put,
158
- container=container,
159
- path=path,
160
- body=data,
161
- append=append,
162
- )
163
- return
164
141
  buffer_offset = 0
165
142
  try:
166
143
  data = memoryview(data)
mlrun/db/auth_utils.py ADDED
@@ -0,0 +1,152 @@
1
+ # Copyright 2024 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.
14
+
15
+ from abc import ABC, abstractmethod
16
+ from datetime import datetime, timedelta
17
+
18
+ import requests
19
+
20
+ import mlrun.errors
21
+ from mlrun.utils import logger
22
+
23
+
24
+ class TokenProvider(ABC):
25
+ @abstractmethod
26
+ def get_token(self):
27
+ pass
28
+
29
+ @abstractmethod
30
+ def is_iguazio_session(self):
31
+ pass
32
+
33
+
34
+ class StaticTokenProvider(TokenProvider):
35
+ def __init__(self, token: str):
36
+ self.token = token
37
+
38
+ def get_token(self):
39
+ return self.token
40
+
41
+ def is_iguazio_session(self):
42
+ return mlrun.platforms.iguazio.is_iguazio_session(self.token)
43
+
44
+
45
+ class OAuthClientIDTokenProvider(TokenProvider):
46
+ def __init__(
47
+ self, token_endpoint: str, client_id: str, client_secret: str, timeout=5
48
+ ):
49
+ if not token_endpoint or not client_id or not client_secret:
50
+ raise mlrun.errors.MLRunValueError(
51
+ "Invalid client_id configuration for authentication. Must provide token endpoint, client-id and secret"
52
+ )
53
+ self.token_endpoint = token_endpoint
54
+ self.client_id = client_id
55
+ self.client_secret = client_secret
56
+ self.timeout = timeout
57
+
58
+ # Since we're only issuing POST requests, which are actually a disguised GET, then it's ok to allow retries
59
+ # on them.
60
+ self._session = mlrun.utils.HTTPSessionWithRetry(
61
+ retry_on_post=True,
62
+ verbose=True,
63
+ )
64
+
65
+ self._cleanup()
66
+ self._refresh_token_if_needed()
67
+
68
+ def get_token(self):
69
+ self._refresh_token_if_needed()
70
+ return self.token
71
+
72
+ def is_iguazio_session(self):
73
+ return False
74
+
75
+ def _cleanup(self):
76
+ self.token = self.token_expiry_time = self.token_refresh_time = None
77
+
78
+ def _refresh_token_if_needed(self):
79
+ now = datetime.now()
80
+ if self.token:
81
+ if self.token_refresh_time and now <= self.token_refresh_time:
82
+ return self.token
83
+
84
+ # We only cleanup if token was really expired - even if we fail in refreshing the token, we can still
85
+ # use the existing one given that it's not expired.
86
+ if now >= self.token_expiry_time:
87
+ self._cleanup()
88
+
89
+ self._issue_token_request()
90
+ return self.token
91
+
92
+ def _issue_token_request(self, raise_on_error=False):
93
+ try:
94
+ headers = {"Content-Type": "application/x-www-form-urlencoded"}
95
+ request_body = {
96
+ "grant_type": "client_credentials",
97
+ "client_id": self.client_id,
98
+ "client_secret": self.client_secret,
99
+ }
100
+ response = self._session.request(
101
+ "POST",
102
+ self.token_endpoint,
103
+ timeout=self.timeout,
104
+ headers=headers,
105
+ data=request_body,
106
+ )
107
+ except requests.RequestException as exc:
108
+ error = f"Retrieving token failed: {mlrun.errors.err_to_str(exc)}"
109
+ if raise_on_error:
110
+ raise mlrun.errors.MLRunRuntimeError(error) from exc
111
+ else:
112
+ logger.warning(error)
113
+ return
114
+
115
+ if not response.ok:
116
+ error = "No error available"
117
+ if response.content:
118
+ try:
119
+ data = response.json()
120
+ error = data.get("error")
121
+ except Exception:
122
+ pass
123
+ logger.warning(
124
+ "Retrieving token failed", status=response.status_code, error=error
125
+ )
126
+ if raise_on_error:
127
+ mlrun.errors.raise_for_status(response)
128
+ return
129
+
130
+ self._parse_response(response.json())
131
+
132
+ def _parse_response(self, data: dict):
133
+ # Response is described in https://datatracker.ietf.org/doc/html/rfc6749#section-4.4.3
134
+ # According to spec, there isn't a refresh token - just the access token and its expiry time (in seconds).
135
+ self.token = data.get("access_token")
136
+ expires_in = data.get("expires_in")
137
+ if not self.token or not expires_in:
138
+ token_str = "****" if self.token else "missing"
139
+ logger.warning(
140
+ "Failed to parse token response", token=token_str, expires_in=expires_in
141
+ )
142
+ return
143
+
144
+ now = datetime.now()
145
+ self.token_expiry_time = now + timedelta(seconds=expires_in)
146
+ self.token_refresh_time = now + timedelta(seconds=expires_in / 2)
147
+ logger.info(
148
+ "Successfully retrieved client-id token",
149
+ expires_in=expires_in,
150
+ expiry=str(self.token_expiry_time),
151
+ refresh=str(self.token_refresh_time),
152
+ )