Browse Source

NotificationManager is now singleton

Dimitri Korsch 3 years ago
parent
commit
a6e732bbb6

+ 22 - 25
pycs/frontend/WebServer.py

@@ -91,8 +91,7 @@ class WebServer:
         self.logger.info('Starting job runner... ')
 
         # create notification manager
-        self.nm = NotificationManager(self.sio)
-        JobRunner().init_notifications(self.nm)
+        NotificationManager().setup(self.sio)
 
         self.start_runner()
         self.define_routes()
@@ -101,7 +100,7 @@ class WebServer:
 
 
     def start_runner(self):
-        app.logger.info(f"Main Thread ID: {threading.get_ident()}")
+        app.logger.info(f"Main Thread ID: 0x{threading.get_ident():x}")
         JobRunner().start()
         self.pipelines.start()
 
@@ -202,19 +201,19 @@ class WebServer:
         )
         self.app.add_url_rule(
             '/projects/<int:identifier>/labels',
-            view_func=CreateLabel.as_view('create_label', self.nm)
+            view_func=CreateLabel.as_view('create_label')
         )
         self.app.add_url_rule(
             '/projects/<int:project_id>/labels/<int:label_id>/remove',
-            view_func=RemoveLabel.as_view('remove_label', self.nm)
+            view_func=RemoveLabel.as_view('remove_label')
         )
         self.app.add_url_rule(
             '/projects/<int:project_id>/labels/<int:label_id>/name',
-            view_func=EditLabelName.as_view('edit_label_name', self.nm)
+            view_func=EditLabelName.as_view('edit_label_name')
         )
         self.app.add_url_rule(
             '/projects/<int:project_id>/labels/<int:label_id>/parent',
-            view_func=EditLabelParent.as_view('edit_label_parent', self.nm)
+            view_func=EditLabelParent.as_view('edit_label_parent')
         )
 
         # collections
@@ -231,7 +230,7 @@ class WebServer:
         # data
         self.app.add_url_rule(
             '/projects/<int:identifier>/data',
-            view_func=UploadFile.as_view('upload_file', self.nm)
+            view_func=UploadFile.as_view('upload_file')
         )
         self.app.add_url_rule(
             '/projects/<int:project_id>/data/<int:start>/<int:length>',
@@ -239,7 +238,7 @@ class WebServer:
         )
         self.app.add_url_rule(
             '/data/<int:identifier>/remove',
-            view_func=RemoveFile.as_view('remove_file', self.nm)
+            view_func=RemoveFile.as_view('remove_file')
         )
         self.app.add_url_rule(
             '/data/<int:file_id>',
@@ -265,27 +264,27 @@ class WebServer:
         )
         self.app.add_url_rule(
             '/data/<int:file_id>/results',
-            view_func=CreateResult.as_view('create_result', self.nm)
+            view_func=CreateResult.as_view('create_result')
         )
         self.app.add_url_rule(
             '/data/<int:file_id>/reset',
-            view_func=ResetResults.as_view('reset_results', self.nm)
+            view_func=ResetResults.as_view('reset_results')
         )
         self.app.add_url_rule(
             '/results/<int:result_id>/remove',
-            view_func=RemoveResult.as_view('remove_result', self.nm)
+            view_func=RemoveResult.as_view('remove_result')
         )
         self.app.add_url_rule(
             '/results/<int:result_id>/confirm',
-            view_func=ConfirmResult.as_view('confirm_result', self.nm)
+            view_func=ConfirmResult.as_view('confirm_result')
         )
         self.app.add_url_rule(
             '/results/<int:result_id>/label',
-            view_func=EditResultLabel.as_view('edit_result_label', self.nm)
+            view_func=EditResultLabel.as_view('edit_result_label')
         )
         self.app.add_url_rule(
             '/results/<int:result_id>/data',
-            view_func=EditResultData.as_view('edit_result_data', self.nm)
+            view_func=EditResultData.as_view('edit_result_data')
         )
 
         # projects
@@ -295,29 +294,27 @@ class WebServer:
         )
         self.app.add_url_rule(
             '/projects',
-            view_func=CreateProject.as_view('create_project', self.nm)
+            view_func=CreateProject.as_view('create_project')
         )
         self.app.add_url_rule(
             '/projects/<int:identifier>/label_provider',
-            view_func=ExecuteLabelProvider.as_view('execute_label_provider',
-                                                   self.nm)
+            view_func=ExecuteLabelProvider.as_view('execute_label_provider')
         )
         self.app.add_url_rule(
             '/projects/<int:identifier>/external_storage',
-            view_func=ExecuteExternalStorage.as_view('execute_external_storage',
-                                                     self.nm)
+            view_func=ExecuteExternalStorage.as_view('execute_external_storage')
         )
         self.app.add_url_rule(
             '/projects/<int:identifier>/remove',
-            view_func=RemoveProject.as_view('remove_project', self.nm)
+            view_func=RemoveProject.as_view('remove_project')
         )
         self.app.add_url_rule(
             '/projects/<int:identifier>/name',
-            view_func=EditProjectName.as_view('edit_project_name', self.nm)
+            view_func=EditProjectName.as_view('edit_project_name')
         )
         self.app.add_url_rule(
             '/projects/<int:identifier>/description',
-            view_func=EditProjectDescription.as_view('edit_project_description', self.nm)
+            view_func=EditProjectDescription.as_view('edit_project_description')
         )
 
         # pipelines
@@ -327,11 +324,11 @@ class WebServer:
         )
         self.app.add_url_rule(
             '/projects/<int:project_id>/pipelines/predict',
-            view_func=PredictModel.as_view('predict_model', self.nm, self.pipelines)
+            view_func=PredictModel.as_view('predict_model', self.pipelines)
         )
         self.app.add_url_rule(
             '/data/<int:file_id>/predict',
-            view_func=PredictFile.as_view('predict_file', self.nm, self.pipelines)
+            view_func=PredictFile.as_view('predict_file', self.pipelines)
         )
 
     def run(self):

+ 1 - 5
pycs/frontend/endpoints/data/RemoveFile.py

@@ -14,10 +14,6 @@ class RemoveFile(View):
     # pylint: disable=arguments-differ
     methods = ['POST']
 
-    def __init__(self, nm: NotificationManager):
-        # pylint: disable=invalid-name
-        self.nm = nm
-
     def dispatch_request(self, identifier):
         # extract request data
         data = request.get_json(force=True)
@@ -43,5 +39,5 @@ class RemoveFile(View):
             # TODO remove temp files
 
         # send notification
-        self.nm.remove_file(file.serialize())
+        NotificationManager.removed("file", file.serialize())
         return make_response()

+ 3 - 4
pycs/frontend/endpoints/data/UploadFile.py

@@ -7,6 +7,7 @@ from flask.views import View
 from werkzeug import formparser
 
 from pycs.database.Project import Project
+from pycs.database.File import File
 from pycs.frontend.notifications.NotificationManager import NotificationManager
 from pycs.util.FileParser import file_info
 
@@ -18,9 +19,7 @@ class UploadFile(View):
     # pylint: disable=arguments-differ
     methods = ['POST']
 
-    def __init__(self, nm: NotificationManager):
-        # pylint: disable=invalid-name
-        self.nm = nm
+    def __init__(self):
 
         self.data_folder = None
         self.file_id = None
@@ -62,7 +61,7 @@ class UploadFile(View):
                                    self.file_size, self.file_id, frames, fps)
 
         # send update
-        self.nm.create_file(file.id)
+        NotificationManager.created("file", file.id, File)
 
         # return default success response
         return make_response()

+ 2 - 4
pycs/frontend/endpoints/labels/CreateLabel.py

@@ -3,6 +3,7 @@ from flask.views import View
 
 from pycs import db
 from pycs.database.Project import Project
+from pycs.database.Label import Label
 from pycs.frontend.notifications.NotificationManager import NotificationManager
 
 
@@ -13,9 +14,6 @@ class CreateLabel(View):
     # pylint: disable=arguments-differ
     methods = ['POST']
 
-    def __init__(self, nm: NotificationManager):
-        # pylint: disable=invalid-name
-        self.nm = nm
 
     def dispatch_request(self, identifier):
         # extract request data
@@ -36,7 +34,7 @@ class CreateLabel(View):
         label, _ = project.create_label(name, parent_id=parent)
 
         # send notification
-        self.nm.create_label(label.id)
+        NotificationManager.created("label", label.id, Label)
 
         # return success response
         return make_response()

+ 2 - 4
pycs/frontend/endpoints/labels/EditLabelName.py

@@ -2,6 +2,7 @@ from flask import request, abort, make_response
 from flask.views import View
 
 from pycs.database.Project import Project
+from pycs.database.Label import Label
 from pycs.frontend.notifications.NotificationManager import NotificationManager
 
 
@@ -12,9 +13,6 @@ class EditLabelName(View):
     # pylint: disable=arguments-differ
     methods = ['POST']
 
-    def __init__(self, nm: NotificationManager):
-        # pylint: disable=invalid-name
-        self.nm = nm
 
     def dispatch_request(self, project_id: int, label_id: int):
         # extract request data
@@ -39,7 +37,7 @@ class EditLabelName(View):
             label.set_name(data['name'])
 
         # send notification
-        self.nm.edit_label(label.id)
+        NotificationManager.edited("label", label.id, Label)
 
         # return success response
         return make_response()

+ 1 - 4
pycs/frontend/endpoints/labels/EditLabelParent.py

@@ -12,9 +12,6 @@ class EditLabelParent(View):
     # pylint: disable=arguments-differ
     methods = ['POST']
 
-    def __init__(self, nm: NotificationManager):
-        # pylint: disable=invalid-name
-        self.nm = nm
 
     def dispatch_request(self, project_id: int, label_id: int):
         # extract request data
@@ -32,7 +29,7 @@ class EditLabelParent(View):
         label.set_parent(data['parent'])
 
         # send notification
-        self.nm.edit_label(label.id)
+        NotificationManager.edited("label", label.id, Label)
 
         # return success response
         return make_response()

+ 3 - 5
pycs/frontend/endpoints/labels/RemoveLabel.py

@@ -3,6 +3,7 @@ from flask.views import View
 
 from pycs import db
 from pycs.database.Project import Project
+from pycs.database.Label import Label
 from pycs.frontend.notifications.NotificationManager import NotificationManager
 
 
@@ -13,9 +14,6 @@ class RemoveLabel(View):
     # pylint: disable=arguments-differ
     methods = ['POST']
 
-    def __init__(self, nm: NotificationManager):
-        # pylint: disable=invalid-name
-        self.nm = nm
 
     def dispatch_request(self, project_id: int, label_id: int):
         # extract request data
@@ -42,11 +40,11 @@ class RemoveLabel(View):
             # remove children's parent entry
             for child in children:
                 child.set_parent(None)
-                self.nm.edit_label(child.id)
+                NotificationManager.edited("label", child.id, Label)
 
             # remove label
             label.remove()
-            self.nm.remove_label(label.serialize())
+            NotificationManager.removed("label", label.serialize())
 
         # return success response
         return make_response()

+ 2 - 4
pycs/frontend/endpoints/pipelines/PredictFile.py

@@ -4,7 +4,6 @@ from flask.views import View
 from pycs.database.File import File
 from pycs.frontend.endpoints.pipelines.PredictModel import PredictModel
 from pycs.frontend.notifications.NotificationList import NotificationList
-from pycs.frontend.notifications.NotificationManager import NotificationManager
 from pycs.jobs.JobGroupBusyException import JobGroupBusyException
 from pycs.jobs.JobRunner import JobRunner
 from pycs.util.PipelineCache import PipelineCache
@@ -17,9 +16,8 @@ class PredictFile(View):
     # pylint: disable=arguments-differ
     methods = ['POST']
 
-    def __init__(self, nm: NotificationManager, pipelines: PipelineCache):
+    def __init__(self, pipelines: PipelineCache):
         # pylint: disable=invalid-name
-        self.nm = nm
         self.pipelines = pipelines
 
     def dispatch_request(self, file_id):
@@ -39,7 +37,7 @@ class PredictFile(View):
 
         # create job
         try:
-            notifications = NotificationList(self.nm)
+            notifications = NotificationList()
 
             JobRunner.Run(project,
                           'Model Interaction',

+ 2 - 4
pycs/frontend/endpoints/pipelines/PredictModel.py

@@ -11,7 +11,6 @@ from pycs import db
 from pycs.database.File import File
 from pycs.database.Project import Project
 from pycs.frontend.notifications.NotificationList import NotificationList
-from pycs.frontend.notifications.NotificationManager import NotificationManager
 from pycs.interfaces.MediaFile import MediaFile
 from pycs.interfaces.MediaStorage import MediaStorage
 from pycs.jobs.JobGroupBusyException import JobGroupBusyException
@@ -26,9 +25,8 @@ class PredictModel(View):
     # pylint: disable=arguments-differ
     methods = ['POST']
 
-    def __init__(self, nm: NotificationManager, pipelines: PipelineCache):
+    def __init__(self, pipelines: PipelineCache):
         # pylint: disable=invalid-name
-        self.nm = nm
         self.pipelines = pipelines
 
     def dispatch_request(self, project_id):
@@ -45,7 +43,7 @@ class PredictModel(View):
 
         # create job
         try:
-            notifications = NotificationList(self.nm)
+            notifications = NotificationList()
 
             JobRunner.Run(project,
                           'Model Interaction',

+ 4 - 8
pycs/frontend/endpoints/projects/CreateProject.py

@@ -29,9 +29,6 @@ class CreateProject(View):
     # pylint: disable=arguments-differ
     methods = ['POST']
 
-    def __init__(self, nm: NotificationManager):
-        # pylint: disable=invalid-name
-        self.nm = nm
 
     @property
     def project_folder(self):
@@ -103,8 +100,7 @@ class CreateProject(View):
 
         # execute label provider and add labels to project
         if label_provider is not None:
-            ExecuteLabelProvider.execute_label_provider(self.nm, project,
-                                                        label_provider)
+            ExecuteLabelProvider.execute_label_provider(project, label_provider)
 
         root_folder = model.root_folder
         # load model and add collections to the project
@@ -135,11 +131,11 @@ class CreateProject(View):
 
         # find media files
         if external_data:
-            ExecuteExternalStorage.find_media_files(self.nm, project)
+            ExecuteExternalStorage.find_media_files(project)
 
         # fire event
-        self.nm.create_model(model.id)
-        self.nm.create_project(project.id)
+        NotificationManager.created("model", model.id, Model)
+        NotificationManager.created("project", project.id, Project)
 
         # return success response
         return make_response()

+ 1 - 4
pycs/frontend/endpoints/projects/EditProjectDescription.py

@@ -13,9 +13,6 @@ class EditProjectDescription(View):
     # pylint: disable=arguments-differ
     methods = ['POST']
 
-    def __init__(self, nm: NotificationManager):
-        # pylint: disable=invalid-name
-        self.nm = nm
 
     def dispatch_request(self, identifier):
         # extract request data
@@ -32,6 +29,6 @@ class EditProjectDescription(View):
 
             # set description
             project.set_description(data['description'])
-            self.nm.edit_project(project.id)
+            NotificationManager.edited("project", project.id, Project)
 
         return make_response()

+ 1 - 4
pycs/frontend/endpoints/projects/EditProjectName.py

@@ -13,9 +13,6 @@ class EditProjectName(View):
     # pylint: disable=arguments-differ
     methods = ['POST']
 
-    def __init__(self, nm: NotificationManager):
-        # pylint: disable=invalid-name
-        self.nm = nm
 
     def dispatch_request(self, identifier):
         # extract request data
@@ -33,6 +30,6 @@ class EditProjectName(View):
 
             # set name
             project.set_name(data['name'])
-            self.nm.edit_project(project.id)
+            NotificationManager.edited("project", project.id, Project)
 
             return make_response()

+ 4 - 7
pycs/frontend/endpoints/projects/ExecuteExternalStorage.py

@@ -9,6 +9,7 @@ from flask.views import View
 
 from pycs import db
 from pycs.database.Project import Project
+from pycs.database.File import File
 from pycs.frontend.notifications.NotificationManager import NotificationManager
 from pycs.jobs.JobGroupBusyException import JobGroupBusyException
 from pycs.jobs.JobRunner import JobRunner
@@ -22,9 +23,6 @@ class ExecuteExternalStorage(View):
     # pylint: disable=arguments-differ
     methods = ['POST']
 
-    def __init__(self, nm: NotificationManager):
-        # pylint: disable=invalid-name
-        self.nm = nm
 
     def dispatch_request(self, identifier):
         # extract request data
@@ -43,7 +41,7 @@ class ExecuteExternalStorage(View):
 
         # execute label provider and add labels to project
         try:
-            ExecuteExternalStorage.find_media_files(self.nm, project)
+            ExecuteExternalStorage.find_media_files(project)
 
         except JobGroupBusyException:
             return abort(400)
@@ -51,12 +49,11 @@ class ExecuteExternalStorage(View):
         return make_response()
 
     @staticmethod
-    def find_media_files(nm: NotificationManager, project: Project):
+    def find_media_files(project: Project):
         """
         start a job that finds media files in the projects data_folder and adds them to the
         database afterwards
 
-        :param nm: notification manager object
         :param project: project
         :return:
         """
@@ -106,7 +103,7 @@ class ExecuteExternalStorage(View):
                                                     frames, fps)
 
                     if is_new:
-                        nm.create_file(file.id)
+                        NotificationManager.created("file", file.id, File)
 
             return current / length
 

+ 5 - 11
pycs/frontend/endpoints/projects/ExecuteLabelProvider.py

@@ -9,6 +9,7 @@ from pycs import app
 from pycs import db
 from pycs.database.LabelProvider import LabelProvider
 from pycs.database.Project import Project
+from pycs.database.Label import Label
 from pycs.frontend.notifications.NotificationManager import NotificationManager
 from pycs.jobs.JobGroupBusyException import JobGroupBusyException
 from pycs.jobs.JobRunner import JobRunner
@@ -21,9 +22,6 @@ class ExecuteLabelProvider(View):
     # pylint: disable=arguments-differ
     methods = ['POST']
 
-    def __init__(self, nm: NotificationManager):
-        # pylint: disable=invalid-name
-        self.nm = nm
 
     def dispatch_request(self, identifier):
         # extract request data
@@ -44,20 +42,18 @@ class ExecuteLabelProvider(View):
 
         # execute label provider and add labels to project
         try:
-            self.execute_label_provider(self.nm, project, label_provider)
+            self.execute_label_provider(project, label_provider)
         except JobGroupBusyException:
             return abort(400)
 
         return make_response()
 
     @staticmethod
-    def execute_label_provider(nm: NotificationManager,
-                               project: Project, label_provider: LabelProvider):
+    def execute_label_provider(project: Project, label_provider: LabelProvider):
         """
         start a job that loads and executes a label provider and saves its results to the
         database afterwards
 
-        :param nm: notification manager object
         :param project: project
         :param label_provider: label provider
         :return:
@@ -82,10 +78,8 @@ class ExecuteLabelProvider(View):
                     created.append((created_label, is_new))
 
             for label, is_new in created:
-                if is_new:
-                    nm.create_label(label.id)
-                else:
-                    nm.edit_label(label.id)
+                notify = NotificationManager.created if is_new else NotificationManager.edited
+                notify("label", label.id, Label)
 
         # run job with given functions
         JobRunner.Run(project,

+ 2 - 5
pycs/frontend/endpoints/projects/RemoveProject.py

@@ -15,9 +15,6 @@ class RemoveProject(View):
     # pylint: disable=arguments-differ
     methods = ['POST']
 
-    def __init__(self, nm: NotificationManager):
-        # pylint: disable=invalid-name
-        self.nm = nm
 
     def dispatch_request(self, identifier):
         # extract request data
@@ -41,8 +38,8 @@ class RemoveProject(View):
             project.remove(commit=False)
 
         # send update
-        self.nm.remove_model(model.serialize())
-        self.nm.remove_project(project.serialize())
+        NotificationManager.removed("model", model.serialize())
+        NotificationManager.removed("project", project.serialize())
 
         # remove from file system
         shutil.rmtree(project.root_folder)

+ 1 - 4
pycs/frontend/endpoints/results/ConfirmResult.py

@@ -13,9 +13,6 @@ class ConfirmResult(View):
     # pylint: disable=arguments-differ
     methods = ['POST']
 
-    def __init__(self, nm: NotificationManager):
-        # pylint: disable=invalid-name
-        self.nm = nm
 
     def dispatch_request(self, result_id: int):
         # extract request data
@@ -30,5 +27,5 @@ class ConfirmResult(View):
             return abort(404)
 
         result.set_origin('user')
-        self.nm.edit_result(result.id)
+        NotificationManager.edited("result", result.id, Result)
         return make_response()

+ 3 - 5
pycs/frontend/endpoints/results/CreateResult.py

@@ -3,6 +3,7 @@ from flask.views import View
 
 from pycs import db
 from pycs.database.File import File
+from pycs.database.Result import Result
 from pycs.frontend.notifications.NotificationManager import NotificationManager
 
 
@@ -13,9 +14,6 @@ class CreateResult(View):
     # pylint: disable=arguments-differ
     methods = ['POST']
 
-    def __init__(self, nm: NotificationManager):
-        # pylint: disable=invalid-name
-        self.nm = nm
 
     def dispatch_request(self, file_id: int):
         # extract request data
@@ -64,7 +62,7 @@ class CreateResult(View):
                 commit=False)
 
         for result in removed:
-            self.nm.remove_result(result.serialize())
+            NotificationManager.removed("result", result.serialize())
 
-        self.nm.create_result(new_result.id)
+        NotificationManager.created("result", new_result.id, Result)
         return jsonify(new_result)

+ 1 - 5
pycs/frontend/endpoints/results/EditResultData.py

@@ -12,10 +12,6 @@ class EditResultData(View):
     # pylint: disable=arguments-differ
     methods = ['POST']
 
-    def __init__(self, nm: NotificationManager):
-        # pylint: disable=invalid-name
-        self.nm = nm
-
     def dispatch_request(self, result_id: int):
         # extract request data
         data = request.get_json(force=True)
@@ -33,5 +29,5 @@ class EditResultData(View):
             result.data = data['data']
             result.origin = 'user'
 
-        self.nm.edit_result(result.id)
+        NotificationManager.edited("result", result.id, Result)
         return make_response()

+ 1 - 4
pycs/frontend/endpoints/results/EditResultLabel.py

@@ -12,9 +12,6 @@ class EditResultLabel(View):
     # pylint: disable=arguments-differ
     methods = ['POST']
 
-    def __init__(self, nm: NotificationManager):
-        # pylint: disable=invalid-name
-        self.nm = nm
 
     def dispatch_request(self, result_id: int):
         # extract request data
@@ -37,5 +34,5 @@ class EditResultLabel(View):
             result.label_id = int(data['label'])
             result.origin = 'user'
 
-        self.nm.edit_result(result.id)
+        NotificationManager.edited("result", result.id, Result)
         return make_response()

+ 1 - 4
pycs/frontend/endpoints/results/RemoveResult.py

@@ -13,9 +13,6 @@ class RemoveResult(View):
     # pylint: disable=arguments-differ
     methods = ['POST']
 
-    def __init__(self, nm: NotificationManager):
-        # pylint: disable=invalid-name
-        self.nm = nm
 
     def dispatch_request(self, result_id: int):
         # extract request data
@@ -33,5 +30,5 @@ class RemoveResult(View):
         with db.session.begin_nested():
             result.remove()
 
-        self.nm.remove_result(result.serialize())
+        NotificationManager.removed("result", result.serialize())
         return make_response()

+ 2 - 4
pycs/frontend/endpoints/results/ResetResults.py

@@ -3,6 +3,7 @@ from flask.views import View, request
 
 from pycs import db
 from pycs.database.File import File
+from pycs.database.Result import Result
 from pycs.frontend.notifications.NotificationManager import NotificationManager
 
 
@@ -13,9 +14,6 @@ class ResetResults(View):
     # pylint: disable=arguments-differ
     methods = ['POST']
 
-    def __init__(self, nm: NotificationManager):
-        # pylint: disable=invalid-name
-        self.nm = nm
 
     def dispatch_request(self, file_id: int):
         # extract request data
@@ -40,6 +38,6 @@ class ResetResults(View):
                 result.remove()
 
         for result in removed:
-            self.nm.remove_result(result)
+            NotificationManager.removed("result", result, Result)
 
         return make_response()

+ 17 - 8
pycs/frontend/notifications/NotificationList.py

@@ -1,3 +1,7 @@
+import warnings
+
+from functools import partial
+
 from pycs.frontend.notifications.NotificationManager import NotificationManager
 
 
@@ -6,24 +10,29 @@ class NotificationList:
     stores notifications to fire them later in the correct thread
     """
 
-    def __init__(self, notifications: NotificationManager):
+    def __init__(self):
         self.__list = []
-        self.notifications = notifications
 
-    def add(self, fun: callable, *params):
+    def add(self, name: str, suffix: str, *args, **kwargs):
         """
         add a function and parameters to be executed later
 
         :param fun: function
         :param params: parameters
         """
-        self.__list.append((fun, *params))
+        func = NotificationManager()._new_signal(name, suffix, *args, **kwargs)
+        self.__list.append(func)
 
-    def fire(self):
+    def fire(self, *args, **kwargs):
         """
         fire all stored events and reset the list
         """
-        for fun, *params in self.__list:
-            fun(*params)
+        for func in self.__list:
+            try:
+                func(*args, **kwargs)
+            except Exception as e:
 
-        self.__list = []
+                warnings.warn(f"{func} failed with arguments {args} and {kwargs}: {e}!")
+
+
+        self.__list.clear()

+ 202 - 155
pycs/frontend/notifications/NotificationManager.py

@@ -1,4 +1,8 @@
+import warnings
+
+from functools import partial
 from socketio import Server
+from typing import Callable
 
 from pycs import app
 from pycs.database.File import File
@@ -8,18 +12,29 @@ from pycs.database.Project import Project
 from pycs.database.Result import Result
 from pycs.frontend.util.JSONEncoder import JSONEncoder
 from pycs.jobs.Job import Job
+from pycs.util import Singleton
 
 
-class NotificationManager:
+class NotificationManager(Singleton):
     """
     send events via a socket.io connection
     """
 
-    def __init__(self, sio: Server):
+    def __repr__(self) -> str:
+        return f"<{self.__class__.__name__} 0x{id(self):x}>"
+
+    def setup(self, sio: Server):
         self.sio = sio
+
+    def init(self):
+        self.sio = None
         self.json = JSONEncoder()
 
-    def __emit(self, name, obj_id, cls=None):
+    def __call__(self, name, obj_id, cls=None):
+        if self.sio is None:
+            warnings.warn("NotificationManager was called before a setup was performed!")
+            return
+
         if cls is not None:
             assert isinstance(obj_id, int), \
                 f"{cls.__name__} ID must be an integer, but was {type(obj_id)} ({obj_id=})!"
@@ -34,155 +49,187 @@ class NotificationManager:
         enc = self.json.default(obj)
         self.sio.emit(name, enc)
 
-    def create_job(self, created_job: Job):
-        """
-        fire create-job event
-
-        :param created_job:
-        :return:
-        """
-        self.__emit('create-job', created_job)
-
-    def edit_job(self, edited_job: Job):
-        """
-        fire edit-job event
-
-        :param edited_job:
-        :return:
-        """
-        self.__emit('edit-job', edited_job)
-
-    def remove_job(self, removed_job: Job):
-        """
-        fire remove-job event
-
-        :param removed_job:
-        :return:
-        """
-        self.__emit('remove-job', removed_job)
-
-    def create_model(self, created_model_id: int):
-        """
-        fire create-model event
-
-        :param created_model:
-        :return:
-        """
-        self.__emit('create-model', created_model_id, Model)
-
-    def remove_model(self, model_serialized: dict):
-        """
-        fire remove-model event
-
-        :param removed_model:
-        :return:
-        """
-        self.__emit('remove-model', model_serialized)
-
-    def create_project(self, created_project_id: int):
-        """
-        fire create-project event
-
-        :param created_project:
-        :return:
-        """
-        self.__emit('create-project', created_project_id, Project)
-
-    def remove_project(self, project_serialized: dict):
-        """
-        fire remove-project event
-
-        :param removed_project:
-        :return:
-        """
-        self.__emit('remove-project', project_serialized)
-
-    def edit_project(self, edited_project_id: int):
-        """
-        fire edit-project event
-
-        :param edited_project:
-        :return:
-        """
-        self.__emit('edit-project', edited_project_id, Project)
-
-    def create_label(self, created_label_id: int):
-        """
-        fire create-label event
-
-        :param created_label:
-        :return:
-        """
-        self.__emit('create-label', created_label_id, Label)
-
-    def edit_label(self, edited_label_id: int):
-        """
-        fire edit-label event
-
-        :param edited_label:
-        :return:
-        """
-        self.__emit('edit-label', edited_label_id, Label)
-
-    def remove_label(self, label_serialized: dict):
-        """
-        fire remove-label event
-
-        :param removed_label:
-        :return:
-        """
-        self.__emit('remove-label', label_serialized)
-
-    def create_file(self, created_file_id: int):
-        """
-        fire create-file event
-
-        :param created_file:
-        :return:
-        """
-        self.__emit('create-file', created_file_id, File)
-
-    def edit_file(self, edited_file_id: int):
-        """
-        fire edit-file event
-
-        :param edited_file:
-        :return:
-        """
-        self.__emit('edit-file', edited_file_id, File)
-
-    def remove_file(self, file_serialized: dict):
-        """
-        fire remove-file event
-
-        :param removed_file:
-        :return:
-        """
-        self.__emit('remove-file', file_serialized)
-
-    def create_result(self, created_result_id: int):
-        """
-        fire create-result event
-
-        :param created_result:
-        :return:
-        """
-        self.__emit('create-result', created_result_id, Result)
-
-    def edit_result(self, edited_result_id: int):
-        """
-        fire edit-result event
-
-        :param edited_result:
-        :return:
-        """
-        self.__emit('edit-result', edited_result_id, Result)
-
-    def remove_result(self, result_serialized: dict):
-        """
-        fire remove-result event
-
-        :param removed_result:
-        :return:
-        """
-        self.__emit('remove-result', result_serialized)
+
+    def _new_signal(self, signal_name, suffix, *args, **kwargs):
+        return partial(self, f'{signal_name}-{suffix}', *args, **kwargs)
+
+
+    def create(self, suffix, *args, **kwargs) -> Callable:
+        return self._new_signal('create', suffix, *args, **kwargs)
+
+
+    def edit(self, suffix, *args, **kwargs) -> Callable:
+        return self._new_signal('edit', suffix, *args, **kwargs)
+
+
+    def remove(self, suffix, *args, **kwargs) -> Callable:
+        return self._new_signal('remove', suffix, *args, **kwargs)
+
+
+    @classmethod
+    def removed(kls, suffix, *args, **kwargs):
+        return kls().remove(suffix)(*args, **kwargs)
+
+
+    @classmethod
+    def edited(kls, suffix, *args, **kwargs):
+        return kls().edit(suffix)(*args, **kwargs)
+
+
+    @classmethod
+    def created(kls, suffix, *args, **kwargs):
+        return kls().create(suffix)(*args, **kwargs)
+
+
+    # def create_job(self, created_job: Job):
+    #     """
+    #     fire create-job event
+
+    #     :param created_job:
+    #     :return:
+    #     """
+    #     self.__emit('create-job', created_job)
+
+    # def edit_job(self, edited_job: Job):
+    #     """
+    #     fire edit-job event
+
+    #     :param edited_job:
+    #     :return:
+    #     """
+    #     self.__emit('edit-job', edited_job)
+
+    # def remove_job(self, removed_job: Job):
+    #     """
+    #     fire remove-job event
+
+    #     :param removed_job:
+    #     :return:
+    #     """
+    #     self.__emit('remove-job', removed_job)
+
+    # def create_model(self, created_model_id: int):
+    #     """
+    #     fire create-model event
+
+    #     :param created_model:
+    #     :return:
+    #     """
+    #     self.__emit('create-model', created_model_id, Model)
+
+    # def remove_model(self, model_serialized: dict):
+    #     """
+    #     fire remove-model event
+
+    #     :param removed_model:
+    #     :return:
+    #     """
+    #     self.__emit('remove-model', model_serialized)
+
+    # def create_project(self, created_project_id: int):
+    #     """
+    #     fire create-project event
+
+    #     :param created_project:
+    #     :return:
+    #     """
+    #     self.__emit('create-project', created_project_id, Project)
+
+    # def remove_project(self, project_serialized: dict):
+    #     """
+    #     fire remove-project event
+
+    #     :param removed_project:
+    #     :return:
+    #     """
+    #     self.__emit('remove-project', project_serialized)
+
+    # def edit_project(self, edited_project_id: int):
+    #     """
+    #     fire edit-project event
+
+    #     :param edited_project:
+    #     :return:
+    #     """
+    #     self.__emit('edit-project', edited_project_id, Project)
+
+    # def create_label(self, created_label_id: int):
+    #     """
+    #     fire create-label event
+
+    #     :param created_label:
+    #     :return:
+    #     """
+    #     self.__emit('create-label', created_label_id, Label)
+
+    # def edit_label(self, edited_label_id: int):
+    #     """
+    #     fire edit-label event
+
+    #     :param edited_label:
+    #     :return:
+    #     """
+    #     self.__emit('edit-label', edited_label_id, Label)
+
+    # def remove_label(self, label_serialized: dict):
+    #     """
+    #     fire remove-label event
+
+    #     :param removed_label:
+    #     :return:
+    #     """
+    #     self.__emit('remove-label', label_serialized)
+
+    # def create_file(self, created_file_id: int):
+    #     """
+    #     fire create-file event
+
+    #     :param created_file:
+    #     :return:
+    #     """
+    #     self.__emit('create-file', created_file_id, File)
+
+    # def edit_file(self, edited_file_id: int):
+    #     """
+    #     fire edit-file event
+
+    #     :param edited_file:
+    #     :return:
+    #     """
+    #     self.__emit('edit-file', edited_file_id, File)
+
+    # def remove_file(self, file_serialized: dict):
+    #     """
+    #     fire remove-file event
+
+    #     :param removed_file:
+    #     :return:
+    #     """
+    #     self.__emit('remove-file', file_serialized)
+
+    # def create_result(self, created_result_id: int):
+    #     """
+    #     fire create-result event
+
+    #     :param created_result:
+    #     :return:
+    #     """
+    #     self.__emit('create-result', created_result_id, Result)
+
+    # def edit_result(self, edited_result_id: int):
+    #     """
+    #     fire edit-result event
+
+    #     :param edited_result:
+    #     :return:
+    #     """
+    #     self.__emit('edit-result', edited_result_id, Result)
+
+    # def remove_result(self, result_serialized: dict):
+    #     """
+    #     fire remove-result event
+
+    #     :param removed_result:
+    #     :return:
+    #     """
+    #     self.__emit('remove-result', result_serialized)

+ 4 - 4
pycs/interfaces/MediaFile.py

@@ -35,7 +35,7 @@ class MediaFile:
         :param reference: use None to remove this file's collection
         """
         self.__file.set_collection_by_reference(reference)
-        self.__notifications.add(self.__notifications.notifications.edit_file, self.__file.id)
+        self.__notifications.add("edit", "file", self.__file.id, cls=File)
 
     def set_image_label(self, label: Union[int, MediaLabel], frame: int = None):
         """
@@ -53,7 +53,7 @@ class MediaFile:
             data = None
 
         created = self.__file.create_result('pipeline', 'labeled-image', label, data)
-        self.__notifications.add(self.__notifications.notifications.create_result, created.id)
+        self.__notifications.add("create", "result", created.id, cls=Result)
 
     def add_bounding_box(self, x: float, y: float, w: float, h: float,
                          label: Union[int, MediaLabel] = None, frame: int = None):
@@ -80,7 +80,7 @@ class MediaFile:
             label = label.id
 
         created = self.__file.create_result('pipeline', 'bounding-box', label, result)
-        self.__notifications.add(self.__notifications.notifications.create_result, created.id)
+        self.__notifications.add("create", "result", created.id, cls=Result)
 
     def remove_predictions(self):
         """
@@ -88,7 +88,7 @@ class MediaFile:
         """
         removed = self.__file.remove_results(origin='pipeline')
         for r in removed:
-            self.__notifications.add(self.__notifications.notifications.remove_result, r.serialize())
+            self.__notifications.add("remove", "result", r.serialize())
 
     def __get_results(self, origin: str) -> List[Union[MediaImageLabel, MediaBoundingBox]]:
         def map_r(result: Result) -> Union[MediaImageLabel, MediaBoundingBox]:

+ 9 - 6
pycs/jobs/JobRunner.py

@@ -47,13 +47,16 @@ class JobRunner(GreenWorker, Singleton):
 
         self.__listeners = defaultdict(Callbacks)
 
-    def init_notifications(self, nm: NotificationManager):
+        self.init_notifications()
 
-        self.on("create",      nm.create_job)
-        self.on("start",       nm.edit_job)
-        self.on("progress",    nm.edit_job)
-        self.on("finish",      nm.edit_job)
-        self.on("remove",      nm.remove_job)
+    def init_notifications(self):
+        nm = NotificationManager()
+
+        self.on("create",      nm.create("job"))
+        self.on("start",       nm.edit("job"))
+        self.on("progress",    nm.edit("job"))
+        self.on("finish",      nm.edit("job"))
+        self.on("remove",      nm.remove("job"))
 
     @classmethod
     def Run(cls, *args, **kwargs):

+ 1 - 1
pycs/util/green_worker.py

@@ -61,7 +61,7 @@ class GreenWorker(abc.ABC):
             continue
 
     def __log(self, log_func, msg):
-        log_func(f"[{self.ident}] {self.__class__.__name__}: {msg}")
+        log_func(f"[0x{self.ident:x}] {self.__class__.__name__}: {msg}")
 
     def debug(self, msg):
         self.__log(app.logger.debug, msg)