mlrun 1.8.0rc30__py3-none-any.whl → 1.8.0rc32__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 -35
- mlrun/api/schemas/__init__.py +1 -6
- mlrun/common/runtimes/constants.py +4 -0
- mlrun/common/schemas/__init__.py +0 -2
- mlrun/common/schemas/model_monitoring/__init__.py +0 -2
- mlrun/common/schemas/model_monitoring/constants.py +1 -6
- mlrun/common/schemas/model_monitoring/grafana.py +17 -11
- mlrun/config.py +9 -36
- mlrun/datastore/storeytargets.py +20 -3
- mlrun/db/base.py +1 -1
- mlrun/db/httpdb.py +5 -4
- mlrun/db/nopdb.py +1 -1
- mlrun/model_monitoring/applications/base.py +111 -40
- mlrun/model_monitoring/applications/results.py +2 -2
- mlrun/model_monitoring/controller.py +4 -3
- mlrun/model_monitoring/db/tsdb/__init__.py +9 -5
- mlrun/model_monitoring/db/tsdb/base.py +60 -39
- mlrun/model_monitoring/db/tsdb/tdengine/tdengine_connector.py +117 -52
- mlrun/model_monitoring/db/tsdb/v3io/v3io_connector.py +140 -14
- mlrun/model_monitoring/helpers.py +16 -15
- mlrun/model_monitoring/stream_processing.py +6 -13
- mlrun/projects/pipelines.py +11 -3
- mlrun/projects/project.py +88 -111
- mlrun/serving/states.py +1 -1
- mlrun/serving/v2_serving.py +20 -10
- mlrun/utils/helpers.py +1 -1
- mlrun/utils/logger.py +13 -10
- mlrun/utils/notifications/notification_pusher.py +24 -0
- mlrun/utils/regex.py +1 -0
- mlrun/utils/version/version.json +2 -2
- {mlrun-1.8.0rc30.dist-info → mlrun-1.8.0rc32.dist-info}/METADATA +2 -2
- {mlrun-1.8.0rc30.dist-info → mlrun-1.8.0rc32.dist-info}/RECORD +36 -36
- {mlrun-1.8.0rc30.dist-info → mlrun-1.8.0rc32.dist-info}/LICENSE +0 -0
- {mlrun-1.8.0rc30.dist-info → mlrun-1.8.0rc32.dist-info}/WHEEL +0 -0
- {mlrun-1.8.0rc30.dist-info → mlrun-1.8.0rc32.dist-info}/entry_points.txt +0 -0
- {mlrun-1.8.0rc30.dist-info → mlrun-1.8.0rc32.dist-info}/top_level.txt +0 -0
mlrun/__init__.py
CHANGED
|
@@ -26,7 +26,6 @@ __all__ = [
|
|
|
26
26
|
"VolumeMount",
|
|
27
27
|
]
|
|
28
28
|
|
|
29
|
-
import collections
|
|
30
29
|
from os import environ, path
|
|
31
30
|
from typing import Optional
|
|
32
31
|
|
|
@@ -215,40 +214,8 @@ def set_env_from_file(env_file: str, return_dict: bool = False) -> Optional[dict
|
|
|
215
214
|
if None in env_vars.values():
|
|
216
215
|
raise MLRunInvalidArgumentError("env file lines must be in the form key=value")
|
|
217
216
|
|
|
218
|
-
|
|
219
|
-
for key, value in ordered_env_vars.items():
|
|
217
|
+
for key, value in env_vars.items():
|
|
220
218
|
environ[key] = value
|
|
221
219
|
|
|
222
220
|
mlconf.reload() # reload mlrun configuration
|
|
223
|
-
return
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
def order_env_vars(env_vars: dict[str, str]) -> dict[str, str]:
|
|
227
|
-
"""
|
|
228
|
-
Order and process environment variables by first handling specific ordered keys,
|
|
229
|
-
then processing the remaining keys in the given dictionary.
|
|
230
|
-
|
|
231
|
-
The function ensures that environment variables defined in the `ordered_keys` list
|
|
232
|
-
are added to the result dictionary first. Any other environment variables from
|
|
233
|
-
`env_vars` are then added in the order they appear in the input dictionary.
|
|
234
|
-
|
|
235
|
-
:param env_vars: A dictionary where each key is the name of an environment variable (str),
|
|
236
|
-
and each value is the corresponding environment variable value (str).
|
|
237
|
-
:return: A dictionary with the processed environment variables, ordered with the specific
|
|
238
|
-
keys first, followed by the rest in their original order.
|
|
239
|
-
"""
|
|
240
|
-
ordered_keys = mlconf.get_ordered_keys()
|
|
241
|
-
|
|
242
|
-
ordered_env_vars = collections.OrderedDict()
|
|
243
|
-
|
|
244
|
-
# First, add the ordered keys to the dictionary
|
|
245
|
-
for key in ordered_keys:
|
|
246
|
-
if key in env_vars:
|
|
247
|
-
ordered_env_vars[key] = env_vars[key]
|
|
248
|
-
|
|
249
|
-
# Then, add the remaining keys (those not in ordered_keys)
|
|
250
|
-
for key, value in env_vars.items():
|
|
251
|
-
if key not in ordered_keys:
|
|
252
|
-
ordered_env_vars[key] = value
|
|
253
|
-
|
|
254
|
-
return ordered_env_vars
|
|
221
|
+
return env_vars if return_dict else None
|
mlrun/api/schemas/__init__.py
CHANGED
|
@@ -193,9 +193,7 @@ FeatureValues = DeprecationHelper(mlrun.common.schemas.FeatureValues)
|
|
|
193
193
|
GrafanaColumn = DeprecationHelper(
|
|
194
194
|
mlrun.common.schemas.model_monitoring.grafana.GrafanaColumn
|
|
195
195
|
)
|
|
196
|
-
|
|
197
|
-
mlrun.common.schemas.model_monitoring.grafana.GrafanaDataPoint
|
|
198
|
-
)
|
|
196
|
+
|
|
199
197
|
GrafanaNumberColumn = DeprecationHelper(
|
|
200
198
|
mlrun.common.schemas.model_monitoring.grafana.GrafanaNumberColumn
|
|
201
199
|
)
|
|
@@ -205,9 +203,6 @@ GrafanaStringColumn = DeprecationHelper(
|
|
|
205
203
|
GrafanaTable = DeprecationHelper(
|
|
206
204
|
mlrun.common.schemas.model_monitoring.grafana.GrafanaTable
|
|
207
205
|
)
|
|
208
|
-
GrafanaTimeSeriesTarget = DeprecationHelper(
|
|
209
|
-
mlrun.common.schemas.model_monitoring.grafana.GrafanaTimeSeriesTarget
|
|
210
|
-
)
|
|
211
206
|
ModelEndpoint = DeprecationHelper(mlrun.common.schemas.ModelEndpoint)
|
|
212
207
|
ModelEndpointList = DeprecationHelper(mlrun.common.schemas.ModelEndpointList)
|
|
213
208
|
ModelEndpointMetadata = DeprecationHelper(mlrun.common.schemas.ModelEndpointMetadata)
|
|
@@ -194,6 +194,10 @@ class RunStates:
|
|
|
194
194
|
# TODO: add aborting state once we have it
|
|
195
195
|
]
|
|
196
196
|
|
|
197
|
+
@staticmethod
|
|
198
|
+
def notification_states():
|
|
199
|
+
return RunStates.terminal_states() + [RunStates.running]
|
|
200
|
+
|
|
197
201
|
@staticmethod
|
|
198
202
|
def run_state_to_pipeline_run_status(run_state: str):
|
|
199
203
|
if not run_state:
|
mlrun/common/schemas/__init__.py
CHANGED
|
@@ -140,11 +140,9 @@ from .model_monitoring import (
|
|
|
140
140
|
FeatureSetFeatures,
|
|
141
141
|
FeatureValues,
|
|
142
142
|
GrafanaColumn,
|
|
143
|
-
GrafanaDataPoint,
|
|
144
143
|
GrafanaNumberColumn,
|
|
145
144
|
GrafanaStringColumn,
|
|
146
145
|
GrafanaTable,
|
|
147
|
-
GrafanaTimeSeriesTarget,
|
|
148
146
|
ModelEndpoint,
|
|
149
147
|
ModelEndpointCreationStrategy,
|
|
150
148
|
ModelEndpointList,
|
|
@@ -51,11 +51,9 @@ from .constants import (
|
|
|
51
51
|
from .grafana import (
|
|
52
52
|
GrafanaColumn,
|
|
53
53
|
GrafanaColumnType,
|
|
54
|
-
GrafanaDataPoint,
|
|
55
54
|
GrafanaNumberColumn,
|
|
56
55
|
GrafanaStringColumn,
|
|
57
56
|
GrafanaTable,
|
|
58
|
-
GrafanaTimeSeriesTarget,
|
|
59
57
|
)
|
|
60
58
|
from .model_endpoints import (
|
|
61
59
|
Features,
|
|
@@ -250,11 +250,6 @@ class TSDBTarget(MonitoringStrEnum):
|
|
|
250
250
|
TDEngine = "tdengine"
|
|
251
251
|
|
|
252
252
|
|
|
253
|
-
class DefaultProfileName(StrEnum):
|
|
254
|
-
STREAM = "mm-infra-stream"
|
|
255
|
-
TSDB = "mm-infra-tsdb"
|
|
256
|
-
|
|
257
|
-
|
|
258
253
|
class ProjectSecretKeys:
|
|
259
254
|
ACCESS_KEY = "MODEL_MONITORING_ACCESS_KEY"
|
|
260
255
|
TSDB_PROFILE_NAME = "TSDB_PROFILE_NAME"
|
|
@@ -473,8 +468,8 @@ FQN_REGEX = re.compile(FQN_PATTERN)
|
|
|
473
468
|
|
|
474
469
|
# refer to `mlrun.utils.regex.project_name`
|
|
475
470
|
PROJECT_PATTERN = r"^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$"
|
|
476
|
-
|
|
477
471
|
MODEL_ENDPOINT_ID_PATTERN = r"^[a-zA-Z0-9_-]+$"
|
|
472
|
+
RESULT_NAME_PATTERN = r"[a-zA-Z_][a-zA-Z0-9_]*"
|
|
478
473
|
|
|
479
474
|
INTERSECT_DICT_KEYS = {
|
|
480
475
|
ModelEndpointMonitoringMetricType.METRIC: "intersect_metrics",
|
|
@@ -46,14 +46,20 @@ class GrafanaTable(BaseModel):
|
|
|
46
46
|
self.rows.append(list(args))
|
|
47
47
|
|
|
48
48
|
|
|
49
|
-
class
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
49
|
+
class GrafanaModelEndpointsTable(GrafanaTable):
|
|
50
|
+
def __init__(self):
|
|
51
|
+
columns = self._init_columns()
|
|
52
|
+
super().__init__(columns=columns)
|
|
53
|
+
|
|
54
|
+
@staticmethod
|
|
55
|
+
def _init_columns():
|
|
56
|
+
return [
|
|
57
|
+
GrafanaColumn(text="endpoint_id", type=GrafanaColumnType.STRING),
|
|
58
|
+
GrafanaColumn(text="endpoint_name", type=GrafanaColumnType.STRING),
|
|
59
|
+
GrafanaColumn(text="endpoint_function", type=GrafanaColumnType.STRING),
|
|
60
|
+
GrafanaColumn(text="endpoint_model", type=GrafanaColumnType.STRING),
|
|
61
|
+
GrafanaColumn(text="endpoint_model_class", type=GrafanaColumnType.STRING),
|
|
62
|
+
GrafanaColumn(text="error_count", type=GrafanaColumnType.NUMBER),
|
|
63
|
+
GrafanaColumn(text="drift_status", type=GrafanaColumnType.NUMBER),
|
|
64
|
+
GrafanaColumn(text="sampling_percentage", type=GrafanaColumnType.NUMBER),
|
|
65
|
+
]
|
mlrun/config.py
CHANGED
|
@@ -1366,35 +1366,6 @@ class Config:
|
|
|
1366
1366
|
ver in mlrun.mlconf.ce.mode for ver in ["lite", "full"]
|
|
1367
1367
|
)
|
|
1368
1368
|
|
|
1369
|
-
def get_s3_storage_options(self) -> dict[str, typing.Any]:
|
|
1370
|
-
"""
|
|
1371
|
-
Generate storage options dictionary as required for handling S3 path in fsspec. The model monitoring stream
|
|
1372
|
-
graph uses this method for generating the storage options for S3 parquet target path.
|
|
1373
|
-
:return: A storage options dictionary in which each key-value pair represents a particular configuration,
|
|
1374
|
-
such as endpoint_url or aws access key.
|
|
1375
|
-
"""
|
|
1376
|
-
key = mlrun.get_secret_or_env("AWS_ACCESS_KEY_ID")
|
|
1377
|
-
secret = mlrun.get_secret_or_env("AWS_SECRET_ACCESS_KEY")
|
|
1378
|
-
|
|
1379
|
-
force_non_anonymous = mlrun.get_secret_or_env("S3_NON_ANONYMOUS")
|
|
1380
|
-
profile = mlrun.get_secret_or_env("AWS_PROFILE")
|
|
1381
|
-
|
|
1382
|
-
storage_options = dict(
|
|
1383
|
-
anon=not (force_non_anonymous or (key and secret)),
|
|
1384
|
-
key=key,
|
|
1385
|
-
secret=secret,
|
|
1386
|
-
)
|
|
1387
|
-
|
|
1388
|
-
endpoint_url = mlrun.get_secret_or_env("S3_ENDPOINT_URL")
|
|
1389
|
-
if endpoint_url:
|
|
1390
|
-
client_kwargs = {"endpoint_url": endpoint_url}
|
|
1391
|
-
storage_options["client_kwargs"] = client_kwargs
|
|
1392
|
-
|
|
1393
|
-
if profile:
|
|
1394
|
-
storage_options["profile"] = profile
|
|
1395
|
-
|
|
1396
|
-
return storage_options
|
|
1397
|
-
|
|
1398
1369
|
def is_explicit_ack_enabled(self) -> bool:
|
|
1399
1370
|
return self.httpdb.nuclio.explicit_ack == "enabled" and (
|
|
1400
1371
|
not self.nuclio_version
|
|
@@ -1402,13 +1373,6 @@ class Config:
|
|
|
1402
1373
|
>= semver.VersionInfo.parse("1.12.10")
|
|
1403
1374
|
)
|
|
1404
1375
|
|
|
1405
|
-
@staticmethod
|
|
1406
|
-
def get_ordered_keys():
|
|
1407
|
-
# Define the keys to process first
|
|
1408
|
-
return [
|
|
1409
|
-
"MLRUN_HTTPDB__HTTP__VERIFY" # Ensure this key is processed first for proper connection setup
|
|
1410
|
-
]
|
|
1411
|
-
|
|
1412
1376
|
|
|
1413
1377
|
# Global configuration
|
|
1414
1378
|
config = Config.from_dict(default_config)
|
|
@@ -1626,6 +1590,15 @@ def read_env(env=None, prefix=env_prefix):
|
|
|
1626
1590
|
# The default function pod resource values are of type str; however, when reading from environment variable numbers,
|
|
1627
1591
|
# it converts them to type int if contains only number, so we want to convert them to str.
|
|
1628
1592
|
_convert_resources_to_str(config)
|
|
1593
|
+
|
|
1594
|
+
# If the environment variable MLRUN_HTTPDB__HTTP__VERIFY is set, we ensure SSL verification settings take precedence
|
|
1595
|
+
# by moving the 'httpdb' configuration to the beginning of the config dictionary.
|
|
1596
|
+
# This ensures that SSL verification is applied before other settings.
|
|
1597
|
+
if "MLRUN_HTTPDB__HTTP__VERIFY" in env:
|
|
1598
|
+
httpdb = config.pop("httpdb", None)
|
|
1599
|
+
if httpdb:
|
|
1600
|
+
config = {"httpdb": httpdb, **config}
|
|
1601
|
+
|
|
1629
1602
|
return config
|
|
1630
1603
|
|
|
1631
1604
|
|
mlrun/datastore/storeytargets.py
CHANGED
|
@@ -42,9 +42,21 @@ def get_url_and_storage_options(path, external_storage_options=None):
|
|
|
42
42
|
|
|
43
43
|
|
|
44
44
|
class TDEngineStoreyTarget(storey.TDEngineTarget):
|
|
45
|
-
def __init__(self, *args, **kwargs):
|
|
46
|
-
|
|
47
|
-
|
|
45
|
+
def __init__(self, *args, url: str, **kwargs):
|
|
46
|
+
if url.startswith("ds://"):
|
|
47
|
+
datastore_profile = (
|
|
48
|
+
mlrun.datastore.datastore_profile.datastore_profile_read(url)
|
|
49
|
+
)
|
|
50
|
+
if not isinstance(
|
|
51
|
+
datastore_profile,
|
|
52
|
+
mlrun.datastore.datastore_profile.TDEngineDatastoreProfile,
|
|
53
|
+
):
|
|
54
|
+
raise ValueError(
|
|
55
|
+
f"Unexpected datastore profile type:{datastore_profile.type}."
|
|
56
|
+
"Only TDEngineDatastoreProfile is supported"
|
|
57
|
+
)
|
|
58
|
+
url = datastore_profile.dsn()
|
|
59
|
+
super().__init__(*args, url=url, **kwargs)
|
|
48
60
|
|
|
49
61
|
|
|
50
62
|
class StoreyTargetUtils:
|
|
@@ -69,7 +81,12 @@ class StoreyTargetUtils:
|
|
|
69
81
|
|
|
70
82
|
class ParquetStoreyTarget(storey.ParquetTarget):
|
|
71
83
|
def __init__(self, *args, **kwargs):
|
|
84
|
+
alt_key_name = kwargs.pop("alternative_v3io_access_key", None)
|
|
72
85
|
args, kwargs = StoreyTargetUtils.process_args_and_kwargs(args, kwargs)
|
|
86
|
+
storage_options = kwargs.get("storage_options", {})
|
|
87
|
+
if storage_options and storage_options.get("v3io_access_key") and alt_key_name:
|
|
88
|
+
if alt_key := mlrun.get_secret_or_env(alt_key_name):
|
|
89
|
+
storage_options["v3io_access_key"] = alt_key
|
|
73
90
|
super().__init__(*args, **kwargs)
|
|
74
91
|
|
|
75
92
|
|
mlrun/db/base.py
CHANGED
|
@@ -720,7 +720,7 @@ class RunDBInterface(ABC):
|
|
|
720
720
|
def list_model_endpoints(
|
|
721
721
|
self,
|
|
722
722
|
project: str,
|
|
723
|
-
|
|
723
|
+
names: Optional[Union[str, list[str]]] = None,
|
|
724
724
|
function_name: Optional[str] = None,
|
|
725
725
|
function_tag: Optional[str] = None,
|
|
726
726
|
model_name: Optional[str] = None,
|
mlrun/db/httpdb.py
CHANGED
|
@@ -3765,7 +3765,7 @@ class HTTPRunDB(RunDBInterface):
|
|
|
3765
3765
|
def list_model_endpoints(
|
|
3766
3766
|
self,
|
|
3767
3767
|
project: str,
|
|
3768
|
-
|
|
3768
|
+
names: Optional[Union[str, list[str]]] = None,
|
|
3769
3769
|
function_name: Optional[str] = None,
|
|
3770
3770
|
function_tag: Optional[str] = None,
|
|
3771
3771
|
model_name: Optional[str] = None,
|
|
@@ -3782,7 +3782,7 @@ class HTTPRunDB(RunDBInterface):
|
|
|
3782
3782
|
List model endpoints with optional filtering by name, function name, model name, labels, and time range.
|
|
3783
3783
|
|
|
3784
3784
|
:param project: The name of the project
|
|
3785
|
-
:param
|
|
3785
|
+
:param names: The name of the model endpoint, or list of names of the model endpoints
|
|
3786
3786
|
:param function_name: The name of the function
|
|
3787
3787
|
:param function_tag: The tag of the function
|
|
3788
3788
|
:param model_name: The name of the model
|
|
@@ -3798,12 +3798,13 @@ class HTTPRunDB(RunDBInterface):
|
|
|
3798
3798
|
"""
|
|
3799
3799
|
path = f"projects/{project}/model-endpoints"
|
|
3800
3800
|
labels = self._parse_labels(labels)
|
|
3801
|
-
|
|
3801
|
+
if names and isinstance(names, str):
|
|
3802
|
+
names = [names]
|
|
3802
3803
|
response = self.api_call(
|
|
3803
3804
|
method=mlrun.common.types.HTTPMethod.GET,
|
|
3804
3805
|
path=path,
|
|
3805
3806
|
params={
|
|
3806
|
-
"name":
|
|
3807
|
+
"name": names,
|
|
3807
3808
|
"model_name": model_name,
|
|
3808
3809
|
"model_tag": model_tag,
|
|
3809
3810
|
"function_name": function_name,
|
mlrun/db/nopdb.py
CHANGED
|
@@ -617,7 +617,7 @@ class NopDB(RunDBInterface):
|
|
|
617
617
|
def list_model_endpoints(
|
|
618
618
|
self,
|
|
619
619
|
project: str,
|
|
620
|
-
|
|
620
|
+
names: Optional[Union[str, list[str]]] = None,
|
|
621
621
|
function_name: Optional[str] = None,
|
|
622
622
|
function_tag: Optional[str] = None,
|
|
623
623
|
model_name: Optional[str] = None,
|
|
@@ -28,6 +28,7 @@ import mlrun.model_monitoring.api as mm_api
|
|
|
28
28
|
import mlrun.model_monitoring.applications.context as mm_context
|
|
29
29
|
import mlrun.model_monitoring.applications.results as mm_results
|
|
30
30
|
from mlrun.serving.utils import MonitoringApplicationToDict
|
|
31
|
+
from mlrun.utils import logger
|
|
31
32
|
|
|
32
33
|
|
|
33
34
|
class ModelMonitoringApplicationBase(MonitoringApplicationToDict, ABC):
|
|
@@ -94,9 +95,9 @@ class ModelMonitoringApplicationBase(MonitoringApplicationToDict, ABC):
|
|
|
94
95
|
context: "mlrun.MLClientCtx",
|
|
95
96
|
sample_data: Optional[pd.DataFrame] = None,
|
|
96
97
|
reference_data: Optional[pd.DataFrame] = None,
|
|
97
|
-
endpoints: Optional[list[tuple[str, str]]] = None,
|
|
98
|
-
start: Optional[
|
|
99
|
-
end: Optional[
|
|
98
|
+
endpoints: Optional[Union[list[tuple[str, str]], list[str], str]] = None,
|
|
99
|
+
start: Optional[str] = None,
|
|
100
|
+
end: Optional[str] = None,
|
|
100
101
|
base_period: Optional[int] = None,
|
|
101
102
|
):
|
|
102
103
|
"""
|
|
@@ -124,7 +125,6 @@ class ModelMonitoringApplicationBase(MonitoringApplicationToDict, ABC):
|
|
|
124
125
|
return self.do_tracking(monitoring_context)
|
|
125
126
|
|
|
126
127
|
if endpoints is not None:
|
|
127
|
-
start, end = self._validate_times(start, end, base_period)
|
|
128
128
|
for window_start, window_end in self._window_generator(
|
|
129
129
|
start, end, base_period
|
|
130
130
|
):
|
|
@@ -137,43 +137,88 @@ class ModelMonitoringApplicationBase(MonitoringApplicationToDict, ABC):
|
|
|
137
137
|
mm_constants.ApplicationEvent.END_INFER_TIME: window_end,
|
|
138
138
|
}
|
|
139
139
|
)
|
|
140
|
-
|
|
141
|
-
f"{endpoint_name}_{window_start.isoformat()}_{window_end.isoformat()}"
|
|
142
|
-
|
|
140
|
+
result_key = (
|
|
141
|
+
f"{endpoint_name}-{endpoint_id}_{window_start.isoformat()}_{window_end.isoformat()}"
|
|
142
|
+
if window_start and window_end
|
|
143
|
+
else f"{endpoint_name}-{endpoint_id}"
|
|
143
144
|
)
|
|
145
|
+
context.log_result(result_key, result)
|
|
144
146
|
else:
|
|
145
147
|
return call_do_tracking()
|
|
146
148
|
|
|
147
149
|
@staticmethod
|
|
148
|
-
def
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
150
|
+
def _handle_endpoints_type_evaluate(
|
|
151
|
+
project: str,
|
|
152
|
+
endpoints: Union[list[tuple[str, str]], list[str], str, None],
|
|
153
|
+
) -> list[tuple[str, str]]:
|
|
154
|
+
if endpoints:
|
|
155
|
+
if isinstance(endpoints, str) or (
|
|
156
|
+
isinstance(endpoints, list) and isinstance(endpoints[0], str)
|
|
157
|
+
):
|
|
158
|
+
endpoints_list = (
|
|
159
|
+
mlrun.get_run_db()
|
|
160
|
+
.list_model_endpoints(
|
|
161
|
+
project,
|
|
162
|
+
names=endpoints,
|
|
163
|
+
latest_only=True,
|
|
164
|
+
)
|
|
165
|
+
.endpoints
|
|
166
|
+
)
|
|
167
|
+
if endpoints_list:
|
|
168
|
+
list_endpoints_result = [
|
|
169
|
+
(endpoint.metadata.name, endpoint.metadata.uid)
|
|
170
|
+
for endpoint in endpoints_list
|
|
171
|
+
]
|
|
172
|
+
retrieve_ep_names = list(
|
|
173
|
+
map(lambda endpoint: endpoint[0], list_endpoints_result)
|
|
174
|
+
)
|
|
175
|
+
missing = set(
|
|
176
|
+
[endpoints] if isinstance(endpoints, str) else endpoints
|
|
177
|
+
) - set(retrieve_ep_names)
|
|
178
|
+
if missing:
|
|
179
|
+
logger.warning(
|
|
180
|
+
"Could not list all the required endpoints.",
|
|
181
|
+
missing_endpoint=missing,
|
|
182
|
+
endpoints=list_endpoints_result,
|
|
183
|
+
)
|
|
184
|
+
else:
|
|
185
|
+
raise mlrun.errors.MLRunNotFoundError(
|
|
186
|
+
f"Did not find any model_endpoint named ' {endpoints}'"
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
if not (
|
|
190
|
+
isinstance(endpoints, list) and isinstance(endpoints[0], (list, tuple))
|
|
191
|
+
):
|
|
192
|
+
raise mlrun.errors.MLRunInvalidArgumentError(
|
|
193
|
+
"Could not resolve endpoints as list of [(name, uid)]"
|
|
194
|
+
)
|
|
195
|
+
return endpoints
|
|
164
196
|
|
|
165
197
|
@staticmethod
|
|
166
198
|
def _window_generator(
|
|
167
|
-
start:
|
|
168
|
-
) -> Iterator[tuple[datetime, datetime]]:
|
|
199
|
+
start: Optional[str], end: Optional[str], base_period: Optional[int]
|
|
200
|
+
) -> Iterator[tuple[Optional[datetime], Optional[datetime]]]:
|
|
201
|
+
if start is None or end is None:
|
|
202
|
+
# A single window based on the `sample_data` input - see `_handler`.
|
|
203
|
+
yield None, None
|
|
204
|
+
return
|
|
205
|
+
|
|
206
|
+
start_dt = datetime.fromisoformat(start)
|
|
207
|
+
end_dt = datetime.fromisoformat(end)
|
|
208
|
+
|
|
169
209
|
if base_period is None:
|
|
170
|
-
yield
|
|
210
|
+
yield start_dt, end_dt
|
|
171
211
|
return
|
|
172
212
|
|
|
213
|
+
if not isinstance(base_period, int) or base_period <= 0:
|
|
214
|
+
raise mlrun.errors.MLRunValueError(
|
|
215
|
+
"`base_period` must be a nonnegative integer - the number of minutes in a monitoring window"
|
|
216
|
+
)
|
|
217
|
+
|
|
173
218
|
window_length = timedelta(minutes=base_period)
|
|
174
|
-
current_start_time =
|
|
175
|
-
while current_start_time <
|
|
176
|
-
current_end_time = min(current_start_time + window_length,
|
|
219
|
+
current_start_time = start_dt
|
|
220
|
+
while current_start_time < end_dt:
|
|
221
|
+
current_end_time = min(current_start_time + window_length, end_dt)
|
|
177
222
|
yield current_start_time, current_end_time
|
|
178
223
|
current_start_time = current_end_time
|
|
179
224
|
|
|
@@ -341,7 +386,7 @@ class ModelMonitoringApplicationBase(MonitoringApplicationToDict, ABC):
|
|
|
341
386
|
class_handler: Optional[str] = None,
|
|
342
387
|
requirements: Optional[Union[str, list[str]]] = None,
|
|
343
388
|
requirements_file: str = "",
|
|
344
|
-
endpoints: Optional[list[tuple[str, str]]] = None,
|
|
389
|
+
endpoints: Optional[Union[list[tuple[str, str]], list[str], str]] = None,
|
|
345
390
|
start: Optional[datetime] = None,
|
|
346
391
|
end: Optional[datetime] = None,
|
|
347
392
|
base_period: Optional[int] = None,
|
|
@@ -369,13 +414,28 @@ class ModelMonitoringApplicationBase(MonitoringApplicationToDict, ABC):
|
|
|
369
414
|
:param requirements: List of Python requirements to be installed in the image.
|
|
370
415
|
:param requirements_file: Path to a Python requirements file to be installed in the image.
|
|
371
416
|
:param endpoints: A list of tuples of the model endpoint (name, uid) to get the data from.
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
417
|
+
allow providing a list of model_endpoint names or name for a single model_endpoint.
|
|
418
|
+
Note: provide names retrieves the model all the active model endpoints using those
|
|
419
|
+
names (cross function model endpoints)
|
|
420
|
+
If provided, and ``sample_data`` is not, you have to provide also the ``start`` and
|
|
421
|
+
``end`` times of the data to analyze from the model endpoints.
|
|
422
|
+
:param start: The start time of the endpoint's data, not included.
|
|
423
|
+
If you want the model endpoint's data at ``start`` included, you need to subtract a
|
|
424
|
+
small ``datetime.timedelta`` from it.
|
|
425
|
+
:param end: The end time of the endpoint's data, included.
|
|
426
|
+
Please note: when ``start`` and ``end`` are set, they create a left-open time interval
|
|
427
|
+
("window") :math:`(\\text{start}, \\text{end}]` that excludes the endpoint's data at
|
|
428
|
+
``start`` and includes the data at ``end``:
|
|
429
|
+
:math:`\\text{start} < t \\leq \\text{end}`, :math:`t` is the time taken in the
|
|
430
|
+
window's data.
|
|
375
431
|
:param base_period: The window length in minutes. If ``None``, the whole window from ``start`` to ``end``
|
|
376
432
|
is taken. If an integer is specified, the application is run from ``start`` to ``end``
|
|
377
433
|
in ``base_period`` length windows, except for the last window that ends at ``end`` and
|
|
378
|
-
therefore may be shorter
|
|
434
|
+
therefore may be shorter:
|
|
435
|
+
:math:`(\\text{start}, \\text{start} + \\text{base_period}],
|
|
436
|
+
(\\text{start} + \\text{base_period}, \\text{start} + 2\\cdot\\text{base_period}],
|
|
437
|
+
..., (\\text{start} + m\\cdot\\text{base_period}, \\text{end}]`,
|
|
438
|
+
where :math:`m` is some positive integer.
|
|
379
439
|
|
|
380
440
|
:returns: The output of the
|
|
381
441
|
:py:meth:`~mlrun.model_monitoring.applications.ModelMonitoringApplicationBase.do_tracking`
|
|
@@ -395,16 +455,27 @@ class ModelMonitoringApplicationBase(MonitoringApplicationToDict, ABC):
|
|
|
395
455
|
project=project,
|
|
396
456
|
)
|
|
397
457
|
|
|
398
|
-
params: dict[str, Union[list[tuple[str, str]],
|
|
458
|
+
params: dict[str, Union[list[tuple[str, str]], str, int, None]] = {}
|
|
399
459
|
if endpoints:
|
|
400
|
-
|
|
460
|
+
endpoints = cls._handle_endpoints_type_evaluate(
|
|
461
|
+
project=project.name,
|
|
462
|
+
endpoints=endpoints,
|
|
463
|
+
)
|
|
401
464
|
params["endpoints"] = endpoints
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
465
|
+
if sample_data is None:
|
|
466
|
+
if start is None or end is None:
|
|
467
|
+
raise mlrun.errors.MLRunValueError(
|
|
468
|
+
"`start` and `end` times must be provided when `endpoints` "
|
|
469
|
+
"is provided without `sample_data`"
|
|
470
|
+
)
|
|
471
|
+
params["start"] = (
|
|
472
|
+
start.isoformat() if isinstance(start, datetime) else start
|
|
473
|
+
)
|
|
474
|
+
params["end"] = end.isoformat() if isinstance(end, datetime) else end
|
|
475
|
+
params["base_period"] = base_period
|
|
405
476
|
elif start or end or base_period:
|
|
406
477
|
raise mlrun.errors.MLRunValueError(
|
|
407
|
-
"Custom start and end times or base_period are supported only with endpoints data"
|
|
478
|
+
"Custom `start` and `end` times or base_period are supported only with endpoints data"
|
|
408
479
|
)
|
|
409
480
|
|
|
410
481
|
inputs: dict[str, str] = {}
|
|
@@ -33,10 +33,10 @@ class _ModelMonitoringApplicationDataRes(ABC):
|
|
|
33
33
|
name: str
|
|
34
34
|
|
|
35
35
|
def __post_init__(self):
|
|
36
|
-
pat = re.compile(
|
|
36
|
+
pat = re.compile(mm_constants.RESULT_NAME_PATTERN)
|
|
37
37
|
if not re.fullmatch(pat, self.name):
|
|
38
38
|
raise mlrun.errors.MLRunValueError(
|
|
39
|
-
"Attribute name must comply with the regex `
|
|
39
|
+
f"Attribute name must comply with the regex `{mm_constants.RESULT_NAME_PATTERN}`"
|
|
40
40
|
)
|
|
41
41
|
|
|
42
42
|
@abstractmethod
|
|
@@ -250,9 +250,10 @@ class MonitoringApplicationController:
|
|
|
250
250
|
|
|
251
251
|
self.model_monitoring_access_key = self._get_model_monitoring_access_key()
|
|
252
252
|
self.v3io_access_key = mlrun.mlconf.get_v3io_access_key()
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
253
|
+
store, _, _ = mlrun.store_manager.get_or_create_store(
|
|
254
|
+
mlrun.mlconf.artifact_path
|
|
255
|
+
)
|
|
256
|
+
self.storage_options = store.get_storage_options()
|
|
256
257
|
|
|
257
258
|
@staticmethod
|
|
258
259
|
def _get_model_monitoring_access_key() -> Optional[str]:
|
|
@@ -19,6 +19,7 @@ import mlrun.common.schemas.secret
|
|
|
19
19
|
import mlrun.datastore.datastore_profile
|
|
20
20
|
import mlrun.errors
|
|
21
21
|
import mlrun.model_monitoring.helpers
|
|
22
|
+
from mlrun.datastore.datastore_profile import DatastoreProfile
|
|
22
23
|
|
|
23
24
|
from .base import TSDBConnector
|
|
24
25
|
|
|
@@ -29,10 +30,13 @@ class ObjectTSDBFactory(enum.Enum):
|
|
|
29
30
|
v3io_tsdb = "v3io-tsdb"
|
|
30
31
|
tdengine = "tdengine"
|
|
31
32
|
|
|
32
|
-
def to_tsdb_connector(
|
|
33
|
+
def to_tsdb_connector(
|
|
34
|
+
self, project: str, profile: DatastoreProfile, **kwargs
|
|
35
|
+
) -> TSDBConnector:
|
|
33
36
|
"""
|
|
34
37
|
Return a TSDBConnector object based on the provided enum value.
|
|
35
38
|
:param project: The name of the project.
|
|
39
|
+
:param profile: Datastore profile containing DSN and credentials for TSDB connection
|
|
36
40
|
:return: `TSDBConnector` object.
|
|
37
41
|
"""
|
|
38
42
|
|
|
@@ -51,7 +55,7 @@ class ObjectTSDBFactory(enum.Enum):
|
|
|
51
55
|
|
|
52
56
|
from .tdengine.tdengine_connector import TDEngineConnector
|
|
53
57
|
|
|
54
|
-
return TDEngineConnector(project=project, **kwargs)
|
|
58
|
+
return TDEngineConnector(project=project, profile=profile, **kwargs)
|
|
55
59
|
|
|
56
60
|
@classmethod
|
|
57
61
|
def _missing_(cls, value: typing.Any):
|
|
@@ -87,12 +91,10 @@ def get_tsdb_connector(
|
|
|
87
91
|
kwargs = {}
|
|
88
92
|
if isinstance(profile, mlrun.datastore.datastore_profile.DatastoreProfileV3io):
|
|
89
93
|
tsdb_connector_type = mlrun.common.schemas.model_monitoring.TSDBTarget.V3IO_TSDB
|
|
90
|
-
kwargs["v3io_access_key"] = profile.v3io_access_key
|
|
91
94
|
elif isinstance(
|
|
92
95
|
profile, mlrun.datastore.datastore_profile.TDEngineDatastoreProfile
|
|
93
96
|
):
|
|
94
97
|
tsdb_connector_type = mlrun.common.schemas.model_monitoring.TSDBTarget.TDEngine
|
|
95
|
-
kwargs["connection_string"] = profile.dsn()
|
|
96
98
|
else:
|
|
97
99
|
extra_message = (
|
|
98
100
|
""
|
|
@@ -109,4 +111,6 @@ def get_tsdb_connector(
|
|
|
109
111
|
tsdb_connector_factory = ObjectTSDBFactory(tsdb_connector_type)
|
|
110
112
|
|
|
111
113
|
# Convert into TSDB connector object
|
|
112
|
-
return tsdb_connector_factory.to_tsdb_connector(
|
|
114
|
+
return tsdb_connector_factory.to_tsdb_connector(
|
|
115
|
+
project=project, profile=profile, **kwargs
|
|
116
|
+
)
|