mlrun 1.3.1rc5__py3-none-any.whl → 1.4.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 (67) hide show
  1. mlrun/__main__.py +57 -4
  2. mlrun/api/api/endpoints/marketplace.py +57 -4
  3. mlrun/api/api/endpoints/runs.py +2 -0
  4. mlrun/api/api/utils.py +102 -0
  5. mlrun/api/crud/__init__.py +1 -0
  6. mlrun/api/crud/marketplace.py +133 -44
  7. mlrun/api/crud/notifications.py +80 -0
  8. mlrun/api/crud/runs.py +2 -0
  9. mlrun/api/crud/secrets.py +1 -0
  10. mlrun/api/db/base.py +32 -0
  11. mlrun/api/db/session.py +3 -11
  12. mlrun/api/db/sqldb/db.py +162 -1
  13. mlrun/api/db/sqldb/models/models_mysql.py +41 -0
  14. mlrun/api/db/sqldb/models/models_sqlite.py +35 -0
  15. mlrun/api/main.py +54 -1
  16. mlrun/api/migrations_mysql/versions/c905d15bd91d_notifications.py +70 -0
  17. mlrun/api/migrations_sqlite/versions/959ae00528ad_notifications.py +61 -0
  18. mlrun/api/schemas/__init__.py +1 -0
  19. mlrun/api/schemas/marketplace.py +18 -8
  20. mlrun/api/{db/filedb/__init__.py → schemas/notification.py} +17 -1
  21. mlrun/api/utils/singletons/db.py +8 -14
  22. mlrun/builder.py +37 -26
  23. mlrun/config.py +12 -2
  24. mlrun/data_types/spark.py +9 -2
  25. mlrun/datastore/base.py +10 -1
  26. mlrun/datastore/sources.py +1 -1
  27. mlrun/db/__init__.py +6 -4
  28. mlrun/db/base.py +1 -2
  29. mlrun/db/httpdb.py +32 -6
  30. mlrun/db/nopdb.py +463 -0
  31. mlrun/db/sqldb.py +47 -7
  32. mlrun/execution.py +3 -0
  33. mlrun/feature_store/api.py +26 -12
  34. mlrun/feature_store/common.py +1 -1
  35. mlrun/feature_store/steps.py +110 -13
  36. mlrun/k8s_utils.py +10 -0
  37. mlrun/model.py +43 -0
  38. mlrun/projects/operations.py +5 -2
  39. mlrun/projects/pipelines.py +4 -3
  40. mlrun/projects/project.py +50 -10
  41. mlrun/run.py +5 -4
  42. mlrun/runtimes/__init__.py +2 -6
  43. mlrun/runtimes/base.py +82 -31
  44. mlrun/runtimes/function.py +22 -0
  45. mlrun/runtimes/kubejob.py +10 -8
  46. mlrun/runtimes/serving.py +1 -1
  47. mlrun/runtimes/sparkjob/__init__.py +0 -1
  48. mlrun/runtimes/sparkjob/abstract.py +0 -2
  49. mlrun/serving/states.py +2 -2
  50. mlrun/utils/helpers.py +1 -1
  51. mlrun/utils/notifications/notification/__init__.py +1 -1
  52. mlrun/utils/notifications/notification/base.py +14 -13
  53. mlrun/utils/notifications/notification/console.py +6 -3
  54. mlrun/utils/notifications/notification/git.py +19 -12
  55. mlrun/utils/notifications/notification/ipython.py +6 -3
  56. mlrun/utils/notifications/notification/slack.py +13 -12
  57. mlrun/utils/notifications/notification_pusher.py +185 -37
  58. mlrun/utils/version/version.json +2 -2
  59. {mlrun-1.3.1rc5.dist-info → mlrun-1.4.0rc2.dist-info}/METADATA +6 -2
  60. {mlrun-1.3.1rc5.dist-info → mlrun-1.4.0rc2.dist-info}/RECORD +64 -63
  61. mlrun/api/db/filedb/db.py +0 -518
  62. mlrun/db/filedb.py +0 -899
  63. mlrun/runtimes/sparkjob/spark2job.py +0 -59
  64. {mlrun-1.3.1rc5.dist-info → mlrun-1.4.0rc2.dist-info}/LICENSE +0 -0
  65. {mlrun-1.3.1rc5.dist-info → mlrun-1.4.0rc2.dist-info}/WHEEL +0 -0
  66. {mlrun-1.3.1rc5.dist-info → mlrun-1.4.0rc2.dist-info}/entry_points.txt +0 -0
  67. {mlrun-1.3.1rc5.dist-info → mlrun-1.4.0rc2.dist-info}/top_level.txt +0 -0
mlrun/api/db/base.py CHANGED
@@ -12,10 +12,12 @@
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
14
  import datetime
15
+ import typing
15
16
  import warnings
16
17
  from abc import ABC, abstractmethod
17
18
  from typing import Any, Dict, List, Optional, Tuple, Union
18
19
 
20
+ import mlrun.model
19
21
  from mlrun.api import schemas
20
22
 
21
23
 
@@ -103,6 +105,7 @@ class DBInterface(ABC):
103
105
  max_partitions: int = 0,
104
106
  requested_logs: bool = None,
105
107
  return_as_run_structs: bool = True,
108
+ with_notifications: bool = False,
106
109
  ):
107
110
  pass
108
111
 
@@ -575,3 +578,32 @@ class DBInterface(ABC):
575
578
  self, session, name: str, project: str
576
579
  ) -> schemas.BackgroundTask:
577
580
  pass
581
+
582
+ @abstractmethod
583
+ def store_run_notifications(
584
+ self,
585
+ session,
586
+ notification_objects: typing.List[mlrun.model.Notification],
587
+ run_uid: str,
588
+ project: str,
589
+ ):
590
+ pass
591
+
592
+ @abstractmethod
593
+ def list_run_notifications(
594
+ self,
595
+ session,
596
+ run_uid: str,
597
+ project: str,
598
+ ) -> typing.List[mlrun.model.Notification]:
599
+ pass
600
+
601
+ def delete_run_notifications(
602
+ self,
603
+ session,
604
+ name: str = None,
605
+ run_uid: str = None,
606
+ project: str = None,
607
+ commit: bool = True,
608
+ ):
609
+ pass
mlrun/api/db/session.py CHANGED
@@ -15,22 +15,14 @@
15
15
  from sqlalchemy.orm import Session
16
16
 
17
17
  from mlrun.api.db.sqldb.session import create_session as sqldb_create_session
18
- from mlrun.config import config
19
18
 
20
19
 
21
- def create_session(db_type=None) -> Session:
22
- db_type = db_type or config.httpdb.db_type
23
- if db_type == "filedb":
24
- return None
25
- else:
26
- return sqldb_create_session()
20
+ def create_session() -> Session:
21
+ return sqldb_create_session()
27
22
 
28
23
 
29
24
  def close_session(db_session):
30
-
31
- # will be None when it's filedb session
32
- if db_session is not None:
33
- db_session.close()
25
+ db_session.close()
34
26
 
35
27
 
36
28
  def run_function_with_new_db_session(func):
mlrun/api/db/sqldb/db.py CHANGED
@@ -31,7 +31,9 @@ from sqlalchemy.orm import Session, aliased
31
31
  import mlrun
32
32
  import mlrun.api.db.session
33
33
  import mlrun.api.utils.projects.remotes.follower
34
+ import mlrun.api.utils.singletons.k8s
34
35
  import mlrun.errors
36
+ import mlrun.model
35
37
  from mlrun.api import schemas
36
38
  from mlrun.api.db.base import DBInterface
37
39
  from mlrun.api.db.sqldb.helpers import (
@@ -53,6 +55,7 @@ from mlrun.api.db.sqldb.models import (
53
55
  Function,
54
56
  Log,
55
57
  MarketplaceSource,
58
+ Notification,
56
59
  Project,
57
60
  Run,
58
61
  Schedule,
@@ -325,6 +328,7 @@ class SQLDB(DBInterface):
325
328
  max_partitions: int = 0,
326
329
  requested_logs: bool = None,
327
330
  return_as_run_structs: bool = True,
331
+ with_notifications: bool = False,
328
332
  ):
329
333
  project = project or config.default_project
330
334
  query = self._find_runs(session, uid, project, labels)
@@ -369,9 +373,28 @@ class SQLDB(DBInterface):
369
373
  if not return_as_run_structs:
370
374
  return query.all()
371
375
 
376
+ # Purposefully not using outer join to avoid returning runs without notifications
377
+ if with_notifications:
378
+ query = query.join(Notification, Run.id == Notification.run)
379
+
372
380
  runs = RunList()
373
381
  for run in query:
374
- runs.append(run.struct)
382
+ run_struct = run.struct
383
+ if with_notifications:
384
+ run_struct.setdefault("spec", {}).setdefault("notifications", [])
385
+ run_struct.setdefault("status", {}).setdefault("notifications", {})
386
+ for notification in run.notifications:
387
+ (
388
+ notification_spec,
389
+ notification_status,
390
+ ) = self._transform_notification_record_to_spec_and_status(
391
+ notification
392
+ )
393
+ run_struct["spec"]["notifications"].append(notification_spec)
394
+ run_struct["status"]["notifications"][
395
+ notification.name
396
+ ] = notification_status
397
+ runs.append(run_struct)
375
398
 
376
399
  return runs
377
400
 
@@ -1689,6 +1712,10 @@ class SQLDB(DBInterface):
1689
1712
  self._verify_empty_list_of_project_related_resources(name, logs, "logs")
1690
1713
  runs = self._find_runs(session, None, name, []).all()
1691
1714
  self._verify_empty_list_of_project_related_resources(name, runs, "runs")
1715
+ notifications = self._get_db_notifications(session, project=name)
1716
+ self._verify_empty_list_of_project_related_resources(
1717
+ name, notifications, "notifications"
1718
+ )
1692
1719
  schedules = self.list_schedules(session, project=name)
1693
1720
  self._verify_empty_list_of_project_related_resources(
1694
1721
  name, schedules, "schedules"
@@ -1709,6 +1736,7 @@ class SQLDB(DBInterface):
1709
1736
  def delete_project_related_resources(self, session: Session, name: str):
1710
1737
  self.del_artifacts(session, project=name)
1711
1738
  self._delete_logs(session, name)
1739
+ self.delete_run_notifications(session, project=name)
1712
1740
  self.del_runs(session, project=name)
1713
1741
  self.delete_schedules(session, name)
1714
1742
  self._delete_functions(session, name)
@@ -2832,6 +2860,13 @@ class SQLDB(DBInterface):
2832
2860
  query = query.filter(Run.uid.in_(uid))
2833
2861
  return self._add_labels_filter(session, query, Run, labels)
2834
2862
 
2863
+ def _get_db_notifications(
2864
+ self, session, name: str = None, run_id: int = None, project: str = None
2865
+ ):
2866
+ return self._query(
2867
+ session, Notification, name=name, run=run_id, project=project
2868
+ ).all()
2869
+
2835
2870
  def _latest_uid_filter(self, session, query):
2836
2871
  # Create a sub query of latest uid (by updated) per (project,key)
2837
2872
  subq = (
@@ -3168,6 +3203,35 @@ class SQLDB(DBInterface):
3168
3203
  # TODO: handle transforming the functions/workflows/artifacts references to real objects
3169
3204
  return schemas.Project(**project_record.full_object)
3170
3205
 
3206
+ def _transform_notification_record_to_spec_and_status(
3207
+ self,
3208
+ notification_record: Notification,
3209
+ ) -> typing.Tuple[dict, dict]:
3210
+ notification_spec = self._transform_notification_record_to_schema(
3211
+ notification_record
3212
+ ).to_dict()
3213
+ notification_status = {
3214
+ "status": notification_spec.pop("status", None),
3215
+ "sent_time": notification_spec.pop("sent_time", None),
3216
+ }
3217
+ return notification_spec, notification_status
3218
+
3219
+ @staticmethod
3220
+ def _transform_notification_record_to_schema(
3221
+ notification_record: Notification,
3222
+ ) -> mlrun.model.Notification:
3223
+ return mlrun.model.Notification(
3224
+ kind=notification_record.kind,
3225
+ name=notification_record.name,
3226
+ message=notification_record.message,
3227
+ severity=notification_record.severity,
3228
+ when=notification_record.when.split(","),
3229
+ condition=notification_record.condition,
3230
+ params=notification_record.params,
3231
+ status=notification_record.status,
3232
+ sent_time=notification_record.sent_time,
3233
+ )
3234
+
3171
3235
  def _move_and_reorder_table_items(
3172
3236
  self, session, moved_object, move_to=None, move_from=None
3173
3237
  ):
@@ -3543,3 +3607,100 @@ class SQLDB(DBInterface):
3543
3607
  ):
3544
3608
  return True
3545
3609
  return False
3610
+
3611
+ def store_run_notifications(
3612
+ self,
3613
+ session,
3614
+ notification_objects: typing.List[mlrun.model.Notification],
3615
+ run_uid: str,
3616
+ project: str,
3617
+ ):
3618
+ # iteration is 0, as we don't support multiple notifications per hyper param run, only for the whole run
3619
+ run = self._get_run(session, run_uid, project, 0)
3620
+ if not run:
3621
+ raise mlrun.errors.MLRunNotFoundError(
3622
+ f"Run not found: uid={run_uid}, project={project}"
3623
+ )
3624
+
3625
+ run_notifications = {
3626
+ notification.name: notification
3627
+ for notification in self._get_db_notifications(session, run_id=run.id)
3628
+ }
3629
+ notifications = []
3630
+ for notification_model in notification_objects:
3631
+ new_notification = False
3632
+ notification = run_notifications.get(notification_model.name, None)
3633
+ if not notification:
3634
+ new_notification = True
3635
+ notification = Notification(
3636
+ name=notification_model.name, run=run.id, project=project
3637
+ )
3638
+
3639
+ notification.kind = notification_model.kind
3640
+ notification.message = notification_model.message
3641
+ notification.severity = notification_model.severity
3642
+ notification.when = ",".join(notification_model.when)
3643
+ notification.condition = notification_model.condition
3644
+ notification.params = notification_model.params
3645
+ notification.status = (
3646
+ notification_model.status
3647
+ or mlrun.api.schemas.NotificationStatus.PENDING
3648
+ )
3649
+ notification.sent_time = notification_model.sent_time
3650
+
3651
+ logger.debug(
3652
+ f"Storing {'new' if new_notification else 'existing'} notification",
3653
+ notification_name=notification.name,
3654
+ run_uid=run_uid,
3655
+ project=project,
3656
+ )
3657
+ notifications.append(notification)
3658
+
3659
+ self._upsert(session, notifications)
3660
+
3661
+ def list_run_notifications(
3662
+ self,
3663
+ session,
3664
+ run_uid: str,
3665
+ project: str = "",
3666
+ ) -> typing.List[mlrun.model.Notification]:
3667
+
3668
+ # iteration is 0, as we don't support multiple notifications per hyper param run, only for the whole run
3669
+ run = self._get_run(session, run_uid, project, 0)
3670
+ if not run:
3671
+ return []
3672
+
3673
+ return [
3674
+ self._transform_notification_record_to_schema(notification)
3675
+ for notification in self._query(session, Notification, run=run.id).all()
3676
+ ]
3677
+
3678
+ def delete_run_notifications(
3679
+ self,
3680
+ session,
3681
+ name: str = None,
3682
+ run_uid: str = None,
3683
+ project: str = None,
3684
+ commit: bool = True,
3685
+ ):
3686
+ run_id = None
3687
+ if run_uid:
3688
+
3689
+ # iteration is 0, as we don't support multiple notifications per hyper param run, only for the whole run
3690
+ run = self._get_run(session, run_uid, project, 0)
3691
+ if not run:
3692
+ raise mlrun.errors.MLRunNotFoundError(
3693
+ f"Run not found: uid={run_uid}, project={project}"
3694
+ )
3695
+ run_id = run.id
3696
+
3697
+ project = project or config.default_project
3698
+ if project == "*":
3699
+ project = None
3700
+
3701
+ query = self._get_db_notifications(session, name, run_id, project)
3702
+ for notification in query:
3703
+ session.delete(notification)
3704
+
3705
+ if commit:
3706
+ session.commit()
@@ -139,6 +139,46 @@ with warnings.catch_warnings():
139
139
  def get_identifier_string(self) -> str:
140
140
  return f"{self.project}/{self.name}/{self.uid}"
141
141
 
142
+ class Notification(Base, mlrun.utils.db.BaseModel):
143
+ __tablename__ = "notifications"
144
+ __table_args__ = (UniqueConstraint("name", "run", name="_notifications_uc"),)
145
+
146
+ id = Column(Integer, primary_key=True)
147
+ project = Column(String(255, collation=SQLCollationUtil.collation()))
148
+ name = Column(
149
+ String(255, collation=SQLCollationUtil.collation()), nullable=False
150
+ )
151
+ kind = Column(
152
+ String(255, collation=SQLCollationUtil.collation()), nullable=False
153
+ )
154
+ message = Column(
155
+ String(255, collation=SQLCollationUtil.collation()), nullable=False
156
+ )
157
+ severity = Column(
158
+ String(255, collation=SQLCollationUtil.collation()), nullable=False
159
+ )
160
+ when = Column(
161
+ String(255, collation=SQLCollationUtil.collation()), nullable=False
162
+ )
163
+ condition = Column(
164
+ String(255, collation=SQLCollationUtil.collation()), nullable=False
165
+ )
166
+ params = Column("params", JSON)
167
+ run = Column(Integer, ForeignKey("runs.id"))
168
+
169
+ # TODO: Separate table for notification state.
170
+ # Currently, we are only supporting one notification being sent per DB row (either on completion or on error).
171
+ # In the future, we might want to support multiple notifications per DB row, and we might want to support on
172
+ # start, therefore we need to separate the state from the notification itself (e.g. this table can be table
173
+ # with notification_id, state, when, last_sent, etc.). This will require some refactoring in the code.
174
+ sent_time = Column(
175
+ sqlalchemy.dialects.mysql.TIMESTAMP(fsp=3),
176
+ nullable=True,
177
+ )
178
+ status = Column(
179
+ String(255, collation=SQLCollationUtil.collation()), nullable=False
180
+ )
181
+
142
182
  class Log(Base, mlrun.utils.db.BaseModel):
143
183
  __tablename__ = "logs"
144
184
 
@@ -182,6 +222,7 @@ with warnings.catch_warnings():
182
222
 
183
223
  labels = relationship(Label, cascade="all, delete-orphan")
184
224
  tags = relationship(Tag, cascade="all, delete-orphan")
225
+ notifications = relationship(Notification, cascade="all, delete-orphan")
185
226
 
186
227
  def get_identifier_string(self) -> str:
187
228
  return f"{self.project}/{self.uid}/{self.iteration}"
@@ -151,6 +151,40 @@ with warnings.catch_warnings():
151
151
  def get_identifier_string(self) -> str:
152
152
  return f"{self.project}/{self.uid}"
153
153
 
154
+ class Notification(Base, mlrun.utils.db.BaseModel):
155
+ __tablename__ = "notifications"
156
+ __table_args__ = (UniqueConstraint("name", "run", name="_notifications_uc"),)
157
+
158
+ id = Column(Integer, primary_key=True)
159
+ project = Column(String(255, collation=SQLCollationUtil.collation()))
160
+ name = Column(
161
+ String(255, collation=SQLCollationUtil.collation()), nullable=False
162
+ )
163
+ kind = Column(
164
+ String(255, collation=SQLCollationUtil.collation()), nullable=False
165
+ )
166
+ message = Column(
167
+ String(255, collation=SQLCollationUtil.collation()), nullable=False
168
+ )
169
+ severity = Column(
170
+ String(255, collation=SQLCollationUtil.collation()), nullable=False
171
+ )
172
+ when = Column(
173
+ String(255, collation=SQLCollationUtil.collation()), nullable=False
174
+ )
175
+ condition = Column(
176
+ String(255, collation=SQLCollationUtil.collation()), nullable=False
177
+ )
178
+ params = Column("params", JSON)
179
+ run = Column(Integer, ForeignKey("runs.id"))
180
+ sent_time = Column(
181
+ TIMESTAMP(),
182
+ nullable=True,
183
+ )
184
+ status = Column(
185
+ String(255, collation=SQLCollationUtil.collation()), nullable=False
186
+ )
187
+
154
188
  class Run(Base, mlrun.utils.db.HasStruct):
155
189
  __tablename__ = "runs"
156
190
  __table_args__ = (
@@ -178,6 +212,7 @@ with warnings.catch_warnings():
178
212
  requested_logs = Column(BOOLEAN)
179
213
  updated = Column(TIMESTAMP, default=datetime.utcnow)
180
214
  labels = relationship(Label)
215
+ notifications = relationship(Notification, cascade="all, delete-orphan")
181
216
 
182
217
  def get_identifier_string(self) -> str:
183
218
  return f"{self.project}/{self.uid}/{self.iteration}"
mlrun/api/main.py CHANGED
@@ -24,12 +24,14 @@ import sqlalchemy.orm
24
24
  import uvicorn
25
25
  from fastapi.exception_handlers import http_exception_handler
26
26
 
27
+ import mlrun.api.db.base
27
28
  import mlrun.api.schemas
28
29
  import mlrun.api.utils.clients.chief
29
30
  import mlrun.api.utils.clients.log_collector
30
31
  import mlrun.errors
31
32
  import mlrun.lists
32
33
  import mlrun.utils
34
+ import mlrun.utils.notifications
33
35
  import mlrun.utils.version
34
36
  from mlrun.api.api.api import api_router
35
37
  from mlrun.api.db.session import close_session, create_session
@@ -56,6 +58,14 @@ from mlrun.utils import logger
56
58
  API_PREFIX = "/api"
57
59
  BASE_VERSIONED_API_PREFIX = f"{API_PREFIX}/v1"
58
60
 
61
+ # When pushing notifications, push notifications only for runs that entered a terminal state
62
+ # since the last time we pushed notifications.
63
+ # On the first time we push notifications, we'll push notifications for all runs that are in a terminal state
64
+ # and their notifications haven't been sent yet.
65
+ # TODO: find better solution than a global variable for chunking the list of runs
66
+ # for which to push notifications
67
+ _last_notification_push_time: datetime.datetime = None
68
+
59
69
 
60
70
  app = fastapi.FastAPI(
61
71
  title="MLRun",
@@ -512,18 +522,20 @@ async def _align_worker_state_with_chief_state(
512
522
 
513
523
 
514
524
  def _monitor_runs():
525
+ db = get_db()
515
526
  db_session = create_session()
516
527
  try:
517
528
  for kind in RuntimeKinds.runtime_with_handlers():
518
529
  try:
519
530
  runtime_handler = get_runtime_handler(kind)
520
- runtime_handler.monitor_runs(get_db(), db_session)
531
+ runtime_handler.monitor_runs(db, db_session)
521
532
  except Exception as exc:
522
533
  logger.warning(
523
534
  "Failed monitoring runs. Ignoring",
524
535
  exc=err_to_str(exc),
525
536
  kind=kind,
526
537
  )
538
+ _push_terminal_run_notifications(db, db_session)
527
539
  finally:
528
540
  close_session(db_session)
529
541
 
@@ -545,6 +557,47 @@ def _cleanup_runtimes():
545
557
  close_session(db_session)
546
558
 
547
559
 
560
+ def _push_terminal_run_notifications(db: mlrun.api.db.base.DBInterface, db_session):
561
+ """
562
+ Get all runs with notification configs which became terminal since the last call to the function
563
+ and push their notifications if they haven't been pushed yet.
564
+ """
565
+
566
+ # Import here to avoid circular import
567
+ import mlrun.api.api.utils
568
+
569
+ # When pushing notifications, push notifications only for runs that entered a terminal state
570
+ # since the last time we pushed notifications.
571
+ # On the first time we push notifications, we'll push notifications for all runs that are in a terminal state
572
+ # and their notifications haven't been sent yet.
573
+ global _last_notification_push_time
574
+
575
+ runs = db.list_runs(
576
+ db_session,
577
+ project="*",
578
+ states=mlrun.runtimes.constants.RunStates.terminal_states(),
579
+ last_update_time_from=_last_notification_push_time,
580
+ with_notifications=True,
581
+ )
582
+
583
+ if not len(runs):
584
+ return
585
+
586
+ # Unmasking the run parameters from secrets before handing them over to the notification handler
587
+ # as importing the `Secrets` crud in the notification handler will cause a circular import
588
+ unmasked_runs = [
589
+ mlrun.api.api.utils.unmask_notification_params_secret_on_task(run)
590
+ for run in runs
591
+ ]
592
+
593
+ logger.debug(
594
+ "Got terminal runs with configured notifications", runs_amount=len(runs)
595
+ )
596
+ mlrun.utils.notifications.NotificationPusher(unmasked_runs).push(db)
597
+
598
+ _last_notification_push_time = datetime.datetime.now(datetime.timezone.utc)
599
+
600
+
548
601
  async def _stop_logs():
549
602
  """
550
603
  Stop logs for runs that are in terminal state and last updated in the previous interval
@@ -0,0 +1,70 @@
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
+ """notifications
16
+
17
+ Revision ID: c905d15bd91d
18
+ Revises: 88e656800d6a
19
+ Create Date: 2022-09-20 10:44:41.727488
20
+
21
+ """
22
+ import sqlalchemy as sa
23
+ from alembic import op
24
+ from sqlalchemy.dialects import mysql
25
+
26
+ # revision identifiers, used by Alembic.
27
+ revision = "c905d15bd91d"
28
+ down_revision = "88e656800d6a"
29
+ branch_labels = None
30
+ depends_on = None
31
+
32
+
33
+ def upgrade():
34
+ # ### commands auto generated by Alembic - please adjust! ###
35
+ op.create_table(
36
+ "notifications",
37
+ sa.Column("id", sa.Integer(), nullable=False),
38
+ sa.Column("project", sa.String(length=255, collation="utf8_bin")),
39
+ sa.Column("name", sa.String(length=255, collation="utf8_bin"), nullable=False),
40
+ sa.Column("kind", sa.String(length=255, collation="utf8_bin"), nullable=False),
41
+ sa.Column(
42
+ "message", sa.String(length=255, collation="utf8_bin"), nullable=False
43
+ ),
44
+ sa.Column(
45
+ "severity", sa.String(length=255, collation="utf8_bin"), nullable=False
46
+ ),
47
+ sa.Column("when", sa.String(length=255, collation="utf8_bin"), nullable=False),
48
+ sa.Column(
49
+ "condition", sa.String(length=255, collation="utf8_bin"), nullable=False
50
+ ),
51
+ sa.Column("params", sa.JSON(), nullable=True),
52
+ sa.Column("run", sa.Integer(), nullable=True),
53
+ sa.Column("sent_time", mysql.TIMESTAMP(fsp=3), nullable=True),
54
+ sa.Column(
55
+ "status", sa.String(length=255, collation="utf8_bin"), nullable=False
56
+ ),
57
+ sa.ForeignKeyConstraint(
58
+ ["run"],
59
+ ["runs.id"],
60
+ ),
61
+ sa.PrimaryKeyConstraint("id"),
62
+ sa.UniqueConstraint("name", "run", name="_notifications_uc"),
63
+ )
64
+ # ### end Alembic commands ###
65
+
66
+
67
+ def downgrade():
68
+ # ### commands auto generated by Alembic - please adjust! ###
69
+ op.drop_table("notifications")
70
+ # ### end Alembic commands ###
@@ -0,0 +1,61 @@
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
+ """notifications
16
+
17
+ Revision ID: 959ae00528ad
18
+ Revises: 803438ecd005
19
+ Create Date: 2022-09-20 10:40:41.354209
20
+
21
+ """
22
+ import sqlalchemy as sa
23
+ from alembic import op
24
+
25
+ # revision identifiers, used by Alembic.
26
+ revision = "959ae00528ad"
27
+ down_revision = "803438ecd005"
28
+ branch_labels = None
29
+ depends_on = None
30
+
31
+
32
+ def upgrade():
33
+ # ### commands auto generated by Alembic - please adjust! ###
34
+ op.create_table(
35
+ "notifications",
36
+ sa.Column("id", sa.Integer(), nullable=False),
37
+ sa.Column("project", sa.String(length=255)),
38
+ sa.Column("name", sa.String(length=255), nullable=False),
39
+ sa.Column("kind", sa.String(length=255), nullable=False),
40
+ sa.Column("message", sa.String(length=255), nullable=False),
41
+ sa.Column("severity", sa.String(length=255), nullable=False),
42
+ sa.Column("when", sa.String(length=255), nullable=False),
43
+ sa.Column("condition", sa.String(length=255), nullable=False),
44
+ sa.Column("params", sa.JSON(), nullable=True),
45
+ sa.Column("run", sa.Integer(), nullable=True),
46
+ sa.Column("sent_time", sa.TIMESTAMP(), nullable=True),
47
+ sa.Column("status", sa.String(length=255), nullable=False),
48
+ sa.ForeignKeyConstraint(
49
+ ["run"],
50
+ ["runs.id"],
51
+ ),
52
+ sa.PrimaryKeyConstraint("id"),
53
+ sa.UniqueConstraint("name", "run", name="_notifications_uc"),
54
+ )
55
+ # ### end Alembic commands ###
56
+
57
+
58
+ def downgrade():
59
+ # ### commands auto generated by Alembic - please adjust! ###
60
+ op.drop_table("notifications")
61
+ # ### end Alembic commands ###
@@ -107,6 +107,7 @@ from .model_endpoints import (
107
107
  ModelEndpointStatus,
108
108
  ModelMonitoringStoreKinds,
109
109
  )
110
+ from .notification import NotificationSeverity, NotificationStatus
110
111
  from .object import ObjectKind, ObjectMetadata, ObjectSpec, ObjectStatus
111
112
  from .pipeline import PipelinesFormat, PipelinesOutput, PipelinesPagination
112
113
  from .project import (