mlrun 1.5.0rc1__py3-none-any.whl → 1.5.0rc2__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 (119) hide show
  1. mlrun/__init__.py +2 -35
  2. mlrun/__main__.py +1 -40
  3. mlrun/api/api/api.py +6 -0
  4. mlrun/api/api/endpoints/feature_store.py +0 -4
  5. mlrun/api/api/endpoints/files.py +14 -2
  6. mlrun/api/api/endpoints/functions.py +6 -1
  7. mlrun/api/api/endpoints/logs.py +17 -3
  8. mlrun/api/api/endpoints/pipelines.py +1 -5
  9. mlrun/api/api/endpoints/projects.py +88 -0
  10. mlrun/api/api/endpoints/runs.py +48 -6
  11. mlrun/api/api/endpoints/workflows.py +355 -0
  12. mlrun/api/api/utils.py +1 -1
  13. mlrun/api/crud/__init__.py +1 -0
  14. mlrun/api/crud/client_spec.py +3 -0
  15. mlrun/api/crud/model_monitoring/deployment.py +36 -7
  16. mlrun/api/crud/model_monitoring/grafana.py +1 -1
  17. mlrun/api/crud/model_monitoring/helpers.py +32 -2
  18. mlrun/api/crud/model_monitoring/model_endpoints.py +27 -5
  19. mlrun/api/crud/notifications.py +9 -4
  20. mlrun/api/crud/pipelines.py +4 -9
  21. mlrun/api/crud/runtime_resources.py +4 -3
  22. mlrun/api/crud/secrets.py +21 -0
  23. mlrun/api/crud/workflows.py +352 -0
  24. mlrun/api/db/base.py +16 -1
  25. mlrun/api/db/sqldb/db.py +97 -16
  26. mlrun/api/launcher.py +26 -7
  27. mlrun/api/main.py +3 -4
  28. mlrun/{mlutils → api/rundb}/__init__.py +2 -6
  29. mlrun/{db → api/rundb}/sqldb.py +35 -83
  30. mlrun/api/runtime_handlers/__init__.py +56 -0
  31. mlrun/api/runtime_handlers/base.py +1247 -0
  32. mlrun/api/runtime_handlers/daskjob.py +209 -0
  33. mlrun/api/runtime_handlers/kubejob.py +37 -0
  34. mlrun/api/runtime_handlers/mpijob.py +147 -0
  35. mlrun/api/runtime_handlers/remotesparkjob.py +29 -0
  36. mlrun/api/runtime_handlers/sparkjob.py +148 -0
  37. mlrun/api/utils/builder.py +1 -4
  38. mlrun/api/utils/clients/chief.py +14 -0
  39. mlrun/api/utils/scheduler.py +98 -15
  40. mlrun/api/utils/singletons/db.py +4 -0
  41. mlrun/artifacts/manager.py +1 -2
  42. mlrun/common/schemas/__init__.py +6 -0
  43. mlrun/common/schemas/auth.py +4 -1
  44. mlrun/common/schemas/client_spec.py +1 -1
  45. mlrun/common/schemas/model_monitoring/__init__.py +1 -0
  46. mlrun/common/schemas/model_monitoring/constants.py +11 -0
  47. mlrun/common/schemas/project.py +1 -0
  48. mlrun/common/schemas/runs.py +1 -8
  49. mlrun/common/schemas/schedule.py +1 -8
  50. mlrun/common/schemas/workflow.py +54 -0
  51. mlrun/config.py +42 -40
  52. mlrun/datastore/sources.py +1 -1
  53. mlrun/db/__init__.py +4 -68
  54. mlrun/db/base.py +12 -0
  55. mlrun/db/factory.py +65 -0
  56. mlrun/db/httpdb.py +175 -19
  57. mlrun/db/nopdb.py +4 -2
  58. mlrun/execution.py +4 -2
  59. mlrun/feature_store/__init__.py +1 -0
  60. mlrun/feature_store/api.py +1 -2
  61. mlrun/feature_store/feature_set.py +0 -10
  62. mlrun/feature_store/feature_vector.py +340 -2
  63. mlrun/feature_store/ingestion.py +5 -10
  64. mlrun/feature_store/retrieval/base.py +118 -104
  65. mlrun/feature_store/retrieval/dask_merger.py +17 -10
  66. mlrun/feature_store/retrieval/job.py +4 -1
  67. mlrun/feature_store/retrieval/local_merger.py +18 -18
  68. mlrun/feature_store/retrieval/spark_merger.py +21 -14
  69. mlrun/feature_store/retrieval/storey_merger.py +21 -15
  70. mlrun/kfpops.py +3 -9
  71. mlrun/launcher/base.py +3 -3
  72. mlrun/launcher/client.py +3 -2
  73. mlrun/launcher/factory.py +16 -13
  74. mlrun/lists.py +0 -11
  75. mlrun/model.py +9 -15
  76. mlrun/model_monitoring/helpers.py +15 -25
  77. mlrun/model_monitoring/model_monitoring_batch.py +72 -4
  78. mlrun/model_monitoring/prometheus.py +219 -0
  79. mlrun/model_monitoring/stores/__init__.py +15 -9
  80. mlrun/model_monitoring/stores/sql_model_endpoint_store.py +3 -1
  81. mlrun/model_monitoring/stream_processing.py +181 -29
  82. mlrun/package/packager.py +6 -8
  83. mlrun/package/packagers/default_packager.py +121 -10
  84. mlrun/platforms/__init__.py +0 -2
  85. mlrun/platforms/iguazio.py +0 -56
  86. mlrun/projects/pipelines.py +57 -158
  87. mlrun/projects/project.py +6 -32
  88. mlrun/render.py +1 -1
  89. mlrun/run.py +2 -124
  90. mlrun/runtimes/__init__.py +6 -42
  91. mlrun/runtimes/base.py +26 -1241
  92. mlrun/runtimes/daskjob.py +2 -198
  93. mlrun/runtimes/function.py +16 -5
  94. mlrun/runtimes/kubejob.py +5 -29
  95. mlrun/runtimes/mpijob/__init__.py +2 -2
  96. mlrun/runtimes/mpijob/abstract.py +10 -1
  97. mlrun/runtimes/mpijob/v1.py +0 -76
  98. mlrun/runtimes/mpijob/v1alpha1.py +1 -74
  99. mlrun/runtimes/nuclio.py +3 -2
  100. mlrun/runtimes/pod.py +0 -10
  101. mlrun/runtimes/remotesparkjob.py +1 -15
  102. mlrun/runtimes/serving.py +1 -1
  103. mlrun/runtimes/sparkjob/__init__.py +0 -1
  104. mlrun/runtimes/sparkjob/abstract.py +4 -131
  105. mlrun/serving/states.py +1 -1
  106. mlrun/utils/db.py +0 -2
  107. mlrun/utils/helpers.py +19 -13
  108. mlrun/utils/notifications/notification_pusher.py +5 -25
  109. mlrun/utils/regex.py +7 -2
  110. mlrun/utils/version/version.json +2 -2
  111. {mlrun-1.5.0rc1.dist-info → mlrun-1.5.0rc2.dist-info}/METADATA +24 -23
  112. {mlrun-1.5.0rc1.dist-info → mlrun-1.5.0rc2.dist-info}/RECORD +116 -107
  113. {mlrun-1.5.0rc1.dist-info → mlrun-1.5.0rc2.dist-info}/WHEEL +1 -1
  114. mlrun/mlutils/data.py +0 -160
  115. mlrun/mlutils/models.py +0 -78
  116. mlrun/mlutils/plots.py +0 -902
  117. {mlrun-1.5.0rc1.dist-info → mlrun-1.5.0rc2.dist-info}/LICENSE +0 -0
  118. {mlrun-1.5.0rc1.dist-info → mlrun-1.5.0rc2.dist-info}/entry_points.txt +0 -0
  119. {mlrun-1.5.0rc1.dist-info → mlrun-1.5.0rc2.dist-info}/top_level.txt +0 -0
@@ -95,7 +95,7 @@ class Pipelines(
95
95
  runs = [run.to_dict() for run in response.runs or []]
96
96
  total_size = response.total_size
97
97
  next_page_token = response.next_page_token
98
- runs = self._format_runs(db_session, runs, format_)
98
+ runs = self._format_runs(runs, format_)
99
99
 
100
100
  return total_size, next_page_token, runs
101
101
 
@@ -131,7 +131,6 @@ class Pipelines(
131
131
 
132
132
  def get_pipeline(
133
133
  self,
134
- db_session: sqlalchemy.orm.Session,
135
134
  run_id: str,
136
135
  project: typing.Optional[str] = None,
137
136
  namespace: typing.Optional[str] = None,
@@ -149,9 +148,7 @@ class Pipelines(
149
148
  raise mlrun.errors.MLRunNotFoundError(
150
149
  f"Pipeline run with id {run_id} is not of project {project}"
151
150
  )
152
- run = self._format_run(
153
- db_session, run, format_, api_run_detail.to_dict()
154
- )
151
+ run = self._format_run(run, format_, api_run_detail.to_dict())
155
152
  except kfp_server_api.ApiException as exc:
156
153
  mlrun.errors.raise_for_status_code(int(exc.status), err_to_str(exc))
157
154
  except mlrun.errors.MLRunHTTPStatusError:
@@ -228,18 +225,16 @@ class Pipelines(
228
225
 
229
226
  def _format_runs(
230
227
  self,
231
- db_session: sqlalchemy.orm.Session,
232
228
  runs: typing.List[dict],
233
229
  format_: mlrun.common.schemas.PipelinesFormat = mlrun.common.schemas.PipelinesFormat.metadata_only,
234
230
  ) -> typing.List[dict]:
235
231
  formatted_runs = []
236
232
  for run in runs:
237
- formatted_runs.append(self._format_run(db_session, run, format_))
233
+ formatted_runs.append(self._format_run(run, format_))
238
234
  return formatted_runs
239
235
 
240
236
  def _format_run(
241
237
  self,
242
- db_session: sqlalchemy.orm.Session,
243
238
  run: dict,
244
239
  format_: mlrun.common.schemas.PipelinesFormat = mlrun.common.schemas.PipelinesFormat.metadata_only,
245
240
  api_run_detail: typing.Optional[dict] = None,
@@ -258,7 +253,7 @@ class Pipelines(
258
253
  "The full kfp api_run_detail object is needed to generate the summary format"
259
254
  )
260
255
  return mlrun.kfpops.format_summary_from_kfp_run(
261
- api_run_detail, run["project"], db_session
256
+ api_run_detail, run["project"]
262
257
  )
263
258
  else:
264
259
  raise NotImplementedError(
@@ -18,6 +18,7 @@ import mergedeep
18
18
  import sqlalchemy.orm
19
19
 
20
20
  import mlrun.api.api.utils
21
+ import mlrun.api.runtime_handlers
21
22
  import mlrun.api.utils.projects.remotes.follower
22
23
  import mlrun.api.utils.singletons.db
23
24
  import mlrun.common.schemas
@@ -50,7 +51,7 @@ class RuntimeResources(
50
51
  self.validate_runtime_resources_kind(kind)
51
52
  kinds = [kind]
52
53
  for kind in kinds:
53
- runtime_handler = mlrun.runtimes.get_runtime_handler(kind)
54
+ runtime_handler = mlrun.api.runtime_handlers.get_runtime_handler(kind)
54
55
  resources = runtime_handler.list_resources(
55
56
  project, object_id, label_selector, group_by
56
57
  )
@@ -88,7 +89,7 @@ class RuntimeResources(
88
89
  )
89
90
  runtimes_resources_output = [] if group_by is None else {}
90
91
  for kind, runtime_resources_list in runtime_resources_by_kind.items():
91
- runtime_handler = mlrun.runtimes.get_runtime_handler(kind)
92
+ runtime_handler = mlrun.api.runtime_handlers.get_runtime_handler(kind)
92
93
  resources = runtime_handler.build_output_from_runtime_resources(
93
94
  runtime_resources_list, group_by
94
95
  )
@@ -122,7 +123,7 @@ class RuntimeResources(
122
123
  self.validate_runtime_resources_kind(kind)
123
124
  kinds = [kind]
124
125
  for kind in kinds:
125
- runtime_handler = mlrun.runtimes.get_runtime_handler(kind)
126
+ runtime_handler = mlrun.api.runtime_handlers.get_runtime_handler(kind)
126
127
  if object_id:
127
128
  runtime_handler.delete_runtime_object_resources(
128
129
  mlrun.api.utils.singletons.db.get_db(),
mlrun/api/crud/secrets.py CHANGED
@@ -17,9 +17,11 @@ import json
17
17
  import typing
18
18
  import uuid
19
19
 
20
+ import mlrun.api
20
21
  import mlrun.api.utils.clients.iguazio
21
22
  import mlrun.api.utils.events.events_factory as events_factory
22
23
  import mlrun.api.utils.singletons.k8s
24
+ import mlrun.common
23
25
  import mlrun.common.schemas
24
26
  import mlrun.errors
25
27
  import mlrun.utils.helpers
@@ -531,3 +533,22 @@ class Secrets(
531
533
  @staticmethod
532
534
  def _generate_uuid() -> str:
533
535
  return str(uuid.uuid4())
536
+
537
+
538
+ def get_project_secret_provider(project: str) -> typing.Callable:
539
+ """Implement secret provider for handle the related project secret on the API side.
540
+
541
+ :param project: Project name.
542
+
543
+ :return: A secret provider function.
544
+ """
545
+
546
+ def secret_provider(key: str):
547
+ return mlrun.api.crud.secrets.Secrets().get_project_secret(
548
+ project=project,
549
+ provider=mlrun.common.schemas.secret.SecretProviderName.kubernetes,
550
+ allow_secrets_from_k8s=True,
551
+ secret_key=key,
552
+ )
553
+
554
+ return secret_provider
@@ -0,0 +1,352 @@
1
+ # Copyright 2018 Iguazio
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ #
15
+ import uuid
16
+ from typing import Dict
17
+
18
+ from sqlalchemy.orm import Session
19
+
20
+ import mlrun.common.schemas
21
+ import mlrun.utils.singleton
22
+ from mlrun.api.api.utils import (
23
+ apply_enrichment_and_validation_on_function,
24
+ get_run_db_instance,
25
+ get_scheduler,
26
+ )
27
+ from mlrun.config import config
28
+ from mlrun.model import Credentials, RunMetadata, RunObject, RunSpec
29
+
30
+
31
+ class WorkflowRunners(
32
+ metaclass=mlrun.utils.singleton.Singleton,
33
+ ):
34
+ @staticmethod
35
+ def create_runner(
36
+ run_name: str,
37
+ project: str,
38
+ db_session: Session,
39
+ auth_info: mlrun.common.schemas.AuthInfo,
40
+ image: str,
41
+ ) -> mlrun.run.KubejobRuntime:
42
+ """
43
+ Creating the base object for the workflow runner function with
44
+ all the necessary metadata to create it on server-side.
45
+
46
+ :param run_name: workflow-runner function name
47
+ :param project: project name
48
+ :param db_session: session that manages the current dialog with the database
49
+ :param auth_info: auth info of the request
50
+ :param image: image for the workflow runner job
51
+
52
+ :returns: workflow runner object
53
+ """
54
+ runner = mlrun.new_function(
55
+ name=run_name,
56
+ project=project,
57
+ kind=mlrun.runtimes.RuntimeKinds.job,
58
+ # For preventing deployment:
59
+ image=image,
60
+ )
61
+
62
+ runner.set_db_connection(get_run_db_instance(db_session))
63
+
64
+ # Enrichment and validation requires access key
65
+ runner.metadata.credentials.access_key = Credentials.generate_access_key
66
+
67
+ apply_enrichment_and_validation_on_function(
68
+ function=runner,
69
+ auth_info=auth_info,
70
+ )
71
+
72
+ runner.save()
73
+ return runner
74
+
75
+ def schedule(
76
+ self,
77
+ runner: mlrun.run.KubejobRuntime,
78
+ project: mlrun.common.schemas.Project,
79
+ workflow_request: mlrun.common.schemas.WorkflowRequest,
80
+ db_session: Session = None,
81
+ auth_info: mlrun.common.schemas.AuthInfo = None,
82
+ ):
83
+ """
84
+ Schedule workflow runner.
85
+
86
+ :param runner: workflow runner function object
87
+ :param project: MLRun project
88
+ :param workflow_request: contains the workflow spec, that will be scheduled
89
+ :param db_session: session that manages the current dialog with the database
90
+ :param auth_info: auth info of the request
91
+ """
92
+ labels = {
93
+ "job-type": "workflow-runner",
94
+ "workflow": workflow_request.spec.name,
95
+ }
96
+
97
+ run_spec = self._prepare_run_object_for_scheduling(
98
+ project=project,
99
+ workflow_request=workflow_request,
100
+ labels=labels,
101
+ )
102
+ # this includes filling the spec.function which is required for submit run
103
+ runner._store_function(
104
+ runspec=run_spec, meta=run_spec.metadata, db=runner._get_db()
105
+ )
106
+ workflow_spec = workflow_request.spec
107
+ schedule = workflow_spec.schedule
108
+ scheduled_object = {
109
+ "task": run_spec.to_dict(),
110
+ "schedule": schedule,
111
+ }
112
+
113
+ get_scheduler().store_schedule(
114
+ db_session=db_session,
115
+ auth_info=auth_info,
116
+ project=project.metadata.name,
117
+ name=workflow_spec.name,
118
+ scheduled_object=scheduled_object,
119
+ cron_trigger=schedule,
120
+ labels=runner.metadata.labels,
121
+ kind=mlrun.common.schemas.ScheduleKinds.job,
122
+ )
123
+
124
+ def _prepare_run_object_for_scheduling(
125
+ self,
126
+ project: mlrun.common.schemas.Project,
127
+ workflow_request: mlrun.common.schemas.WorkflowRequest,
128
+ labels: Dict[str, str],
129
+ ) -> mlrun.run.RunObject:
130
+ """
131
+ Preparing all the necessary metadata and specifications for scheduling workflow from server-side.
132
+
133
+ :param project: MLRun project
134
+ :param workflow_request: contains the workflow spec and extra data for the run object
135
+ :param labels: dict of the task labels
136
+
137
+ :returns: RunObject ready for schedule.
138
+ """
139
+ meta_uid = uuid.uuid4().hex
140
+
141
+ save = self._set_source(project, workflow_request.source)
142
+ workflow_spec = workflow_request.spec
143
+ run_object = RunObject(
144
+ spec=RunSpec(
145
+ parameters=dict(
146
+ url=project.spec.source,
147
+ project_name=project.metadata.name,
148
+ workflow_name=workflow_spec.name,
149
+ workflow_path=workflow_spec.path,
150
+ workflow_arguments=workflow_spec.args,
151
+ artifact_path=workflow_request.artifact_path,
152
+ workflow_handler=workflow_spec.handler,
153
+ namespace=workflow_request.namespace,
154
+ ttl=workflow_spec.ttl,
155
+ engine=workflow_spec.engine,
156
+ local=workflow_spec.run_local,
157
+ save=save,
158
+ subpath=project.spec.subpath,
159
+ ),
160
+ handler="mlrun.projects.load_and_run",
161
+ scrape_metrics=config.scrape_metrics,
162
+ output_path=(
163
+ workflow_request.artifact_path or config.artifact_path
164
+ ).replace("{{run.uid}}", meta_uid),
165
+ ),
166
+ metadata=RunMetadata(
167
+ uid=meta_uid, name=workflow_spec.name, project=project.metadata.name
168
+ ),
169
+ )
170
+
171
+ # Setting labels:
172
+ return self._label_run_object(run_object, labels)
173
+
174
+ def run(
175
+ self,
176
+ runner: mlrun.run.KubejobRuntime,
177
+ project: mlrun.common.schemas.Project,
178
+ workflow_request: mlrun.common.schemas.WorkflowRequest = None,
179
+ load_only: bool = False,
180
+ ) -> RunObject:
181
+ """
182
+ Run workflow runner.
183
+
184
+ :param runner: workflow runner function object
185
+ :param project: MLRun project
186
+ :param workflow_request: contains the workflow spec, that will be executed
187
+ :param load_only: If True, will only load the project remotely (without running workflow)
188
+
189
+ :returns: run context object (RunObject) with run metadata, results and status
190
+ """
191
+ labels = {"project": project.metadata.name}
192
+ if load_only:
193
+ labels["job-type"] = "project-loader"
194
+ else:
195
+ labels["job-type"] = "workflow-runner"
196
+ labels["workflow"] = runner.metadata.name
197
+
198
+ run_spec = self._prepare_run_object_for_single_run(
199
+ project=project,
200
+ labels=labels,
201
+ workflow_request=workflow_request,
202
+ run_name=runner.metadata.name,
203
+ load_only=load_only,
204
+ )
205
+
206
+ artifact_path = workflow_request.artifact_path if workflow_request else ""
207
+ return runner.run(
208
+ runspec=run_spec,
209
+ artifact_path=artifact_path,
210
+ local=False,
211
+ watch=False,
212
+ )
213
+
214
+ @staticmethod
215
+ def get_workflow_id(
216
+ uid: str, project: str, engine: str, db_session: Session
217
+ ) -> mlrun.common.schemas.GetWorkflowResponse:
218
+ """
219
+ Retrieving the actual workflow id form the workflow runner
220
+
221
+ :param uid: the id of the workflow runner job
222
+ :param project: name of the project
223
+ :param engine: pipeline runner, for example: "kfp"
224
+ :param db_session: session that manages the current dialog with the database
225
+
226
+ :return: The id of the workflow.
227
+ """
228
+ # Reading run:
229
+ run = mlrun.api.crud.Runs().get_run(
230
+ db_session=db_session, uid=uid, iter=0, project=project
231
+ )
232
+ run_object = RunObject.from_dict(run)
233
+ state = run_object.status.state
234
+ workflow_id = None
235
+ if isinstance(run_object.status.results, dict):
236
+ workflow_id = run_object.status.results.get("workflow_id", None)
237
+
238
+ if workflow_id is None:
239
+ if (
240
+ engine == "local"
241
+ and state.casefold() == mlrun.run.RunStatuses.running.casefold()
242
+ ):
243
+ workflow_id = ""
244
+ else:
245
+ raise mlrun.errors.MLRunNotFoundError(
246
+ f"workflow id of run {project}:{uid} not found"
247
+ )
248
+
249
+ return mlrun.common.schemas.GetWorkflowResponse(workflow_id=workflow_id)
250
+
251
+ def _prepare_run_object_for_single_run(
252
+ self,
253
+ project: mlrun.common.schemas.Project,
254
+ labels: Dict[str, str],
255
+ workflow_request: mlrun.common.schemas.WorkflowRequest = None,
256
+ run_name: str = None,
257
+ load_only: bool = False,
258
+ ) -> mlrun.run.RunObject:
259
+ """
260
+ Preparing all the necessary metadata and specifications for running workflow from server-side.
261
+
262
+ :param project: MLRun project
263
+ :param labels: dict of the task labels
264
+ :param workflow_request: contains the workflow spec and extra data for the run object
265
+ :param run_name: workflow-runner function name
266
+ :param load_only: if True, will only load the project remotely (without running workflow)
267
+
268
+ :returns: RunObject ready for execution.
269
+ """
270
+ source = workflow_request.source if workflow_request else ""
271
+ save = self._set_source(project, source, load_only)
272
+ run_object = RunObject(
273
+ spec=RunSpec(
274
+ parameters=dict(
275
+ url=project.spec.source,
276
+ project_name=project.metadata.name,
277
+ load_only=load_only,
278
+ save=save,
279
+ ),
280
+ handler="mlrun.projects.load_and_run",
281
+ ),
282
+ metadata=RunMetadata(name=run_name),
283
+ )
284
+
285
+ if not load_only:
286
+ workflow_spec = workflow_request.spec
287
+ run_object.spec.parameters.update(
288
+ dict(
289
+ workflow_name=workflow_spec.name,
290
+ workflow_path=workflow_spec.path,
291
+ workflow_arguments=workflow_spec.args,
292
+ artifact_path=workflow_request.artifact_path,
293
+ workflow_handler=workflow_spec.handler,
294
+ namespace=workflow_request.namespace,
295
+ ttl=workflow_spec.ttl,
296
+ engine=workflow_spec.engine,
297
+ local=workflow_spec.run_local,
298
+ subpath=project.spec.subpath,
299
+ )
300
+ )
301
+
302
+ # Setting labels:
303
+ return self._label_run_object(run_object, labels)
304
+
305
+ @staticmethod
306
+ def _set_source(
307
+ project: mlrun.common.schemas.Project, source: str, load_only: bool = False
308
+ ) -> bool:
309
+ """
310
+ Setting the project source.
311
+ In case the user provided a source we want to load the project from the source
312
+ (like from a specific commit/branch from git repo) without changing the source of the project (save=False).
313
+
314
+ :param project: MLRun project
315
+ :param source: the source of the project, needs to be a remote URL that contains the project yaml file.
316
+ :param load_only: if we only load the project, it must be saved to ensure we are not running a pipeline
317
+ without a project as it's not supported.
318
+
319
+ :returns: True if the project need to be saved afterward.
320
+ """
321
+
322
+ save = True
323
+ if source and not load_only:
324
+ save = False
325
+ project.spec.source = source
326
+
327
+ if "://" not in project.spec.source:
328
+ raise mlrun.errors.MLRunInvalidArgumentError(
329
+ f"Remote workflows can only be performed by a project with remote source (e.g git:// or http://),"
330
+ f" but the specified source '{project.spec.source}' is not remote. "
331
+ f"Either put your code in Git, or archive it and then set a source to it."
332
+ f" For more details, read"
333
+ f" https://docs.mlrun.org/en/latest/concepts/scheduled-jobs.html#scheduling-a-workflow"
334
+ )
335
+ return save
336
+
337
+ @staticmethod
338
+ def _label_run_object(
339
+ run_object: mlrun.run.RunObject,
340
+ labels: Dict[str, str],
341
+ ) -> mlrun.run.RunObject:
342
+ """
343
+ Setting labels to the task
344
+
345
+ :param run_object: run object to set labels on
346
+ :param labels: dict of labels
347
+
348
+ :returns: labeled RunObject
349
+ """
350
+ for key, value in labels.items():
351
+ run_object = run_object.set_label(key, value)
352
+ return run_object
mlrun/api/db/base.py CHANGED
@@ -259,6 +259,21 @@ class DBInterface(ABC):
259
259
  ):
260
260
  pass
261
261
 
262
+ def store_schedule(
263
+ self,
264
+ session,
265
+ project: str,
266
+ name: str,
267
+ kind: mlrun.common.schemas.ScheduleKinds = None,
268
+ scheduled_object: Any = None,
269
+ cron_trigger: mlrun.common.schemas.ScheduleCronTrigger = None,
270
+ labels: Dict = None,
271
+ last_run_uri: str = None,
272
+ concurrency_limit: int = None,
273
+ next_run_time: datetime = None,
274
+ ):
275
+ pass
276
+
262
277
  @abstractmethod
263
278
  def list_schedules(
264
279
  self,
@@ -272,7 +287,7 @@ class DBInterface(ABC):
272
287
 
273
288
  @abstractmethod
274
289
  def get_schedule(
275
- self, session, project: str, name: str
290
+ self, session, project: str, name: str, raise_on_not_found: bool = True
276
291
  ) -> mlrun.common.schemas.ScheduleRecord:
277
292
  pass
278
293
 
mlrun/api/db/sqldb/db.py CHANGED
@@ -1174,6 +1174,63 @@ class SQLDB(DBInterface):
1174
1174
 
1175
1175
  return results
1176
1176
 
1177
+ def store_schedule(
1178
+ self,
1179
+ session: Session,
1180
+ project: str,
1181
+ name: str,
1182
+ kind: mlrun.common.schemas.ScheduleKinds = None,
1183
+ scheduled_object: Any = None,
1184
+ cron_trigger: mlrun.common.schemas.ScheduleCronTrigger = None,
1185
+ labels: Dict = None,
1186
+ last_run_uri: str = None,
1187
+ concurrency_limit: int = None,
1188
+ next_run_time: datetime = None,
1189
+ ) -> typing.Optional[mlrun.common.schemas.ScheduleRecord]:
1190
+ schedule = self.get_schedule(
1191
+ session=session, project=project, name=name, raise_on_not_found=False
1192
+ )
1193
+ schedule_exist = schedule is not None
1194
+
1195
+ if not schedule_exist:
1196
+ schedule = Schedule(
1197
+ project=project,
1198
+ name=name,
1199
+ kind=kind.value,
1200
+ creation_time=datetime.now(timezone.utc),
1201
+ concurrency_limit=concurrency_limit,
1202
+ next_run_time=next_run_time,
1203
+ scheduled_object=scheduled_object,
1204
+ cron_trigger=cron_trigger,
1205
+ )
1206
+ labels = labels or {}
1207
+
1208
+ self._update_schedule_body(
1209
+ schedule=schedule,
1210
+ scheduled_object=scheduled_object,
1211
+ cron_trigger=cron_trigger,
1212
+ labels=labels,
1213
+ last_run_uri=last_run_uri,
1214
+ concurrency_limit=concurrency_limit,
1215
+ next_run_time=next_run_time,
1216
+ )
1217
+
1218
+ logger.debug(
1219
+ "Storing schedule to db",
1220
+ project=project,
1221
+ name=name,
1222
+ kind=kind,
1223
+ cron_trigger=cron_trigger,
1224
+ labels=labels,
1225
+ concurrency_limit=concurrency_limit,
1226
+ scheduled_object=scheduled_object,
1227
+ )
1228
+
1229
+ self._upsert(session, [schedule])
1230
+
1231
+ if schedule_exist:
1232
+ return schedule
1233
+
1177
1234
  def create_schedule(
1178
1235
  self,
1179
1236
  session: Session,
@@ -1232,6 +1289,37 @@ class SQLDB(DBInterface):
1232
1289
  ):
1233
1290
  schedule = self._get_schedule_record(session, project, name)
1234
1291
 
1292
+ self._update_schedule_body(
1293
+ schedule=schedule,
1294
+ scheduled_object=scheduled_object,
1295
+ cron_trigger=cron_trigger,
1296
+ labels=labels,
1297
+ last_run_uri=last_run_uri,
1298
+ concurrency_limit=concurrency_limit,
1299
+ next_run_time=next_run_time,
1300
+ )
1301
+
1302
+ logger.debug(
1303
+ "Updating schedule in db",
1304
+ project=project,
1305
+ name=name,
1306
+ cron_trigger=cron_trigger,
1307
+ labels=labels,
1308
+ concurrency_limit=concurrency_limit,
1309
+ next_run_time=next_run_time,
1310
+ )
1311
+ self._upsert(session, [schedule])
1312
+
1313
+ @staticmethod
1314
+ def _update_schedule_body(
1315
+ schedule: mlrun.common.schemas.ScheduleRecord,
1316
+ scheduled_object: Any = None,
1317
+ cron_trigger: mlrun.common.schemas.ScheduleCronTrigger = None,
1318
+ labels: Dict = None,
1319
+ last_run_uri: str = None,
1320
+ concurrency_limit: int = None,
1321
+ next_run_time: datetime = None,
1322
+ ):
1235
1323
  # explicitly ensure the updated fields are not None, as they can be empty strings/dictionaries etc.
1236
1324
  if scheduled_object is not None:
1237
1325
  schedule.scheduled_object = scheduled_object
@@ -1253,17 +1341,6 @@ class SQLDB(DBInterface):
1253
1341
  # saved in the DB in UTC timezone, therefore we transform next_run_time to UTC as well.
1254
1342
  schedule.next_run_time = next_run_time.astimezone(pytz.utc)
1255
1343
 
1256
- logger.debug(
1257
- "Updating schedule in db",
1258
- project=project,
1259
- name=name,
1260
- cron_trigger=cron_trigger,
1261
- labels=labels,
1262
- concurrency_limit=concurrency_limit,
1263
- next_run_time=next_run_time,
1264
- )
1265
- self._upsert(session, [schedule])
1266
-
1267
1344
  def list_schedules(
1268
1345
  self,
1269
1346
  session: Session,
@@ -1286,19 +1363,23 @@ class SQLDB(DBInterface):
1286
1363
  return schedules
1287
1364
 
1288
1365
  def get_schedule(
1289
- self, session: Session, project: str, name: str
1290
- ) -> mlrun.common.schemas.ScheduleRecord:
1366
+ self, session: Session, project: str, name: str, raise_on_not_found: bool = True
1367
+ ) -> typing.Optional[mlrun.common.schemas.ScheduleRecord]:
1291
1368
  logger.debug("Getting schedule from db", project=project, name=name)
1292
- schedule_record = self._get_schedule_record(session, project, name)
1369
+ schedule_record = self._get_schedule_record(
1370
+ session, project, name, raise_on_not_found
1371
+ )
1372
+ if not schedule_record:
1373
+ return
1293
1374
  schedule = self._transform_schedule_record_to_scheme(schedule_record)
1294
1375
  return schedule
1295
1376
 
1296
1377
  def _get_schedule_record(
1297
- self, session: Session, project: str, name: str
1378
+ self, session: Session, project: str, name: str, raise_on_not_found: bool = True
1298
1379
  ) -> mlrun.common.schemas.ScheduleRecord:
1299
1380
  query = self._query(session, Schedule, project=project, name=name)
1300
1381
  schedule_record = query.one_or_none()
1301
- if not schedule_record:
1382
+ if not schedule_record and raise_on_not_found:
1302
1383
  raise mlrun.errors.MLRunNotFoundError(
1303
1384
  f"Schedule not found: project={project}, name={name}"
1304
1385
  )