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.
- mlrun/__init__.py +2 -35
- mlrun/__main__.py +1 -40
- mlrun/api/api/api.py +6 -0
- mlrun/api/api/endpoints/feature_store.py +0 -4
- mlrun/api/api/endpoints/files.py +14 -2
- mlrun/api/api/endpoints/functions.py +6 -1
- mlrun/api/api/endpoints/logs.py +17 -3
- mlrun/api/api/endpoints/pipelines.py +1 -5
- mlrun/api/api/endpoints/projects.py +88 -0
- mlrun/api/api/endpoints/runs.py +48 -6
- mlrun/api/api/endpoints/workflows.py +355 -0
- mlrun/api/api/utils.py +1 -1
- mlrun/api/crud/__init__.py +1 -0
- mlrun/api/crud/client_spec.py +3 -0
- mlrun/api/crud/model_monitoring/deployment.py +36 -7
- mlrun/api/crud/model_monitoring/grafana.py +1 -1
- mlrun/api/crud/model_monitoring/helpers.py +32 -2
- mlrun/api/crud/model_monitoring/model_endpoints.py +27 -5
- mlrun/api/crud/notifications.py +9 -4
- mlrun/api/crud/pipelines.py +4 -9
- mlrun/api/crud/runtime_resources.py +4 -3
- mlrun/api/crud/secrets.py +21 -0
- mlrun/api/crud/workflows.py +352 -0
- mlrun/api/db/base.py +16 -1
- mlrun/api/db/sqldb/db.py +97 -16
- mlrun/api/launcher.py +26 -7
- mlrun/api/main.py +3 -4
- mlrun/{mlutils → api/rundb}/__init__.py +2 -6
- mlrun/{db → api/rundb}/sqldb.py +35 -83
- mlrun/api/runtime_handlers/__init__.py +56 -0
- mlrun/api/runtime_handlers/base.py +1247 -0
- mlrun/api/runtime_handlers/daskjob.py +209 -0
- mlrun/api/runtime_handlers/kubejob.py +37 -0
- mlrun/api/runtime_handlers/mpijob.py +147 -0
- mlrun/api/runtime_handlers/remotesparkjob.py +29 -0
- mlrun/api/runtime_handlers/sparkjob.py +148 -0
- mlrun/api/utils/builder.py +1 -4
- mlrun/api/utils/clients/chief.py +14 -0
- mlrun/api/utils/scheduler.py +98 -15
- mlrun/api/utils/singletons/db.py +4 -0
- mlrun/artifacts/manager.py +1 -2
- mlrun/common/schemas/__init__.py +6 -0
- mlrun/common/schemas/auth.py +4 -1
- mlrun/common/schemas/client_spec.py +1 -1
- mlrun/common/schemas/model_monitoring/__init__.py +1 -0
- mlrun/common/schemas/model_monitoring/constants.py +11 -0
- mlrun/common/schemas/project.py +1 -0
- mlrun/common/schemas/runs.py +1 -8
- mlrun/common/schemas/schedule.py +1 -8
- mlrun/common/schemas/workflow.py +54 -0
- mlrun/config.py +42 -40
- mlrun/datastore/sources.py +1 -1
- mlrun/db/__init__.py +4 -68
- mlrun/db/base.py +12 -0
- mlrun/db/factory.py +65 -0
- mlrun/db/httpdb.py +175 -19
- mlrun/db/nopdb.py +4 -2
- mlrun/execution.py +4 -2
- mlrun/feature_store/__init__.py +1 -0
- mlrun/feature_store/api.py +1 -2
- mlrun/feature_store/feature_set.py +0 -10
- mlrun/feature_store/feature_vector.py +340 -2
- mlrun/feature_store/ingestion.py +5 -10
- mlrun/feature_store/retrieval/base.py +118 -104
- mlrun/feature_store/retrieval/dask_merger.py +17 -10
- mlrun/feature_store/retrieval/job.py +4 -1
- mlrun/feature_store/retrieval/local_merger.py +18 -18
- mlrun/feature_store/retrieval/spark_merger.py +21 -14
- mlrun/feature_store/retrieval/storey_merger.py +21 -15
- mlrun/kfpops.py +3 -9
- mlrun/launcher/base.py +3 -3
- mlrun/launcher/client.py +3 -2
- mlrun/launcher/factory.py +16 -13
- mlrun/lists.py +0 -11
- mlrun/model.py +9 -15
- mlrun/model_monitoring/helpers.py +15 -25
- mlrun/model_monitoring/model_monitoring_batch.py +72 -4
- mlrun/model_monitoring/prometheus.py +219 -0
- mlrun/model_monitoring/stores/__init__.py +15 -9
- mlrun/model_monitoring/stores/sql_model_endpoint_store.py +3 -1
- mlrun/model_monitoring/stream_processing.py +181 -29
- mlrun/package/packager.py +6 -8
- mlrun/package/packagers/default_packager.py +121 -10
- mlrun/platforms/__init__.py +0 -2
- mlrun/platforms/iguazio.py +0 -56
- mlrun/projects/pipelines.py +57 -158
- mlrun/projects/project.py +6 -32
- mlrun/render.py +1 -1
- mlrun/run.py +2 -124
- mlrun/runtimes/__init__.py +6 -42
- mlrun/runtimes/base.py +26 -1241
- mlrun/runtimes/daskjob.py +2 -198
- mlrun/runtimes/function.py +16 -5
- mlrun/runtimes/kubejob.py +5 -29
- mlrun/runtimes/mpijob/__init__.py +2 -2
- mlrun/runtimes/mpijob/abstract.py +10 -1
- mlrun/runtimes/mpijob/v1.py +0 -76
- mlrun/runtimes/mpijob/v1alpha1.py +1 -74
- mlrun/runtimes/nuclio.py +3 -2
- mlrun/runtimes/pod.py +0 -10
- mlrun/runtimes/remotesparkjob.py +1 -15
- mlrun/runtimes/serving.py +1 -1
- mlrun/runtimes/sparkjob/__init__.py +0 -1
- mlrun/runtimes/sparkjob/abstract.py +4 -131
- mlrun/serving/states.py +1 -1
- mlrun/utils/db.py +0 -2
- mlrun/utils/helpers.py +19 -13
- mlrun/utils/notifications/notification_pusher.py +5 -25
- mlrun/utils/regex.py +7 -2
- mlrun/utils/version/version.json +2 -2
- {mlrun-1.5.0rc1.dist-info → mlrun-1.5.0rc2.dist-info}/METADATA +24 -23
- {mlrun-1.5.0rc1.dist-info → mlrun-1.5.0rc2.dist-info}/RECORD +116 -107
- {mlrun-1.5.0rc1.dist-info → mlrun-1.5.0rc2.dist-info}/WHEEL +1 -1
- mlrun/mlutils/data.py +0 -160
- mlrun/mlutils/models.py +0 -78
- mlrun/mlutils/plots.py +0 -902
- {mlrun-1.5.0rc1.dist-info → mlrun-1.5.0rc2.dist-info}/LICENSE +0 -0
- {mlrun-1.5.0rc1.dist-info → mlrun-1.5.0rc2.dist-info}/entry_points.txt +0 -0
- {mlrun-1.5.0rc1.dist-info → mlrun-1.5.0rc2.dist-info}/top_level.txt +0 -0
mlrun/api/crud/pipelines.py
CHANGED
|
@@ -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(
|
|
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(
|
|
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"]
|
|
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.
|
|
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.
|
|
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.
|
|
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(
|
|
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
|
)
|