mlrun 1.8.0rc29__py3-none-any.whl → 1.8.0rc31__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 (50) hide show
  1. mlrun/__init__.py +2 -34
  2. mlrun/api/schemas/__init__.py +1 -6
  3. mlrun/artifacts/document.py +3 -3
  4. mlrun/artifacts/manager.py +1 -0
  5. mlrun/artifacts/model.py +3 -3
  6. mlrun/common/model_monitoring/helpers.py +16 -7
  7. mlrun/common/runtimes/constants.py +5 -0
  8. mlrun/common/schemas/__init__.py +0 -2
  9. mlrun/common/schemas/model_monitoring/__init__.py +0 -2
  10. mlrun/common/schemas/model_monitoring/constants.py +4 -7
  11. mlrun/common/schemas/model_monitoring/grafana.py +17 -11
  12. mlrun/config.py +9 -36
  13. mlrun/datastore/datastore_profile.py +1 -1
  14. mlrun/datastore/sources.py +14 -13
  15. mlrun/datastore/storeytargets.py +20 -3
  16. mlrun/db/httpdb.py +4 -30
  17. mlrun/k8s_utils.py +2 -5
  18. mlrun/launcher/base.py +16 -0
  19. mlrun/model_monitoring/api.py +1 -2
  20. mlrun/model_monitoring/applications/_application_steps.py +23 -37
  21. mlrun/model_monitoring/applications/base.py +55 -40
  22. mlrun/model_monitoring/applications/context.py +0 -3
  23. mlrun/model_monitoring/applications/results.py +16 -16
  24. mlrun/model_monitoring/controller.py +35 -31
  25. mlrun/model_monitoring/db/tsdb/__init__.py +9 -5
  26. mlrun/model_monitoring/db/tsdb/base.py +60 -39
  27. mlrun/model_monitoring/db/tsdb/tdengine/tdengine_connector.py +122 -53
  28. mlrun/model_monitoring/db/tsdb/v3io/v3io_connector.py +140 -14
  29. mlrun/model_monitoring/helpers.py +124 -16
  30. mlrun/model_monitoring/stream_processing.py +6 -21
  31. mlrun/projects/pipelines.py +11 -3
  32. mlrun/projects/project.py +104 -115
  33. mlrun/run.py +2 -2
  34. mlrun/runtimes/nuclio/function.py +4 -2
  35. mlrun/serving/routers.py +3 -4
  36. mlrun/serving/server.py +10 -8
  37. mlrun/serving/states.py +12 -2
  38. mlrun/serving/v2_serving.py +25 -20
  39. mlrun/utils/async_http.py +32 -19
  40. mlrun/utils/helpers.py +5 -2
  41. mlrun/utils/logger.py +14 -10
  42. mlrun/utils/notifications/notification_pusher.py +25 -0
  43. mlrun/utils/regex.py +1 -0
  44. mlrun/utils/version/version.json +2 -2
  45. {mlrun-1.8.0rc29.dist-info → mlrun-1.8.0rc31.dist-info}/METADATA +4 -4
  46. {mlrun-1.8.0rc29.dist-info → mlrun-1.8.0rc31.dist-info}/RECORD +50 -50
  47. {mlrun-1.8.0rc29.dist-info → mlrun-1.8.0rc31.dist-info}/LICENSE +0 -0
  48. {mlrun-1.8.0rc29.dist-info → mlrun-1.8.0rc31.dist-info}/WHEEL +0 -0
  49. {mlrun-1.8.0rc29.dist-info → mlrun-1.8.0rc31.dist-info}/entry_points.txt +0 -0
  50. {mlrun-1.8.0rc29.dist-info → mlrun-1.8.0rc31.dist-info}/top_level.txt +0 -0
mlrun/__init__.py CHANGED
@@ -214,40 +214,8 @@ def set_env_from_file(env_file: str, return_dict: bool = False) -> Optional[dict
214
214
  if None in env_vars.values():
215
215
  raise MLRunInvalidArgumentError("env file lines must be in the form key=value")
216
216
 
217
- ordered_env_vars = order_env_vars(env_vars)
218
- for key, value in ordered_env_vars.items():
217
+ for key, value in env_vars.items():
219
218
  environ[key] = value
220
219
 
221
220
  mlconf.reload() # reload mlrun configuration
222
- return ordered_env_vars if return_dict else None
223
-
224
-
225
- def order_env_vars(env_vars: dict[str, str]) -> dict[str, str]:
226
- """
227
- Order and process environment variables by first handling specific ordered keys,
228
- then processing the remaining keys in the given dictionary.
229
-
230
- The function ensures that environment variables defined in the `ordered_keys` list
231
- are added to the result dictionary first. Any other environment variables from
232
- `env_vars` are then added in the order they appear in the input dictionary.
233
-
234
- :param env_vars: A dictionary where each key is the name of an environment variable (str),
235
- and each value is the corresponding environment variable value (str).
236
- :return: A dictionary with the processed environment variables, ordered with the specific
237
- keys first, followed by the rest in their original order.
238
- """
239
- ordered_keys = mlconf.get_ordered_keys()
240
-
241
- ordered_env_vars: dict[str, str] = {}
242
-
243
- # First, add the ordered keys to the dictionary
244
- for key in ordered_keys:
245
- if key in env_vars:
246
- ordered_env_vars[key] = env_vars[key]
247
-
248
- # Then, add the remaining keys (those not in ordered_keys)
249
- for key, value in env_vars.items():
250
- if key not in ordered_keys:
251
- ordered_env_vars[key] = value
252
-
253
- return ordered_env_vars
221
+ return env_vars if return_dict else None
@@ -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
- GrafanaDataPoint = DeprecationHelper(
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)
@@ -97,9 +97,9 @@ class MLRunLoader:
97
97
  A factory class for creating instances of a dynamically defined document loader.
98
98
 
99
99
  Args:
100
- artifact_key (str, optional): The key for the artifact to be logged. Special characters and symbols
101
- not valid in artifact names will be encoded as their hexadecimal representation. The '%%' pattern
102
- in the key will be replaced by the hex-encoded version of the source path. Defaults to "%%".
100
+ artifact_key (str, optional): The key for the artifact to be logged.
101
+ The '%%' pattern in the key will be replaced by the source path
102
+ with any unsupported characters converted to '_'. Defaults to "%%".
103
103
  local_path (str): The source path of the document to be loaded.
104
104
  loader_spec (DocumentLoaderSpec): Specification for the document loader.
105
105
  producer (Optional[Union[MlrunProject, str, MLClientCtx]], optional): The producer of the document.
@@ -403,6 +403,7 @@ class ArtifactManager:
403
403
  project=item.project,
404
404
  tag=item.tag,
405
405
  tree=item.tree,
406
+ iter=item.iter,
406
407
  deletion_strategy=deletion_strategy,
407
408
  secrets=secrets,
408
409
  )
mlrun/artifacts/model.py CHANGED
@@ -429,6 +429,9 @@ def get_model(model_dir, suffix=""):
429
429
  extra_dataitems = {}
430
430
  default_suffix = ".pkl"
431
431
 
432
+ if hasattr(model_dir, "artifact_url"):
433
+ model_dir = model_dir.artifact_url
434
+
432
435
  alternative_suffix = next(
433
436
  (
434
437
  optional_suffix
@@ -438,9 +441,6 @@ def get_model(model_dir, suffix=""):
438
441
  None,
439
442
  )
440
443
 
441
- if hasattr(model_dir, "artifact_url"):
442
- model_dir = model_dir.artifact_url
443
-
444
444
  if mlrun.datastore.is_store_uri(model_dir):
445
445
  model_spec, target = mlrun.datastore.store_manager.get_store_artifact(model_dir)
446
446
  if not model_spec or model_spec.kind != "model":
@@ -36,6 +36,20 @@ def parse_model_endpoint_store_prefix(store_prefix: str):
36
36
  return endpoint, container, path
37
37
 
38
38
 
39
+ def get_kafka_topic(project: str, function_name: typing.Optional[str] = None) -> str:
40
+ if (
41
+ function_name is None
42
+ or function_name == mm_constants.MonitoringFunctionNames.STREAM
43
+ ):
44
+ function_specifier = ""
45
+ else:
46
+ function_specifier = f"_{function_name}"
47
+
48
+ return (
49
+ f"monitoring_stream_{mlrun.mlconf.system_id}_{project}{function_specifier}_v1"
50
+ )
51
+
52
+
39
53
  def parse_monitoring_stream_path(
40
54
  stream_uri: str, project: str, function_name: typing.Optional[str] = None
41
55
  ) -> str:
@@ -43,13 +57,8 @@ def parse_monitoring_stream_path(
43
57
  if "?topic" in stream_uri:
44
58
  raise mlrun.errors.MLRunValueError("Custom kafka topic is not allowed")
45
59
  # Add topic to stream kafka uri
46
- if (
47
- function_name is None
48
- or function_name == mm_constants.MonitoringFunctionNames.STREAM
49
- ):
50
- stream_uri += f"?topic=monitoring_stream_{project}_v1"
51
- else:
52
- stream_uri += f"?topic=monitoring_stream_{project}_{function_name}_v1"
60
+ topic = get_kafka_topic(project=project, function_name=function_name)
61
+ stream_uri += f"?topic={topic}"
53
62
 
54
63
  return stream_uri
55
64
 
@@ -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:
@@ -229,6 +233,7 @@ class RunStates:
229
233
  mlrun_pipelines.common.models.RunStatuses.runtime_state_unspecified: RunStates.unknown,
230
234
  mlrun_pipelines.common.models.RunStatuses.error: RunStates.error,
231
235
  mlrun_pipelines.common.models.RunStatuses.paused: RunStates.unknown,
236
+ mlrun_pipelines.common.models.RunStatuses.unknown: RunStates.unknown,
232
237
  }[pipeline_run_status]
233
238
 
234
239
 
@@ -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,
@@ -163,7 +163,6 @@ class ApplicationEvent:
163
163
  END_INFER_TIME = "end_infer_time"
164
164
  ENDPOINT_ID = "endpoint_id"
165
165
  ENDPOINT_NAME = "endpoint_name"
166
- OUTPUT_STREAM_URI = "output_stream_uri"
167
166
 
168
167
 
169
168
  class WriterEvent(MonitoringStrEnum):
@@ -251,11 +250,6 @@ class TSDBTarget(MonitoringStrEnum):
251
250
  TDEngine = "tdengine"
252
251
 
253
252
 
254
- class DefaultProfileName(StrEnum):
255
- STREAM = "mm-infra-stream"
256
- TSDB = "mm-infra-tsdb"
257
-
258
-
259
253
  class ProjectSecretKeys:
260
254
  ACCESS_KEY = "MODEL_MONITORING_ACCESS_KEY"
261
255
  TSDB_PROFILE_NAME = "TSDB_PROFILE_NAME"
@@ -474,10 +468,13 @@ FQN_REGEX = re.compile(FQN_PATTERN)
474
468
 
475
469
  # refer to `mlrun.utils.regex.project_name`
476
470
  PROJECT_PATTERN = r"^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$"
477
-
478
471
  MODEL_ENDPOINT_ID_PATTERN = r"^[a-zA-Z0-9_-]+$"
472
+ RESULT_NAME_PATTERN = r"[a-zA-Z_][a-zA-Z0-9_]*"
479
473
 
480
474
  INTERSECT_DICT_KEYS = {
481
475
  ModelEndpointMonitoringMetricType.METRIC: "intersect_metrics",
482
476
  ModelEndpointMonitoringMetricType.RESULT: "intersect_results",
483
477
  }
478
+
479
+ CRON_TRIGGER_KINDS = ("http", "cron")
480
+ STREAM_TRIGGER_KINDS = ("v3io-stream", "kafka-cluster")
@@ -46,14 +46,20 @@ class GrafanaTable(BaseModel):
46
46
  self.rows.append(list(args))
47
47
 
48
48
 
49
- class GrafanaDataPoint(BaseModel):
50
- value: float
51
- timestamp: int # Unix timestamp in milliseconds
52
-
53
-
54
- class GrafanaTimeSeriesTarget(BaseModel):
55
- target: str
56
- datapoints: list[tuple[float, int]] = []
57
-
58
- def add_data_point(self, data_point: GrafanaDataPoint):
59
- self.datapoints.append((data_point.value, data_point.timestamp))
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
 
@@ -193,7 +193,7 @@ class DatastoreProfileKafkaSource(DatastoreProfile):
193
193
  kwargs_public: typing.Optional[dict]
194
194
  kwargs_private: typing.Optional[dict]
195
195
 
196
- def attributes(self):
196
+ def attributes(self) -> dict[str, typing.Any]:
197
197
  attributes = {}
198
198
  if self.kwargs_public:
199
199
  attributes = merge(attributes, self.kwargs_public)
@@ -1200,19 +1200,20 @@ class KafkaSource(OnlineSource):
1200
1200
  new_topics = [
1201
1201
  NewTopic(topic, num_partitions, replication_factor) for topic in topics
1202
1202
  ]
1203
- kafka_admin = KafkaAdminClient(
1204
- bootstrap_servers=brokers,
1205
- sasl_mechanism=self.attributes.get("sasl", {}).get("sasl_mechanism"),
1206
- sasl_plain_username=self.attributes.get("sasl", {}).get("username"),
1207
- sasl_plain_password=self.attributes.get("sasl", {}).get("password"),
1208
- sasl_kerberos_service_name=self.attributes.get("sasl", {}).get(
1209
- "sasl_kerberos_service_name", "kafka"
1210
- ),
1211
- sasl_kerberos_domain_name=self.attributes.get("sasl", {}).get(
1212
- "sasl_kerberos_domain_name"
1213
- ),
1214
- sasl_oauth_token_provider=self.attributes.get("sasl", {}).get("mechanism"),
1215
- )
1203
+
1204
+ kafka_admin_kwargs = {}
1205
+ if "sasl" in self.attributes:
1206
+ sasl = self.attributes["sasl"]
1207
+ kafka_admin_kwargs.update(
1208
+ {
1209
+ "security_protocol": "SASL_PLAINTEXT",
1210
+ "sasl_mechanism": sasl["mechanism"],
1211
+ "sasl_plain_username": sasl["user"],
1212
+ "sasl_plain_password": sasl["password"],
1213
+ }
1214
+ )
1215
+
1216
+ kafka_admin = KafkaAdminClient(bootstrap_servers=brokers, **kafka_admin_kwargs)
1216
1217
  try:
1217
1218
  kafka_admin.create_topics(new_topics)
1218
1219
  finally:
@@ -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
- kwargs["url"] = mlrun.model_monitoring.helpers.get_tsdb_connection_string()
47
- super().__init__(*args, **kwargs)
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/httpdb.py CHANGED
@@ -1734,36 +1734,10 @@ class HTTPRunDB(RunDBInterface):
1734
1734
  def create_schedule(
1735
1735
  self, project: str, schedule: mlrun.common.schemas.ScheduleInput
1736
1736
  ):
1737
- """Create a new schedule on the given project. The details on the actual object to schedule as well as the
1738
- schedule itself are within the schedule object provided.
1739
- The :py:class:`~ScheduleCronTrigger` follows the guidelines in
1740
- https://apscheduler.readthedocs.io/en/3.x/modules/triggers/cron.html.
1741
- It also supports a :py:func:`~ScheduleCronTrigger.from_crontab` function that accepts a
1742
- crontab-formatted string (see https://en.wikipedia.org/wiki/Cron for more information on the format and
1743
- note that the 0 weekday is always monday).
1744
-
1745
-
1746
- Example::
1747
-
1748
- from mlrun.common import schemas
1749
-
1750
- # Execute the get_data_func function every Tuesday at 15:30
1751
- schedule = schemas.ScheduleInput(
1752
- name="run_func_on_tuesdays",
1753
- kind="job",
1754
- scheduled_object=get_data_func,
1755
- cron_trigger=schemas.ScheduleCronTrigger(
1756
- day_of_week="tue", hour=15, minute=30
1757
- ),
1758
- )
1759
- db.create_schedule(project_name, schedule)
1760
- """
1761
-
1762
- project = project or config.default_project
1763
- path = f"projects/{project}/schedules"
1764
-
1765
- error_message = f"Failed creating schedule {project}/{schedule.name}"
1766
- self.api_call("POST", path, error_message, body=dict_to_json(schedule.dict()))
1737
+ """The create_schedule functionality has been deprecated."""
1738
+ raise mlrun.errors.MLRunBadRequestError(
1739
+ "The create_schedule functionality has been deprecated."
1740
+ )
1767
1741
 
1768
1742
  def update_schedule(
1769
1743
  self, project: str, name: str, schedule: mlrun.common.schemas.ScheduleUpdate
mlrun/k8s_utils.py CHANGED
@@ -142,6 +142,7 @@ def verify_label_key(key: str, allow_k8s_prefix: bool = False):
142
142
  if not key:
143
143
  raise mlrun.errors.MLRunInvalidArgumentError("label key cannot be empty")
144
144
 
145
+ prefix = ""
145
146
  parts = key.split("/")
146
147
  if len(parts) == 1:
147
148
  name = parts[0]
@@ -180,11 +181,7 @@ def verify_label_key(key: str, allow_k8s_prefix: bool = False):
180
181
 
181
182
  # Allow the use of Kubernetes reserved prefixes ('k8s.io/' or 'kubernetes.io/')
182
183
  # only when setting node selectors, not when adding new labels.
183
- if (
184
- key.startswith("k8s.io/")
185
- or key.startswith("kubernetes.io/")
186
- and not allow_k8s_prefix
187
- ):
184
+ if not allow_k8s_prefix and prefix in {"k8s.io", "kubernetes.io"}:
188
185
  raise mlrun.errors.MLRunInvalidArgumentError(
189
186
  "Labels cannot start with 'k8s.io/' or 'kubernetes.io/'"
190
187
  )
mlrun/launcher/base.py CHANGED
@@ -401,6 +401,7 @@ class BaseLauncher(abc.ABC):
401
401
  status=run.status.state,
402
402
  name=run.metadata.name,
403
403
  )
404
+ self._update_end_time_if_terminal_state(runtime, run)
404
405
  if (
405
406
  run.status.state
406
407
  in mlrun.common.runtimes.constants.RunStates.error_and_abortion_states()
@@ -416,6 +417,21 @@ class BaseLauncher(abc.ABC):
416
417
 
417
418
  return None
418
419
 
420
+ @staticmethod
421
+ def _update_end_time_if_terminal_state(
422
+ runtime: "mlrun.runtimes.BaseRuntime", run: "mlrun.run.RunObject"
423
+ ):
424
+ if (
425
+ run.status.state
426
+ in mlrun.common.runtimes.constants.RunStates.terminal_states()
427
+ and not run.status.end_time
428
+ ):
429
+ end_time = mlrun.utils.now_date().isoformat()
430
+ updates = {"status.end_time": end_time}
431
+ runtime._get_db().update_run(
432
+ updates, run.metadata.uid, run.metadata.project
433
+ )
434
+
419
435
  @staticmethod
420
436
  def _refresh_function_metadata(runtime: "mlrun.runtimes.BaseRuntime"):
421
437
  pass
@@ -619,8 +619,8 @@ def _create_model_monitoring_function_base(
619
619
  app_step.__class__ = mlrun.serving.MonitoringApplicationStep
620
620
 
621
621
  app_step.error_handler(
622
- name="ApplicationErrorHandler",
623
622
  class_name="mlrun.model_monitoring.applications._application_steps._ApplicationErrorHandler",
623
+ name="ApplicationErrorHandler",
624
624
  full_event=True,
625
625
  project=project,
626
626
  )
@@ -629,7 +629,6 @@ def _create_model_monitoring_function_base(
629
629
  class_name="mlrun.model_monitoring.applications._application_steps._PushToMonitoringWriter",
630
630
  name="PushToMonitoringWriter",
631
631
  project=project,
632
- writer_application_name=mm_constants.MonitoringFunctionNames.WRITER,
633
632
  )
634
633
 
635
634
  def block_to_mock_server(*args, **kwargs) -> typing.NoReturn:
@@ -18,10 +18,8 @@ from typing import Any, Optional, Union
18
18
 
19
19
  import mlrun.common.schemas
20
20
  import mlrun.common.schemas.alert as alert_objects
21
- import mlrun.common.schemas.model_monitoring.constants as mm_constant
22
- import mlrun.datastore
23
- import mlrun.model_monitoring
24
- from mlrun.model_monitoring.helpers import get_stream_path
21
+ import mlrun.common.schemas.model_monitoring.constants as mm_constants
22
+ import mlrun.model_monitoring.helpers
25
23
  from mlrun.serving import GraphContext
26
24
  from mlrun.serving.utils import StepToDict
27
25
  from mlrun.utils import logger
@@ -37,29 +35,14 @@ from .results import (
37
35
  class _PushToMonitoringWriter(StepToDict):
38
36
  kind = "monitoring_application_stream_pusher"
39
37
 
40
- def __init__(
41
- self,
42
- project: str,
43
- writer_application_name: str,
44
- stream_uri: Optional[str] = None,
45
- name: Optional[str] = None,
46
- ):
38
+ def __init__(self, project: str) -> None:
47
39
  """
48
40
  Class for pushing application results to the monitoring writer stream.
49
41
 
50
- :param project: Project name.
51
- :param writer_application_name: Writer application name.
52
- :param stream_uri: Stream URI for pushing results.
53
- :param name: Name of the PushToMonitoringWriter
54
- instance default to PushToMonitoringWriter.
42
+ :param project: Project name.
55
43
  """
56
44
  self.project = project
57
- self.application_name_to_push = writer_application_name
58
- self.stream_uri = stream_uri or get_stream_path(
59
- project=self.project, function_name=self.application_name_to_push
60
- )
61
45
  self.output_stream = None
62
- self.name = name or "PushToMonitoringWriter"
63
46
 
64
47
  def do(
65
48
  self,
@@ -82,40 +65,43 @@ class _PushToMonitoringWriter(StepToDict):
82
65
  self._lazy_init()
83
66
  application_results, application_context = event
84
67
  writer_event = {
85
- mm_constant.WriterEvent.ENDPOINT_NAME: application_context.endpoint_name,
86
- mm_constant.WriterEvent.APPLICATION_NAME: application_context.application_name,
87
- mm_constant.WriterEvent.ENDPOINT_ID: application_context.endpoint_id,
88
- mm_constant.WriterEvent.START_INFER_TIME: application_context.start_infer_time.isoformat(
68
+ mm_constants.WriterEvent.ENDPOINT_NAME: application_context.endpoint_name,
69
+ mm_constants.WriterEvent.APPLICATION_NAME: application_context.application_name,
70
+ mm_constants.WriterEvent.ENDPOINT_ID: application_context.endpoint_id,
71
+ mm_constants.WriterEvent.START_INFER_TIME: application_context.start_infer_time.isoformat(
89
72
  sep=" ", timespec="microseconds"
90
73
  ),
91
- mm_constant.WriterEvent.END_INFER_TIME: application_context.end_infer_time.isoformat(
74
+ mm_constants.WriterEvent.END_INFER_TIME: application_context.end_infer_time.isoformat(
92
75
  sep=" ", timespec="microseconds"
93
76
  ),
94
77
  }
95
78
  for result in application_results:
96
79
  data = result.to_dict()
97
80
  if isinstance(result, ModelMonitoringApplicationResult):
98
- writer_event[mm_constant.WriterEvent.EVENT_KIND] = (
99
- mm_constant.WriterEventKind.RESULT
81
+ writer_event[mm_constants.WriterEvent.EVENT_KIND] = (
82
+ mm_constants.WriterEventKind.RESULT
100
83
  )
101
84
  elif isinstance(result, _ModelMonitoringApplicationStats):
102
- writer_event[mm_constant.WriterEvent.EVENT_KIND] = (
103
- mm_constant.WriterEventKind.STATS
85
+ writer_event[mm_constants.WriterEvent.EVENT_KIND] = (
86
+ mm_constants.WriterEventKind.STATS
104
87
  )
105
88
  else:
106
- writer_event[mm_constant.WriterEvent.EVENT_KIND] = (
107
- mm_constant.WriterEventKind.METRIC
89
+ writer_event[mm_constants.WriterEvent.EVENT_KIND] = (
90
+ mm_constants.WriterEventKind.METRIC
108
91
  )
109
- writer_event[mm_constant.WriterEvent.DATA] = json.dumps(data)
110
- logger.info(
111
- f"Pushing data = {writer_event} \n to stream = {self.stream_uri}"
92
+ writer_event[mm_constants.WriterEvent.DATA] = json.dumps(data)
93
+ logger.debug(
94
+ "Pushing data to output stream", writer_event=str(writer_event)
112
95
  )
113
96
  self.output_stream.push([writer_event])
114
- logger.info(f"Pushed data to {self.stream_uri} successfully")
97
+ logger.debug("Pushed data to output stream successfully")
115
98
 
116
99
  def _lazy_init(self):
117
100
  if self.output_stream is None:
118
- self.output_stream = mlrun.datastore.get_stream_pusher(self.stream_uri)
101
+ self.output_stream = mlrun.model_monitoring.helpers.get_output_stream(
102
+ project=self.project,
103
+ function_name=mm_constants.MonitoringFunctionNames.WRITER,
104
+ )
119
105
 
120
106
 
121
107
  class _PrepareMonitoringEvent(StepToDict):