mlrun 1.8.0rc28__py3-none-any.whl → 1.8.0rc30__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 (38) hide show
  1. mlrun/__init__.py +2 -1
  2. mlrun/artifacts/document.py +3 -3
  3. mlrun/artifacts/manager.py +1 -0
  4. mlrun/artifacts/model.py +3 -3
  5. mlrun/common/model_monitoring/helpers.py +16 -7
  6. mlrun/common/runtimes/constants.py +1 -0
  7. mlrun/common/schemas/model_monitoring/constants.py +3 -1
  8. mlrun/datastore/datastore_profile.py +1 -1
  9. mlrun/datastore/sources.py +14 -13
  10. mlrun/db/httpdb.py +10 -34
  11. mlrun/k8s_utils.py +2 -5
  12. mlrun/launcher/base.py +16 -0
  13. mlrun/model_monitoring/api.py +1 -2
  14. mlrun/model_monitoring/applications/_application_steps.py +23 -37
  15. mlrun/model_monitoring/applications/context.py +0 -3
  16. mlrun/model_monitoring/applications/results.py +14 -14
  17. mlrun/model_monitoring/controller.py +31 -28
  18. mlrun/model_monitoring/db/tsdb/tdengine/tdengine_connector.py +6 -2
  19. mlrun/model_monitoring/helpers.py +108 -1
  20. mlrun/model_monitoring/stream_processing.py +0 -8
  21. mlrun/projects/project.py +20 -8
  22. mlrun/run.py +2 -2
  23. mlrun/runtimes/nuclio/function.py +11 -7
  24. mlrun/serving/routers.py +3 -4
  25. mlrun/serving/server.py +10 -8
  26. mlrun/serving/states.py +11 -1
  27. mlrun/serving/v2_serving.py +5 -10
  28. mlrun/utils/async_http.py +32 -19
  29. mlrun/utils/helpers.py +4 -1
  30. mlrun/utils/logger.py +1 -0
  31. mlrun/utils/notifications/notification_pusher.py +1 -0
  32. mlrun/utils/version/version.json +2 -2
  33. {mlrun-1.8.0rc28.dist-info → mlrun-1.8.0rc30.dist-info}/METADATA +4 -4
  34. {mlrun-1.8.0rc28.dist-info → mlrun-1.8.0rc30.dist-info}/RECORD +38 -38
  35. {mlrun-1.8.0rc28.dist-info → mlrun-1.8.0rc30.dist-info}/LICENSE +0 -0
  36. {mlrun-1.8.0rc28.dist-info → mlrun-1.8.0rc30.dist-info}/WHEEL +0 -0
  37. {mlrun-1.8.0rc28.dist-info → mlrun-1.8.0rc30.dist-info}/entry_points.txt +0 -0
  38. {mlrun-1.8.0rc28.dist-info → mlrun-1.8.0rc30.dist-info}/top_level.txt +0 -0
@@ -16,7 +16,7 @@ import datetime
16
16
  import functools
17
17
  import os
18
18
  from fnmatch import fnmatchcase
19
- from typing import TYPE_CHECKING, Callable, Optional, TypedDict, cast
19
+ from typing import TYPE_CHECKING, Callable, Optional, TypedDict, Union, cast
20
20
 
21
21
  import numpy as np
22
22
  import pandas as pd
@@ -28,6 +28,7 @@ import mlrun.common.schemas.model_monitoring.constants as mm_constants
28
28
  import mlrun.data_types.infer
29
29
  import mlrun.datastore.datastore_profile
30
30
  import mlrun.model_monitoring
31
+ import mlrun.platforms.iguazio
31
32
  import mlrun.utils.helpers
32
33
  from mlrun.common.schemas import ModelEndpoint
33
34
  from mlrun.common.schemas.model_monitoring.model_endpoints import (
@@ -294,6 +295,112 @@ _get_stream_profile = functools.partial(
294
295
  )
295
296
 
296
297
 
298
+ def _get_v3io_output_stream(
299
+ *,
300
+ v3io_profile: mlrun.datastore.datastore_profile.DatastoreProfileV3io,
301
+ project: str,
302
+ function_name: str,
303
+ v3io_access_key: Optional[str],
304
+ mock: bool = False,
305
+ ) -> mlrun.platforms.iguazio.OutputStream:
306
+ stream_uri = mlrun.mlconf.get_model_monitoring_file_target_path(
307
+ project=project,
308
+ kind=mm_constants.FileTargetKind.STREAM,
309
+ target="online",
310
+ function_name=function_name,
311
+ )
312
+ endpoint, stream_path = mlrun.platforms.iguazio.parse_path(stream_uri)
313
+ return mlrun.platforms.iguazio.OutputStream(
314
+ stream_path,
315
+ endpoint=endpoint,
316
+ access_key=v3io_access_key or v3io_profile.v3io_access_key,
317
+ mock=mock,
318
+ )
319
+
320
+
321
+ def _get_kafka_output_stream(
322
+ *,
323
+ kafka_profile: mlrun.datastore.datastore_profile.DatastoreProfileKafkaSource,
324
+ project: str,
325
+ function_name: str,
326
+ mock: bool = False,
327
+ ) -> mlrun.platforms.iguazio.KafkaOutputStream:
328
+ topic = mlrun.common.model_monitoring.helpers.get_kafka_topic(
329
+ project=project, function_name=function_name
330
+ )
331
+ profile_attributes = kafka_profile.attributes()
332
+ producer_options = profile_attributes.get("producer_options", {})
333
+ if "sasl" in profile_attributes:
334
+ sasl = profile_attributes["sasl"]
335
+ producer_options.update(
336
+ {
337
+ "security_protocol": "SASL_PLAINTEXT",
338
+ "sasl_mechanism": sasl["mechanism"],
339
+ "sasl_plain_username": sasl["user"],
340
+ "sasl_plain_password": sasl["password"],
341
+ },
342
+ )
343
+ return mlrun.platforms.iguazio.KafkaOutputStream(
344
+ brokers=kafka_profile.brokers,
345
+ topic=topic,
346
+ producer_options=producer_options,
347
+ mock=mock,
348
+ )
349
+
350
+
351
+ def get_output_stream(
352
+ project: str,
353
+ function_name: str = mm_constants.MonitoringFunctionNames.STREAM,
354
+ secret_provider: Optional[Callable[[str], str]] = None,
355
+ profile: Optional[mlrun.datastore.datastore_profile.DatastoreProfile] = None,
356
+ v3io_access_key: Optional[str] = None,
357
+ mock: bool = False,
358
+ ) -> Union[
359
+ mlrun.platforms.iguazio.OutputStream, mlrun.platforms.iguazio.KafkaOutputStream
360
+ ]:
361
+ """
362
+ Get stream path from the project secret. If wasn't set, take it from the system configurations
363
+
364
+ :param project: Project name.
365
+ :param function_name: Application name. Default is model_monitoring_stream.
366
+ :param secret_provider: Optional secret provider to get the connection string secret.
367
+ If not set, the env vars are used.
368
+ :param profile: Optional datastore profile of the stream (V3IO/KafkaSource profile).
369
+ :param v3io_access_key: Optional V3IO access key.
370
+ :param mock: Should the output stream be mocked or not.
371
+ :return: Monitoring stream path to the relevant application.
372
+ """
373
+
374
+ profile = profile or _get_stream_profile(
375
+ project=project, secret_provider=secret_provider
376
+ )
377
+
378
+ if isinstance(profile, mlrun.datastore.datastore_profile.DatastoreProfileV3io):
379
+ return _get_v3io_output_stream(
380
+ v3io_profile=profile,
381
+ project=project,
382
+ function_name=function_name,
383
+ v3io_access_key=v3io_access_key,
384
+ mock=mock,
385
+ )
386
+
387
+ elif isinstance(
388
+ profile, mlrun.datastore.datastore_profile.DatastoreProfileKafkaSource
389
+ ):
390
+ return _get_kafka_output_stream(
391
+ kafka_profile=profile,
392
+ project=project,
393
+ function_name=function_name,
394
+ mock=mock,
395
+ )
396
+
397
+ else:
398
+ raise mlrun.errors.MLRunValueError(
399
+ f"Received an unexpected stream profile type: {type(profile)}\n"
400
+ "Expects `DatastoreProfileV3io` or `DatastoreProfileKafkaSource`."
401
+ )
402
+
403
+
297
404
  def batch_dict2timedelta(batch_dict: _BatchDict) -> datetime.timedelta:
298
405
  """
299
406
  Convert a batch dictionary to timedelta.
@@ -20,11 +20,8 @@ import storey
20
20
 
21
21
  import mlrun
22
22
  import mlrun.common.model_monitoring.helpers
23
- import mlrun.config
24
- import mlrun.datastore.targets
25
23
  import mlrun.feature_store as fstore
26
24
  import mlrun.feature_store.steps
27
- import mlrun.model_monitoring.db
28
25
  import mlrun.serving.states
29
26
  import mlrun.utils
30
27
  from mlrun.common.schemas.model_monitoring.constants import (
@@ -392,10 +389,6 @@ class ProcessEndpointEvent(mlrun.feature_store.steps.MapClass):
392
389
  if not is_not_none(model, [EventFieldType.MODEL]):
393
390
  return None
394
391
 
395
- version = full_event.body.get(EventFieldType.VERSION)
396
- versioned_model = f"{model}:{version}" if version else f"{model}:latest"
397
-
398
- full_event.body[EventFieldType.VERSIONED_MODEL] = versioned_model
399
392
  endpoint_id = event[EventFieldType.ENDPOINT_ID]
400
393
 
401
394
  # In case this process fails, resume state from existing record
@@ -493,7 +486,6 @@ class ProcessEndpointEvent(mlrun.feature_store.steps.MapClass):
493
486
  events.append(
494
487
  {
495
488
  EventFieldType.FUNCTION_URI: function_uri,
496
- EventFieldType.MODEL: versioned_model,
497
489
  EventFieldType.ENDPOINT_NAME: event.get(EventFieldType.MODEL),
498
490
  EventFieldType.MODEL_CLASS: model_class,
499
491
  EventFieldType.TIMESTAMP: timestamp,
mlrun/projects/project.py CHANGED
@@ -1254,8 +1254,14 @@ class MlrunProject(ModelObj):
1254
1254
  mlrun.utils.helpers.validate_builder_source(source, pull_at_runtime, workdir)
1255
1255
 
1256
1256
  self.spec.load_source_on_run = pull_at_runtime
1257
+
1258
+ source_has_changed = source != self.spec.source
1257
1259
  self.spec.source = source or self.spec.source
1258
1260
 
1261
+ # new source should not relay on old workdir
1262
+ if source_has_changed:
1263
+ self.spec.workdir = workdir
1264
+
1259
1265
  if self.spec.source.startswith("git://"):
1260
1266
  source, reference, branch = resolve_git_reference_from_source(source)
1261
1267
  if not branch and not reference:
@@ -1264,7 +1270,6 @@ class MlrunProject(ModelObj):
1264
1270
  "'git://<url>/org/repo.git#<branch-name or refs/heads/..>'"
1265
1271
  )
1266
1272
 
1267
- self.spec.workdir = workdir or self.spec.workdir
1268
1273
  try:
1269
1274
  # reset function objects (to recalculate build attributes)
1270
1275
  self.sync_functions()
@@ -2217,6 +2222,12 @@ class MlrunProject(ModelObj):
2217
2222
  reset_policy=reset_policy,
2218
2223
  )
2219
2224
  )
2225
+ if not alerts:
2226
+ warnings.warn(
2227
+ "No alert config has been created. "
2228
+ "Try specifying a result name explicitly or verifying that results are available"
2229
+ )
2230
+
2220
2231
  return alerts
2221
2232
 
2222
2233
  def set_model_monitoring_function(
@@ -3456,12 +3467,13 @@ class MlrunProject(ModelObj):
3456
3467
 
3457
3468
  arguments = arguments or {}
3458
3469
  need_repo = self.spec._need_repo()
3459
- if self.spec.repo and self.spec.repo.is_dirty():
3460
- msg = "You seem to have uncommitted git changes, use .push()"
3461
- if dirty or not need_repo:
3462
- logger.warning("WARNING!, " + msg)
3463
- else:
3464
- raise ProjectError(msg + " or dirty=True")
3470
+ if not dirty:
3471
+ if self.spec.repo and self.spec.repo.is_dirty():
3472
+ msg = "You seem to have uncommitted git changes, use .push()"
3473
+ if not need_repo:
3474
+ logger.warning("WARNING!, " + msg)
3475
+ else:
3476
+ raise ProjectError(msg + " or dirty=True")
3465
3477
 
3466
3478
  if need_repo and self.spec.repo and not self.spec.source:
3467
3479
  raise ProjectError(
@@ -4120,7 +4132,7 @@ class MlrunProject(ModelObj):
4120
4132
 
4121
4133
  :param image: target image name/path. If not specified the project's existing `default_image` name will be
4122
4134
  used. If not set, the `mlconf.default_project_image_name` value will be used
4123
- :param set_as_default: set `image` to be the project's default image (default False)
4135
+ :param set_as_default: set `image` to be the project's default image (default True)
4124
4136
  :param with_mlrun: add the current mlrun package to the container build
4125
4137
  :param base_image: base image name/path (commands and source code will be added to it) defaults to
4126
4138
  mlrun.mlconf.default_base_image
mlrun/run.py CHANGED
@@ -911,7 +911,7 @@ def _run_pipeline(
911
911
 
912
912
  def retry_pipeline(
913
913
  run_id: str,
914
- project: Optional[str] = None,
914
+ project: str,
915
915
  namespace: Optional[str] = None,
916
916
  ) -> str:
917
917
  """Retry a pipeline run.
@@ -920,7 +920,7 @@ def retry_pipeline(
920
920
  retryable state, a new run is created as a clone of the original run.
921
921
 
922
922
  :param run_id: ID of the pipeline run to retry.
923
- :param project: Optional; name of the project associated with the pipeline run.
923
+ :param project: name of the project associated with the pipeline run.
924
924
  :param namespace: Optional; Kubernetes namespace to use if not the default.
925
925
 
926
926
  :returns: ID of the retried pipeline run or the ID of a cloned run if the original run is not retryable.
@@ -369,8 +369,9 @@ class RemoteRuntime(KubeResource):
369
369
  )
370
370
  """
371
371
  self.spec.build.source = source
372
- # update handler in function_handler
373
- self.spec.function_handler = handler
372
+ # update handler in function_handler if needed
373
+ if handler:
374
+ self.spec.function_handler = handler
374
375
  if workdir:
375
376
  self.spec.workdir = workdir
376
377
  if runtime:
@@ -600,13 +601,15 @@ class RemoteRuntime(KubeResource):
600
601
  # this also means that the function object will be updated with the function status
601
602
  self._wait_for_function_deployment(db, verbose=verbose)
602
603
  # check if there are any background tasks related to creating model endpoints
603
- background_tasks = mlrun.common.schemas.BackgroundTaskList(
604
- **data.pop("background_tasks", {"background_tasks": []})
605
- ).background_tasks
606
- if background_tasks:
604
+ model_endpoints_creation_background_tasks = (
605
+ mlrun.common.schemas.BackgroundTaskList(
606
+ **data.pop("background_tasks", {"background_tasks": []})
607
+ ).background_tasks
608
+ )
609
+ if model_endpoints_creation_background_tasks:
607
610
  self._check_model_endpoint_task_state(
608
611
  db=db,
609
- background_task=background_tasks[0],
612
+ background_task=model_endpoints_creation_background_tasks[0],
610
613
  wait_for_completion=False,
611
614
  )
612
615
 
@@ -1014,6 +1017,7 @@ class RemoteRuntime(KubeResource):
1014
1017
  ):
1015
1018
  """
1016
1019
  Add a sidecar container to the function pod
1020
+
1017
1021
  :param name: Sidecar container name.
1018
1022
  :param image: Sidecar container image.
1019
1023
  :param ports: Sidecar container ports to expose. Can be a single port or a list of ports.
mlrun/serving/routers.py CHANGED
@@ -596,7 +596,6 @@ class VotingEnsemble(ParallelRun):
596
596
  self.vote_type = vote_type
597
597
  self.vote_flag = True if self.vote_type is not None else False
598
598
  self.weights = weights
599
- self.version = kwargs.get("version", "v1")
600
599
  self.log_router = True
601
600
  self.prediction_col_name = prediction_col_name or "prediction"
602
601
  self.format_response_with_col_name_flag = format_response_with_col_name_flag
@@ -927,14 +926,14 @@ class VotingEnsemble(ParallelRun):
927
926
  "model_name": self.name,
928
927
  "outputs": votes,
929
928
  }
930
- if self.version:
931
- response_body["model_version"] = self.version
929
+ if self.model_endpoint_uid:
930
+ response_body["model_endpoint_uid"] = self.model_endpoint_uid
932
931
  response.body = response_body
933
932
  elif name == self.name and event.method == "GET" and not subpath:
934
933
  response = copy.copy(event)
935
934
  response_body = {
936
935
  "name": self.name,
937
- "version": self.version or "",
936
+ "model_endpoint_uid": self.model_endpoint_uid or "",
938
937
  "inputs": [],
939
938
  "outputs": [],
940
939
  }
mlrun/serving/server.py CHANGED
@@ -65,7 +65,7 @@ class _StreamContext:
65
65
  self.hostname = socket.gethostname()
66
66
  self.function_uri = function_uri
67
67
  self.output_stream = None
68
- self.stream_uri = None
68
+ stream_uri = None
69
69
  log_stream = parameters.get(FileTargetKind.LOG_STREAM, "")
70
70
 
71
71
  if (enabled or log_stream) and function_uri:
@@ -78,17 +78,19 @@ class _StreamContext:
78
78
 
79
79
  if log_stream == DUMMY_STREAM:
80
80
  # Dummy stream used for testing, see tests/serving/test_serving.py
81
- self.stream_uri = DUMMY_STREAM
81
+ stream_uri = DUMMY_STREAM
82
82
  elif not stream_args.get("mock"): # if not a mock: `context.is_mock = True`
83
- self.stream_uri = mlrun.model_monitoring.get_stream_path(
84
- project=project
85
- )
83
+ stream_uri = mlrun.model_monitoring.get_stream_path(project=project)
86
84
 
87
85
  if log_stream:
88
86
  # Update the stream path to the log stream value
89
- self.stream_uri = log_stream.format(project=project)
90
-
91
- self.output_stream = get_stream_pusher(self.stream_uri, **stream_args)
87
+ stream_uri = log_stream.format(project=project)
88
+ self.output_stream = get_stream_pusher(stream_uri, **stream_args)
89
+ else:
90
+ # Get the output stream from the profile
91
+ self.output_stream = mlrun.model_monitoring.helpers.get_output_stream(
92
+ project=project, mock=stream_args.get("mock", False)
93
+ )
92
94
 
93
95
 
94
96
  class GraphServer(ModelObj):
mlrun/serving/states.py CHANGED
@@ -652,6 +652,14 @@ class TaskStep(BaseStep):
652
652
  if isinstance(self.endpoint_type, schemas.EndpointType)
653
653
  else self.endpoint_type
654
654
  )
655
+ self.model_endpoint_creation_strategy = (
656
+ self.model_endpoint_creation_strategy.value
657
+ if isinstance(
658
+ self.model_endpoint_creation_strategy,
659
+ schemas.ModelEndpointCreationStrategy,
660
+ )
661
+ else self.model_endpoint_creation_strategy
662
+ )
655
663
  return super().to_dict(fields, exclude, strip)
656
664
 
657
665
 
@@ -755,9 +763,11 @@ class RouterStep(TaskStep):
755
763
  self._routes: ObjectDict = None
756
764
  self.routes = routes
757
765
  self.endpoint_type = schemas.EndpointType.ROUTER
766
+ if isinstance(class_name, type):
767
+ class_name = class_name.__name__
758
768
  self.model_endpoint_creation_strategy = (
759
769
  schemas.ModelEndpointCreationStrategy.INPLACE
760
- if class_name and "serving.VotingEnsemble" in class_name
770
+ if class_name and "VotingEnsemble" in class_name
761
771
  else schemas.ModelEndpointCreationStrategy.SKIP
762
772
  )
763
773
 
@@ -95,9 +95,6 @@ class V2ModelServer(StepToDict):
95
95
  :param kwargs: extra arguments (can be accessed using self.get_param(key))
96
96
  """
97
97
  self.name = name
98
- self.version = ""
99
- if name and ":" in name:
100
- self.version = name.split(":", 1)[-1]
101
98
  self.context = context
102
99
  self.ready = False
103
100
  self.error = ""
@@ -325,8 +322,8 @@ class V2ModelServer(StepToDict):
325
322
  "outputs": outputs,
326
323
  "timestamp": start.isoformat(sep=" ", timespec="microseconds"),
327
324
  }
328
- if self.version:
329
- response["model_version"] = self.version
325
+ if self.model_endpoint_uid:
326
+ response["model_endpoint_uid"] = self.model_endpoint_uid
330
327
  elif op == "ready" and event.method == "GET":
331
328
  # get model health operation
332
329
  setattr(event, "terminated", True)
@@ -352,7 +349,7 @@ class V2ModelServer(StepToDict):
352
349
  setattr(event, "terminated", True)
353
350
  event_body = {
354
351
  "name": self.name.split(":")[0],
355
- "version": self.version or "",
352
+ "model_endpoint_uid": self.model_endpoint_uid or "",
356
353
  "inputs": [],
357
354
  "outputs": [],
358
355
  }
@@ -386,8 +383,8 @@ class V2ModelServer(StepToDict):
386
383
  "model_name": self.name,
387
384
  "outputs": outputs,
388
385
  }
389
- if self.version:
390
- response["model_version"] = self.version
386
+ if self.model_endpoint_uid:
387
+ response["model_endpoint_uid"] = self.model_endpoint_uid
391
388
 
392
389
  elif hasattr(self, "op_" + op):
393
390
  # custom operation (child methods starting with "op_")
@@ -510,7 +507,6 @@ class _ModelLogPusher:
510
507
  self.verbose = context.verbose
511
508
  self.hostname = context.stream.hostname
512
509
  self.function_uri = context.stream.function_uri
513
- self.stream_path = context.stream.stream_uri
514
510
  self.sampling_percentage = float(context.get_param("sampling_percentage", 100))
515
511
  self.output_stream = output_stream or context.stream.output_stream
516
512
  self._worker = context.worker_id
@@ -520,7 +516,6 @@ class _ModelLogPusher:
520
516
  "class": self.model.__class__.__name__,
521
517
  "worker": self._worker,
522
518
  "model": self.model.name,
523
- "version": self.model.version,
524
519
  "host": self.hostname,
525
520
  "function_uri": self.function_uri,
526
521
  "endpoint_id": self.model.model_endpoint_uid,
mlrun/utils/async_http.py CHANGED
@@ -24,10 +24,17 @@ from aiohttp_retry import ExponentialRetry, RequestParams, RetryClient, RetryOpt
24
24
  from aiohttp_retry.client import _RequestContext
25
25
 
26
26
  from mlrun.config import config
27
- from mlrun.errors import err_to_str, raise_for_status
27
+ from mlrun.errors import err_to_str
28
+ from mlrun.errors import raise_for_status as ml_raise_for_status
28
29
 
29
30
  from .helpers import logger as mlrun_logger
30
31
 
32
+ DEFAULT_BLACKLISTED_METHODS = [
33
+ "POST",
34
+ "PUT",
35
+ "PATCH",
36
+ ]
37
+
31
38
 
32
39
  class AsyncClientWithRetry(RetryClient):
33
40
  """
@@ -46,16 +53,6 @@ class AsyncClientWithRetry(RetryClient):
46
53
  *args,
47
54
  **kwargs,
48
55
  ):
49
- # do not retry on PUT / PATCH as they might have side effects (not truly idempotent)
50
- blacklisted_methods = (
51
- blacklisted_methods
52
- if blacklisted_methods is not None
53
- else [
54
- "POST",
55
- "PUT",
56
- "PATCH",
57
- ]
58
- )
59
56
  super().__init__(
60
57
  *args,
61
58
  retry_options=ExponentialRetryOverride(
@@ -72,9 +69,13 @@ class AsyncClientWithRetry(RetryClient):
72
69
  **kwargs,
73
70
  )
74
71
 
72
+ @property
73
+ def retry_options(self) -> "ExponentialRetryOverride":
74
+ return typing.cast(ExponentialRetryOverride, self._retry_options)
75
+
75
76
  def methods_blacklist_update_required(self, new_blacklist: str):
76
77
  self._retry_options: ExponentialRetryOverride
77
- return set(self._retry_options.blacklisted_methods).difference(
78
+ return set(self.retry_options.blacklisted_methods).difference(
78
79
  set(new_blacklist)
79
80
  )
80
81
 
@@ -114,8 +115,8 @@ class ExponentialRetryOverride(ExponentialRetry):
114
115
 
115
116
  def __init__(
116
117
  self,
117
- retry_on_exception: bool,
118
- blacklisted_methods: list[str],
118
+ retry_on_exception: typing.Optional[bool] = True,
119
+ blacklisted_methods: typing.Optional[list[str]] = None,
119
120
  *args,
120
121
  **kwargs,
121
122
  ):
@@ -123,7 +124,12 @@ class ExponentialRetryOverride(ExponentialRetry):
123
124
  self.retry_on_exception = retry_on_exception
124
125
 
125
126
  # methods that should not be retried
126
- self.blacklisted_methods = blacklisted_methods
127
+ # do not retry on PUT / PATCH as they might have side effects (not truly idempotent)
128
+ self.blacklisted_methods = (
129
+ blacklisted_methods
130
+ if blacklisted_methods is not None
131
+ else DEFAULT_BLACKLISTED_METHODS
132
+ )
127
133
 
128
134
  # default exceptions that should be retried on (when retry_on_exception is True)
129
135
  if "exceptions" not in kwargs:
@@ -136,6 +142,10 @@ class _CustomRequestContext(_RequestContext):
136
142
  Extends by adding retry logic on both error statuses and certain exceptions.
137
143
  """
138
144
 
145
+ @property
146
+ def retry_options(self) -> ExponentialRetryOverride:
147
+ return typing.cast(ExponentialRetryOverride, self._retry_options)
148
+
139
149
  async def _do_request(self) -> aiohttp.ClientResponse:
140
150
  current_attempt = 0
141
151
  while True:
@@ -186,9 +196,9 @@ class _CustomRequestContext(_RequestContext):
186
196
  return response
187
197
 
188
198
  last_attempt = current_attempt == self._retry_options.attempts
189
- if self._is_status_code_ok(response.status) or last_attempt:
199
+ if not self._is_status_retryable(response.status) or last_attempt:
190
200
  if self._raise_for_status:
191
- raise_for_status(response)
201
+ ml_raise_for_status(response)
192
202
 
193
203
  self._response = response
194
204
  return response
@@ -204,7 +214,7 @@ class _CustomRequestContext(_RequestContext):
204
214
  )
205
215
 
206
216
  except Exception as exc:
207
- if not self._retry_options.retry_on_exception:
217
+ if not self.retry_options.retry_on_exception:
208
218
  self._logger.warning(
209
219
  "Request failed, retry on exception is disabled",
210
220
  method=params.method,
@@ -250,7 +260,10 @@ class _CustomRequestContext(_RequestContext):
250
260
  await asyncio.sleep(retry_wait)
251
261
 
252
262
  def _is_method_retryable(self, method: str):
253
- return method not in self._retry_options.blacklisted_methods
263
+ return method not in self.retry_options.blacklisted_methods
264
+
265
+ def _is_status_retryable(self, status: int):
266
+ return status in self._retry_options.statuses
254
267
 
255
268
  async def _check_response_callback(
256
269
  self,
mlrun/utils/helpers.py CHANGED
@@ -34,6 +34,7 @@ from importlib import import_module, reload
34
34
  from os import path
35
35
  from types import ModuleType
36
36
  from typing import Any, Optional
37
+ from urllib.parse import urlparse
37
38
 
38
39
  import git
39
40
  import inflection
@@ -1736,7 +1737,9 @@ setting partitioned=False"""
1736
1737
 
1737
1738
  def is_ecr_url(registry: str) -> bool:
1738
1739
  # example URL: <aws_account_id>.dkr.ecr.<region>.amazonaws.com
1739
- return ".ecr." in registry and ".amazonaws.com" in registry
1740
+ parsed_url = urlparse(f"https://{registry}")
1741
+ hostname = parsed_url.hostname
1742
+ return hostname and ".ecr." in hostname and hostname.endswith(".amazonaws.com")
1740
1743
 
1741
1744
 
1742
1745
  def get_local_file_schema() -> list:
mlrun/utils/logger.py CHANGED
@@ -53,6 +53,7 @@ class _BaseFormatter(logging.Formatter):
53
53
  json_object,
54
54
  option=orjson.OPT_NAIVE_UTC
55
55
  | orjson.OPT_SERIALIZE_NUMPY
56
+ | orjson.OPT_NON_STR_KEYS
56
57
  | orjson.OPT_SORT_KEYS,
57
58
  default=default,
58
59
  ).decode()
@@ -55,6 +55,7 @@ class _NotificationPusherBase:
55
55
  event_loop = asyncio.get_event_loop()
56
56
  except RuntimeError:
57
57
  event_loop = asyncio.new_event_loop()
58
+ asyncio.set_event_loop(event_loop)
58
59
 
59
60
  if not event_loop.is_running():
60
61
  event_loop.run_until_complete(async_push_callback())
@@ -1,4 +1,4 @@
1
1
  {
2
- "git_commit": "f7d4248834e0690d0e02b88fe62c16f9b40378e0",
3
- "version": "1.8.0-rc28"
2
+ "git_commit": "d77b75367fbca42653b1bcfcee75ff821848202b",
3
+ "version": "1.8.0-rc30"
4
4
  }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: mlrun
3
- Version: 1.8.0rc28
3
+ Version: 1.8.0rc30
4
4
  Summary: Tracking and config of machine learning runs
5
5
  Home-page: https://github.com/mlrun/mlrun
6
6
  Author: Yaron Haviv
@@ -23,8 +23,8 @@ Description-Content-Type: text/markdown
23
23
  License-File: LICENSE
24
24
  Requires-Dist: urllib3<1.27,>=1.26.9
25
25
  Requires-Dist: GitPython>=3.1.41,~=3.1
26
- Requires-Dist: aiohttp~=3.10.0
27
- Requires-Dist: aiohttp-retry~=2.8.0
26
+ Requires-Dist: aiohttp~=3.11
27
+ Requires-Dist: aiohttp-retry~=2.9
28
28
  Requires-Dist: click~=8.1
29
29
  Requires-Dist: nest-asyncio~=1.0
30
30
  Requires-Dist: ipython~=8.10
@@ -51,7 +51,7 @@ Requires-Dist: setuptools>=75.2
51
51
  Requires-Dist: deprecated~=1.2
52
52
  Requires-Dist: jinja2>=3.1.3,~=3.1
53
53
  Requires-Dist: orjson<4,>=3.9.15
54
- Requires-Dist: mlrun-pipelines-kfp-common~=0.3.6
54
+ Requires-Dist: mlrun-pipelines-kfp-common~=0.3.7
55
55
  Requires-Dist: mlrun-pipelines-kfp-v1-8~=0.3.5; python_version < "3.11"
56
56
  Requires-Dist: docstring_parser~=0.16
57
57
  Requires-Dist: aiosmtplib~=3.0