mlrun 1.6.2rc6__py3-none-any.whl → 1.6.3rc3__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 (57) hide show
  1. mlrun/artifacts/model.py +28 -22
  2. mlrun/common/db/sql_session.py +3 -0
  3. mlrun/common/model_monitoring/helpers.py +4 -2
  4. mlrun/common/schemas/__init__.py +2 -0
  5. mlrun/common/schemas/common.py +40 -0
  6. mlrun/common/schemas/model_monitoring/__init__.py +1 -0
  7. mlrun/common/schemas/model_monitoring/constants.py +21 -5
  8. mlrun/common/schemas/project.py +2 -0
  9. mlrun/config.py +51 -20
  10. mlrun/data_types/data_types.py +4 -0
  11. mlrun/datastore/azure_blob.py +9 -9
  12. mlrun/datastore/base.py +22 -44
  13. mlrun/datastore/google_cloud_storage.py +6 -6
  14. mlrun/datastore/v3io.py +70 -46
  15. mlrun/db/base.py +18 -0
  16. mlrun/db/httpdb.py +41 -36
  17. mlrun/execution.py +3 -3
  18. mlrun/frameworks/tf_keras/callbacks/logging_callback.py +3 -3
  19. mlrun/frameworks/tf_keras/model_handler.py +7 -7
  20. mlrun/k8s_utils.py +10 -5
  21. mlrun/kfpops.py +19 -10
  22. mlrun/model.py +6 -0
  23. mlrun/model_monitoring/api.py +8 -8
  24. mlrun/model_monitoring/batch.py +1 -1
  25. mlrun/model_monitoring/controller.py +0 -7
  26. mlrun/model_monitoring/features_drift_table.py +6 -0
  27. mlrun/model_monitoring/helpers.py +4 -1
  28. mlrun/model_monitoring/stores/kv_model_endpoint_store.py +13 -13
  29. mlrun/model_monitoring/stores/sql_model_endpoint_store.py +0 -1
  30. mlrun/model_monitoring/stream_processing.py +50 -36
  31. mlrun/package/packagers/pandas_packagers.py +3 -3
  32. mlrun/package/utils/_archiver.py +3 -1
  33. mlrun/platforms/iguazio.py +6 -65
  34. mlrun/projects/pipelines.py +29 -12
  35. mlrun/projects/project.py +69 -55
  36. mlrun/run.py +2 -0
  37. mlrun/runtimes/base.py +24 -1
  38. mlrun/runtimes/function.py +9 -9
  39. mlrun/runtimes/kubejob.py +5 -3
  40. mlrun/runtimes/local.py +2 -2
  41. mlrun/runtimes/mpijob/abstract.py +6 -6
  42. mlrun/runtimes/pod.py +3 -3
  43. mlrun/runtimes/serving.py +3 -3
  44. mlrun/runtimes/sparkjob/spark3job.py +3 -3
  45. mlrun/serving/remote.py +4 -2
  46. mlrun/utils/async_http.py +3 -3
  47. mlrun/utils/helpers.py +20 -0
  48. mlrun/utils/http.py +3 -3
  49. mlrun/utils/logger.py +2 -2
  50. mlrun/utils/notifications/notification_pusher.py +6 -6
  51. mlrun/utils/version/version.json +2 -2
  52. {mlrun-1.6.2rc6.dist-info → mlrun-1.6.3rc3.dist-info}/METADATA +15 -17
  53. {mlrun-1.6.2rc6.dist-info → mlrun-1.6.3rc3.dist-info}/RECORD +57 -56
  54. {mlrun-1.6.2rc6.dist-info → mlrun-1.6.3rc3.dist-info}/LICENSE +0 -0
  55. {mlrun-1.6.2rc6.dist-info → mlrun-1.6.3rc3.dist-info}/WHEEL +0 -0
  56. {mlrun-1.6.2rc6.dist-info → mlrun-1.6.3rc3.dist-info}/entry_points.txt +0 -0
  57. {mlrun-1.6.2rc6.dist-info → mlrun-1.6.3rc3.dist-info}/top_level.txt +0 -0
@@ -19,6 +19,7 @@ import plotly.graph_objects as go
19
19
  from plotly.subplots import make_subplots
20
20
 
21
21
  import mlrun.common.schemas.model_monitoring
22
+ import mlrun.common.schemas.model_monitoring.constants as mm_constants
22
23
 
23
24
  # A type for representing a drift result, a tuple of the status and the drift mean:
24
25
  DriftResultType = Tuple[mlrun.common.schemas.model_monitoring.DriftStatus, float]
@@ -112,6 +113,11 @@ class FeaturesDriftTablePlot:
112
113
  :return: The full path to the html file of the plot.
113
114
  """
114
115
  # Plot the drift table:
116
+ features = [
117
+ feature
118
+ for feature in features
119
+ if feature not in mm_constants.FeatureSetFeatures.list()
120
+ ]
115
121
  figure = self._plot(
116
122
  features=features,
117
123
  sample_set_statistics=sample_set_statistics,
@@ -41,7 +41,7 @@ class _MLRunNoRunsFoundError(Exception):
41
41
  pass
42
42
 
43
43
 
44
- def get_stream_path(project: str = None, application_name: str = None):
44
+ def get_stream_path(project: str = None, application_name: str = None) -> str:
45
45
  """
46
46
  Get stream path from the project secret. If wasn't set, take it from the system configurations
47
47
 
@@ -62,6 +62,9 @@ def get_stream_path(project: str = None, application_name: str = None):
62
62
  application_name=application_name,
63
63
  )
64
64
 
65
+ if isinstance(stream_uri, list): # ML-6043 - user side gets only the new stream uri
66
+ stream_uri = stream_uri[1]
67
+
65
68
  return mlrun.common.model_monitoring.helpers.parse_monitoring_stream_path(
66
69
  stream_uri=stream_uri, project=project, application_name=application_name
67
70
  )
@@ -540,24 +540,24 @@ class KVModelEndpointStore(ModelEndpointStore):
540
540
  and endpoint[mlrun.common.schemas.model_monitoring.EventFieldType.METRICS]
541
541
  == "null"
542
542
  ):
543
- endpoint[
544
- mlrun.common.schemas.model_monitoring.EventFieldType.METRICS
545
- ] = json.dumps(
546
- {
547
- mlrun.common.schemas.model_monitoring.EventKeyMetrics.GENERIC: {
548
- mlrun.common.schemas.model_monitoring.EventLiveStats.LATENCY_AVG_1H: 0,
549
- mlrun.common.schemas.model_monitoring.EventLiveStats.PREDICTIONS_PER_SECOND: 0,
543
+ endpoint[mlrun.common.schemas.model_monitoring.EventFieldType.METRICS] = (
544
+ json.dumps(
545
+ {
546
+ mlrun.common.schemas.model_monitoring.EventKeyMetrics.GENERIC: {
547
+ mlrun.common.schemas.model_monitoring.EventLiveStats.LATENCY_AVG_1H: 0,
548
+ mlrun.common.schemas.model_monitoring.EventLiveStats.PREDICTIONS_PER_SECOND: 0,
549
+ }
550
550
  }
551
- }
551
+ )
552
552
  )
553
553
  # Validate key `uid` instead of `endpoint_id`
554
554
  # For backwards compatibility reasons, we replace the `endpoint_id` with `uid` which is the updated key name
555
555
  if mlrun.common.schemas.model_monitoring.EventFieldType.ENDPOINT_ID in endpoint:
556
- endpoint[
557
- mlrun.common.schemas.model_monitoring.EventFieldType.UID
558
- ] = endpoint[
559
- mlrun.common.schemas.model_monitoring.EventFieldType.ENDPOINT_ID
560
- ]
556
+ endpoint[mlrun.common.schemas.model_monitoring.EventFieldType.UID] = (
557
+ endpoint[
558
+ mlrun.common.schemas.model_monitoring.EventFieldType.ENDPOINT_ID
559
+ ]
560
+ )
561
561
 
562
562
  @staticmethod
563
563
  def _encode_field(field: typing.Union[str, bytes]) -> bytes:
@@ -31,7 +31,6 @@ from .models import get_model_endpoints_table
31
31
 
32
32
 
33
33
  class SQLModelEndpointStore(ModelEndpointStore):
34
-
35
34
  """
36
35
  Handles the DB operations when the DB target is from type SQL. For the SQL operations, we use SQLAlchemy, a Python
37
36
  SQL toolkit that handles the communication with the database. When using SQL for storing the model endpoints
@@ -24,6 +24,7 @@ import mlrun
24
24
  import mlrun.common.model_monitoring.helpers
25
25
  import mlrun.config
26
26
  import mlrun.datastore.targets
27
+ import mlrun.feature_store as fstore
27
28
  import mlrun.feature_store.steps
28
29
  import mlrun.model_monitoring.prometheus
29
30
  import mlrun.serving.states
@@ -49,7 +50,7 @@ class EventStreamProcessor:
49
50
  parquet_batching_timeout_secs: int,
50
51
  parquet_target: str,
51
52
  sample_window: int = 10,
52
- aggregate_windows: typing.Optional[typing.List[str]] = None,
53
+ aggregate_windows: typing.Optional[list[str]] = None,
53
54
  aggregate_period: str = "30s",
54
55
  model_monitoring_access_key: str = None,
55
56
  ):
@@ -587,6 +588,8 @@ class ProcessBeforeParquet(mlrun.feature_store.steps.MapClass):
587
588
  for key in [
588
589
  EventFieldType.FEATURES,
589
590
  EventFieldType.NAMED_FEATURES,
591
+ EventFieldType.PREDICTION,
592
+ EventFieldType.NAMED_PREDICTIONS,
590
593
  ]:
591
594
  event.pop(key, None)
592
595
 
@@ -629,14 +632,14 @@ class ProcessEndpointEvent(mlrun.feature_store.steps.MapClass):
629
632
  self.project: str = project
630
633
 
631
634
  # First and last requests timestamps (value) of each endpoint (key)
632
- self.first_request: typing.Dict[str, str] = dict()
633
- self.last_request: typing.Dict[str, str] = dict()
635
+ self.first_request: dict[str, str] = dict()
636
+ self.last_request: dict[str, str] = dict()
634
637
 
635
638
  # Number of errors (value) per endpoint (key)
636
- self.error_count: typing.Dict[str, int] = collections.defaultdict(int)
639
+ self.error_count: dict[str, int] = collections.defaultdict(int)
637
640
 
638
641
  # Set of endpoints in the current events
639
- self.endpoints: typing.Set[str] = set()
642
+ self.endpoints: set[str] = set()
640
643
 
641
644
  def do(self, full_event):
642
645
  event = full_event.body
@@ -745,18 +748,12 @@ class ProcessEndpointEvent(mlrun.feature_store.steps.MapClass):
745
748
  # in list of events. This list will be used as the body for the storey event.
746
749
  events = []
747
750
  for i, (feature, prediction) in enumerate(zip(features, predictions)):
748
- # Validate that inputs are based on numeric values
749
- if not self.is_valid(
750
- endpoint_id,
751
- self.is_list_of_numerics,
752
- feature,
753
- ["request", "inputs", f"[{i}]"],
754
- ):
755
- return None
756
-
757
751
  if not isinstance(prediction, list):
758
752
  prediction = [prediction]
759
753
 
754
+ if not isinstance(feature, list):
755
+ feature = [feature]
756
+
760
757
  events.append(
761
758
  {
762
759
  EventFieldType.FUNCTION_URI: function_uri,
@@ -803,18 +800,6 @@ class ProcessEndpointEvent(mlrun.feature_store.steps.MapClass):
803
800
  f"{self.last_request[endpoint_id]} - write to TSDB will be rejected"
804
801
  )
805
802
 
806
- @staticmethod
807
- def is_list_of_numerics(
808
- field: typing.List[typing.Union[int, float, dict, list]],
809
- dict_path: typing.List[str],
810
- ):
811
- if all(isinstance(x, int) or isinstance(x, float) for x in field):
812
- return True
813
- logger.error(
814
- f"List does not consist of only numeric values: {field} [Event -> {','.join(dict_path)}]"
815
- )
816
- return False
817
-
818
803
  def resume_state(self, endpoint_id):
819
804
  # Make sure process is resumable, if process fails for any reason, be able to pick things up close to where we
820
805
  # left them
@@ -849,7 +834,7 @@ class ProcessEndpointEvent(mlrun.feature_store.steps.MapClass):
849
834
  endpoint_id: str,
850
835
  validation_function,
851
836
  field: typing.Any,
852
- dict_path: typing.List[str],
837
+ dict_path: list[str],
853
838
  ):
854
839
  if validation_function(field, dict_path):
855
840
  return True
@@ -857,7 +842,7 @@ class ProcessEndpointEvent(mlrun.feature_store.steps.MapClass):
857
842
  return False
858
843
 
859
844
 
860
- def is_not_none(field: typing.Any, dict_path: typing.List[str]):
845
+ def is_not_none(field: typing.Any, dict_path: list[str]):
861
846
  if field is not None:
862
847
  return True
863
848
  logger.error(
@@ -946,9 +931,11 @@ class MapFeatureNames(mlrun.feature_store.steps.MapClass):
946
931
  return self.label_columns[endpoint_id]
947
932
  return None
948
933
 
949
- def do(self, event: typing.Dict):
934
+ def do(self, event: dict):
950
935
  endpoint_id = event[EventFieldType.ENDPOINT_ID]
951
936
 
937
+ feature_values = event[EventFieldType.FEATURES]
938
+ label_values = event[EventFieldType.PREDICTION]
952
939
  # Get feature names and label columns
953
940
  if endpoint_id not in self.feature_names:
954
941
  endpoint_record = get_endpoint_record(
@@ -984,6 +971,12 @@ class MapFeatureNames(mlrun.feature_store.steps.MapClass):
984
971
  },
985
972
  )
986
973
 
974
+ update_monitoring_feature_set(
975
+ endpoint_record=endpoint_record,
976
+ feature_names=feature_names,
977
+ feature_values=feature_values,
978
+ )
979
+
987
980
  # Similar process with label columns
988
981
  if not label_columns and self._infer_columns_from_data:
989
982
  label_columns = self._infer_label_columns_from_data(event)
@@ -1002,6 +995,11 @@ class MapFeatureNames(mlrun.feature_store.steps.MapClass):
1002
995
  endpoint_id=endpoint_id,
1003
996
  attributes={EventFieldType.LABEL_NAMES: json.dumps(label_columns)},
1004
997
  )
998
+ update_monitoring_feature_set(
999
+ endpoint_record=endpoint_record,
1000
+ feature_names=label_columns,
1001
+ feature_values=label_values,
1002
+ )
1005
1003
 
1006
1004
  self.label_columns[endpoint_id] = label_columns
1007
1005
  self.feature_names[endpoint_id] = feature_names
@@ -1019,7 +1017,6 @@ class MapFeatureNames(mlrun.feature_store.steps.MapClass):
1019
1017
 
1020
1018
  # Add feature_name:value pairs along with a mapping dictionary of all of these pairs
1021
1019
  feature_names = self.feature_names[endpoint_id]
1022
- feature_values = event[EventFieldType.FEATURES]
1023
1020
  self._map_dictionary_values(
1024
1021
  event=event,
1025
1022
  named_iters=feature_names,
@@ -1029,7 +1026,6 @@ class MapFeatureNames(mlrun.feature_store.steps.MapClass):
1029
1026
 
1030
1027
  # Add label_name:value pairs along with a mapping dictionary of all of these pairs
1031
1028
  label_names = self.label_columns[endpoint_id]
1032
- label_values = event[EventFieldType.PREDICTION]
1033
1029
  self._map_dictionary_values(
1034
1030
  event=event,
1035
1031
  named_iters=label_names,
@@ -1045,9 +1041,9 @@ class MapFeatureNames(mlrun.feature_store.steps.MapClass):
1045
1041
 
1046
1042
  @staticmethod
1047
1043
  def _map_dictionary_values(
1048
- event: typing.Dict,
1049
- named_iters: typing.List,
1050
- values_iters: typing.List,
1044
+ event: dict,
1045
+ named_iters: list,
1046
+ values_iters: list,
1051
1047
  mapping_dictionary: str,
1052
1048
  ):
1053
1049
  """Adding name-value pairs to event dictionary based on two provided lists of names and values. These pairs
@@ -1082,7 +1078,7 @@ class UpdateEndpoint(mlrun.feature_store.steps.MapClass):
1082
1078
  self.project = project
1083
1079
  self.model_endpoint_store_target = model_endpoint_store_target
1084
1080
 
1085
- def do(self, event: typing.Dict):
1081
+ def do(self, event: dict):
1086
1082
  update_endpoint_record(
1087
1083
  project=self.project,
1088
1084
  endpoint_id=event.pop(EventFieldType.ENDPOINT_ID),
@@ -1117,7 +1113,7 @@ class InferSchema(mlrun.feature_store.steps.MapClass):
1117
1113
  self.table = table
1118
1114
  self.keys = set()
1119
1115
 
1120
- def do(self, event: typing.Dict):
1116
+ def do(self, event: dict):
1121
1117
  key_set = set(event.keys())
1122
1118
  if not key_set.issubset(self.keys):
1123
1119
  self.keys.update(key_set)
@@ -1241,3 +1237,21 @@ def get_endpoint_record(project: str, endpoint_id: str):
1241
1237
  project=project,
1242
1238
  )
1243
1239
  return model_endpoint_store.get_model_endpoint(endpoint_id=endpoint_id)
1240
+
1241
+
1242
+ def update_monitoring_feature_set(
1243
+ endpoint_record: dict[str, typing.Any],
1244
+ feature_names: list[str],
1245
+ feature_values: list[typing.Any],
1246
+ ):
1247
+ monitoring_feature_set = fstore.get_feature_set(
1248
+ endpoint_record[
1249
+ mlrun.common.schemas.model_monitoring.EventFieldType.FEATURE_SET_URI
1250
+ ]
1251
+ )
1252
+ for name, val in zip(feature_names, feature_values):
1253
+ monitoring_feature_set.add_feature(
1254
+ fstore.Feature(name=name, value_type=type(val))
1255
+ )
1256
+
1257
+ monitoring_feature_set.save()
@@ -838,9 +838,9 @@ class PandasDataFramePackager(DefaultPackager):
838
838
  """
839
839
  if isinstance(obj, dict):
840
840
  for key, value in obj.items():
841
- obj[
842
- PandasDataFramePackager._prepare_result(obj=key)
843
- ] = PandasDataFramePackager._prepare_result(obj=value)
841
+ obj[PandasDataFramePackager._prepare_result(obj=key)] = (
842
+ PandasDataFramePackager._prepare_result(obj=value)
843
+ )
844
844
  elif isinstance(obj, list):
845
845
  for i, value in enumerate(obj):
846
846
  obj[i] = PandasDataFramePackager._prepare_result(obj=value)
@@ -179,7 +179,9 @@ class _TarArchiver(_Archiver):
179
179
 
180
180
  # Extract:
181
181
  with tarfile.open(archive_path, f"r:{cls._MODE_STRING}") as tar_file:
182
- tar_file.extractall(directory_path)
182
+ # use 'data' to ensure no security risks are imposed by the archive files
183
+ # see: https://docs.python.org/3/library/tarfile.html#tarfile.TarFile.extractall
184
+ tar_file.extractall(directory_path, filter="data")
183
185
 
184
186
  return str(directory_path)
185
187
 
@@ -16,19 +16,15 @@ import json
16
16
  import os
17
17
  import urllib
18
18
  from collections import namedtuple
19
- from datetime import datetime
20
- from http import HTTPStatus
21
19
  from urllib.parse import urlparse
22
20
 
23
21
  import kfp.dsl
24
22
  import requests
25
23
  import semver
26
- import urllib3
27
24
  import v3io
28
25
 
29
26
  import mlrun.errors
30
27
  from mlrun.config import config as mlconf
31
- from mlrun.errors import err_to_str
32
28
  from mlrun.utils import dict_to_json
33
29
 
34
30
  _cached_control_session = None
@@ -488,25 +484,6 @@ class V3ioStreamClient:
488
484
  return response.output.records
489
485
 
490
486
 
491
- def create_control_session(url, username, password):
492
- # for systems without production cert - silence no cert verification WARN
493
- urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
494
- if not username or not password:
495
- raise ValueError("cannot create session key, missing username or password")
496
-
497
- session = requests.Session()
498
- session.auth = (username, password)
499
- try:
500
- auth = session.post(f"{url}/api/sessions", verify=False)
501
- except OSError as exc:
502
- raise OSError(f"error: cannot connect to {url}: {err_to_str(exc)}")
503
-
504
- if not auth.ok:
505
- raise OSError(f"failed to create session: {url}, {auth.text}")
506
-
507
- return auth.json()["data"]["id"]
508
-
509
-
510
487
  def is_iguazio_endpoint(endpoint_url: str) -> bool:
511
488
  # TODO: find a better heuristic
512
489
  return ".default-tenant." in endpoint_url
@@ -533,21 +510,6 @@ def is_iguazio_session_cookie(session_cookie: str) -> bool:
533
510
  return False
534
511
 
535
512
 
536
- def is_iguazio_system_2_10_or_above(dashboard_url):
537
- # for systems without production cert - silence no cert verification WARN
538
- urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
539
- response = requests.get(f"{dashboard_url}/api/external_versions", verify=False)
540
-
541
- if not response.ok:
542
- if response.status_code == HTTPStatus.NOT_FOUND.value:
543
- # in iguazio systems prior to 2.10 this endpoint didn't exist, so the api returns 404 cause endpoint not
544
- # found
545
- return False
546
- response.raise_for_status()
547
-
548
- return True
549
-
550
-
551
513
  # we assign the control session or access key to the password since this is iguazio auth scheme
552
514
  # (requests should be sent with username:control_session/access_key as auth header)
553
515
  def add_or_refresh_credentials(
@@ -577,33 +539,12 @@ def add_or_refresh_credentials(
577
539
  # (ideally if we could identify we're in enterprise we would have verify here that token and username have value)
578
540
  if not is_iguazio_endpoint(api_url):
579
541
  return "", "", token
580
- iguazio_dashboard_url = "https://dashboard" + api_url[api_url.find(".") :]
581
-
582
- # in 2.8 mlrun api is protected with control session, from 2.10 it's protected with access key
583
- is_access_key_auth = is_iguazio_system_2_10_or_above(iguazio_dashboard_url)
584
- if is_access_key_auth:
585
- if not username or not token:
586
- raise ValueError(
587
- "username and access key required to authenticate against iguazio system"
588
- )
589
- return username, token, ""
590
-
591
- if not username or not password:
592
- raise ValueError("username and password needed to create session")
593
-
594
- global _cached_control_session
595
- now = datetime.now()
596
- if _cached_control_session:
597
- if (
598
- _cached_control_session[2] == username
599
- and _cached_control_session[3] == password
600
- and (now - _cached_control_session[1]).seconds < 20 * 60 * 60
601
- ):
602
- return _cached_control_session[2], _cached_control_session[0], ""
603
-
604
- control_session = create_control_session(iguazio_dashboard_url, username, password)
605
- _cached_control_session = (control_session, now, username, password)
606
- return username, control_session, ""
542
+
543
+ if not username or not token:
544
+ raise ValueError(
545
+ "username and access key required to authenticate against iguazio system"
546
+ )
547
+ return username, token, ""
607
548
 
608
549
 
609
550
  def parse_path(url, suffix="/"):
@@ -69,16 +69,16 @@ class WorkflowSpec(mlrun.model.ModelObj):
69
69
 
70
70
  def __init__(
71
71
  self,
72
- engine=None,
73
- code=None,
74
- path=None,
75
- args=None,
76
- name=None,
77
- handler=None,
78
- args_schema: dict = None,
72
+ engine: typing.Optional[str] = None,
73
+ code: typing.Optional[str] = None,
74
+ path: typing.Optional[str] = None,
75
+ args: typing.Optional[dict] = None,
76
+ name: typing.Optional[str] = None,
77
+ handler: typing.Optional[str] = None,
78
+ args_schema: typing.Optional[dict] = None,
79
79
  schedule: typing.Union[str, mlrun.common.schemas.ScheduleCronTrigger] = None,
80
- cleanup_ttl: int = None,
81
- image: str = None,
80
+ cleanup_ttl: typing.Optional[int] = None,
81
+ image: typing.Optional[str] = None,
82
82
  ):
83
83
  self.engine = engine
84
84
  self.code = code
@@ -401,6 +401,9 @@ def enrich_function_object(
401
401
  else:
402
402
  f.spec.build.source = project.spec.source
403
403
  f.spec.build.load_source_on_run = project.spec.load_source_on_run
404
+ f.spec.build.source_code_target_dir = (
405
+ project.spec.build.source_code_target_dir
406
+ )
404
407
  f.spec.workdir = project.spec.workdir or project.spec.subpath
405
408
  f.prepare_image_for_deploy()
406
409
 
@@ -605,6 +608,7 @@ class _KFPRunner(_PipelineRunner):
605
608
  namespace=namespace,
606
609
  artifact_path=artifact_path,
607
610
  cleanup_ttl=workflow_spec.cleanup_ttl,
611
+ timeout=int(mlrun.mlconf.workflows.timeouts.kfp),
608
612
  )
609
613
 
610
614
  # The user provided workflow code might have made changes to function specs that require cleanup
@@ -862,10 +866,21 @@ class _RemoteRunner(_PipelineRunner):
862
866
  )
863
867
  return
864
868
 
869
+ get_workflow_id_timeout = max(
870
+ int(mlrun.mlconf.workflows.timeouts.remote),
871
+ int(getattr(mlrun.mlconf.workflows.timeouts, inner_engine.engine)),
872
+ )
873
+
874
+ logger.debug(
875
+ "Workflow submitted, waiting for pipeline run to start",
876
+ workflow_name=workflow_response.name,
877
+ get_workflow_id_timeout=get_workflow_id_timeout,
878
+ )
879
+
865
880
  # Getting workflow id from run:
866
881
  response = retry_until_successful(
867
882
  1,
868
- getattr(mlrun.mlconf.workflows.timeouts, inner_engine.engine),
883
+ get_workflow_id_timeout,
869
884
  logger,
870
885
  False,
871
886
  run_db.get_workflow_id,
@@ -988,6 +1003,7 @@ def load_and_run(
988
1003
  cleanup_ttl: int = None,
989
1004
  load_only: bool = False,
990
1005
  wait_for_completion: bool = False,
1006
+ project_context: str = None,
991
1007
  ):
992
1008
  """
993
1009
  Auxiliary function that the RemoteRunner run once or run every schedule.
@@ -1018,10 +1034,11 @@ def load_and_run(
1018
1034
  workflow and all its resources are deleted)
1019
1035
  :param load_only: for just loading the project, inner use.
1020
1036
  :param wait_for_completion: wait for workflow completion before returning
1037
+ :param project_context: project context path (used for loading the project)
1021
1038
  """
1022
1039
  try:
1023
1040
  project = mlrun.load_project(
1024
- context=f"./{project_name}",
1041
+ context=project_context or f"./{project_name}",
1025
1042
  url=url,
1026
1043
  name=project_name,
1027
1044
  init_git=init_git,
@@ -1053,7 +1070,7 @@ def load_and_run(
1053
1070
 
1054
1071
  raise error
1055
1072
 
1056
- context.logger.info(f"Loaded project {project.name} from remote successfully")
1073
+ context.logger.info(f"Loaded project {project.name} successfully")
1057
1074
 
1058
1075
  if load_only:
1059
1076
  return