Ver código fonte

deleted Database class. Reworked all view, so that they don't need a database instance

Dimitri Korsch 3 anos atrás
pai
commit
d7076a97ab
68 arquivos alterados com 1098 adições e 1158 exclusões
  1. 3 14
      app.py
  2. 1 0
      migrations/README
  3. 50 0
      migrations/alembic.ini
  4. 91 0
      migrations/env.py
  5. 24 0
      migrations/script.py.mako
  6. 125 0
      migrations/versions/b03df3e31b8d_.py
  7. 8 2
      pycs/__init__.py
  8. 5 4
      pycs/database/Collection.py
  9. 0 201
      pycs/database/Database.py
  10. 9 0
      pycs/database/File.py
  11. 18 4
      pycs/database/Label.py
  12. 2 2
      pycs/database/LabelProvider.py
  13. 6 4
      pycs/database/Model.py
  14. 21 15
      pycs/database/Project.py
  15. 6 2
      pycs/database/base.py
  16. 0 49
      pycs/database/discovery/LabelProviderDiscovery.py
  17. 0 32
      pycs/database/discovery/ModelDiscovery.py
  18. 0 0
      pycs/database/discovery/__init__.py
  19. 6 3
      pycs/database/util/JSONEncoder.py
  20. 1 1
      pycs/database/util/__init__.py
  21. 108 102
      pycs/frontend/WebServer.py
  22. 2 6
      pycs/frontend/endpoints/ListLabelProviders.py
  23. 2 6
      pycs/frontend/endpoints/ListModels.py
  24. 2 6
      pycs/frontend/endpoints/ListProjects.py
  25. 6 4
      pycs/frontend/endpoints/additional/FolderInformation.py
  26. 6 18
      pycs/frontend/endpoints/data/GetFile.py
  27. 4 9
      pycs/frontend/endpoints/data/GetPreviousAndNextFile.py
  28. 5 14
      pycs/frontend/endpoints/data/GetResizedFile.py
  29. 13 20
      pycs/frontend/endpoints/data/RemoveFile.py
  30. 24 20
      pycs/frontend/endpoints/data/UploadFile.py
  31. 7 5
      pycs/frontend/endpoints/jobs/RemoveJob.py
  32. 18 15
      pycs/frontend/endpoints/labels/CreateLabel.py
  33. 14 16
      pycs/frontend/endpoints/labels/EditLabelName.py
  34. 14 16
      pycs/frontend/endpoints/labels/EditLabelParent.py
  35. 5 13
      pycs/frontend/endpoints/labels/ListLabelTree.py
  36. 5 13
      pycs/frontend/endpoints/labels/ListLabels.py
  37. 23 22
      pycs/frontend/endpoints/labels/RemoveLabel.py
  38. 11 14
      pycs/frontend/endpoints/pipelines/FitModel.py
  39. 9 13
      pycs/frontend/endpoints/pipelines/PredictFile.py
  40. 66 63
      pycs/frontend/endpoints/pipelines/PredictModel.py
  41. 73 64
      pycs/frontend/endpoints/projects/CreateProject.py
  42. 16 17
      pycs/frontend/endpoints/projects/EditProjectDescription.py
  43. 17 18
      pycs/frontend/endpoints/projects/EditProjectName.py
  44. 37 34
      pycs/frontend/endpoints/projects/ExecuteExternalStorage.py
  45. 20 24
      pycs/frontend/endpoints/projects/ExecuteLabelProvider.py
  46. 5 13
      pycs/frontend/endpoints/projects/GetProjectModel.py
  47. 0 40
      pycs/frontend/endpoints/projects/ListCollections.py
  48. 24 0
      pycs/frontend/endpoints/projects/ListProjectCollections.py
  49. 14 15
      pycs/frontend/endpoints/projects/ListProjectFiles.py
  50. 16 28
      pycs/frontend/endpoints/projects/RemoveProject.py
  51. 6 11
      pycs/frontend/endpoints/results/ConfirmResult.py
  52. 43 33
      pycs/frontend/endpoints/results/CreateResult.py
  53. 14 16
      pycs/frontend/endpoints/results/EditResultData.py
  54. 15 17
      pycs/frontend/endpoints/results/EditResultLabel.py
  55. 5 9
      pycs/frontend/endpoints/results/GetProjectResults.py
  56. 3 11
      pycs/frontend/endpoints/results/GetResults.py
  57. 10 13
      pycs/frontend/endpoints/results/RemoveResult.py
  58. 14 16
      pycs/frontend/endpoints/results/ResetResults.py
  59. 4 13
      pycs/frontend/endpoints/results/ResultAsCrop.py
  60. 14 6
      pycs/frontend/util/JSONEncoder.py
  61. 5 8
      pycs/interfaces/MediaFile.py
  62. 3 3
      pycs/interfaces/MediaFileList.py
  63. 1 1
      pycs/interfaces/MediaLabel.py
  64. 9 12
      pycs/interfaces/MediaStorage.py
  65. 2 2
      pycs/jobs/Job.py
  66. 1 1
      pycs/jobs/JobRunner.py
  67. 2 0
      settings.json
  68. 5 5
      test/test_database.py

+ 3 - 14
app.py

@@ -1,20 +1,9 @@
 #!/usr/bin/env python
 #!/usr/bin/env python
 
 
-import os
-import json
-
+from pycs import app
+from pycs import settings
 from pycs.frontend.WebServer import WebServer
 from pycs.frontend.WebServer import WebServer
 
 
-print('- Loading settings')
-with open('settings.json') as file:
-    settings = json.load(file)
-
-# create projects folder
-if not os.path.exists('projects/'):
-    os.mkdir('projects/')
-
-# start web server
-server = WebServer(settings)
-
 if __name__ == '__main__':
 if __name__ == '__main__':
+    server = WebServer(app, settings)
     server.run()
     server.run()

+ 1 - 0
migrations/README

@@ -0,0 +1 @@
+Generic single-database configuration.

+ 50 - 0
migrations/alembic.ini

@@ -0,0 +1,50 @@
+# A generic, single database configuration.
+
+[alembic]
+# template used to generate migration files
+# file_template = %%(rev)s_%%(slug)s
+
+# set to 'true' to run the environment during
+# the 'revision' command, regardless of autogenerate
+# revision_environment = false
+
+
+# Logging configuration
+[loggers]
+keys = root,sqlalchemy,alembic,flask_migrate
+
+[handlers]
+keys = console
+
+[formatters]
+keys = generic
+
+[logger_root]
+level = WARN
+handlers = console
+qualname =
+
+[logger_sqlalchemy]
+level = WARN
+handlers =
+qualname = sqlalchemy.engine
+
+[logger_alembic]
+level = INFO
+handlers =
+qualname = alembic
+
+[logger_flask_migrate]
+level = INFO
+handlers =
+qualname = flask_migrate
+
+[handler_console]
+class = StreamHandler
+args = (sys.stderr,)
+level = INFO
+formatter = generic
+
+[formatter_generic]
+format = %(levelname)-5.5s [%(name)s] %(message)s
+datefmt = %H:%M:%S

+ 91 - 0
migrations/env.py

@@ -0,0 +1,91 @@
+from __future__ import with_statement
+
+import logging
+from logging.config import fileConfig
+
+from flask import current_app
+
+from alembic import context
+
+# this is the Alembic Config object, which provides
+# access to the values within the .ini file in use.
+config = context.config
+
+# Interpret the config file for Python logging.
+# This line sets up loggers basically.
+fileConfig(config.config_file_name)
+logger = logging.getLogger('alembic.env')
+
+# add your model's MetaData object here
+# for 'autogenerate' support
+# from myapp import mymodel
+# target_metadata = mymodel.Base.metadata
+config.set_main_option(
+    'sqlalchemy.url',
+    str(current_app.extensions['migrate'].db.get_engine().url).replace(
+        '%', '%%'))
+target_metadata = current_app.extensions['migrate'].db.metadata
+
+# other values from the config, defined by the needs of env.py,
+# can be acquired:
+# my_important_option = config.get_main_option("my_important_option")
+# ... etc.
+
+
+def run_migrations_offline():
+    """Run migrations in 'offline' mode.
+
+    This configures the context with just a URL
+    and not an Engine, though an Engine is acceptable
+    here as well.  By skipping the Engine creation
+    we don't even need a DBAPI to be available.
+
+    Calls to context.execute() here emit the given string to the
+    script output.
+
+    """
+    url = config.get_main_option("sqlalchemy.url")
+    context.configure(
+        url=url, target_metadata=target_metadata, literal_binds=True
+    )
+
+    with context.begin_transaction():
+        context.run_migrations()
+
+
+def run_migrations_online():
+    """Run migrations in 'online' mode.
+
+    In this scenario we need to create an Engine
+    and associate a connection with the context.
+
+    """
+
+    # this callback is used to prevent an auto-migration from being generated
+    # when there are no changes to the schema
+    # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html
+    def process_revision_directives(context, revision, directives):
+        if getattr(config.cmd_opts, 'autogenerate', False):
+            script = directives[0]
+            if script.upgrade_ops.is_empty():
+                directives[:] = []
+                logger.info('No changes in schema detected.')
+
+    connectable = current_app.extensions['migrate'].db.get_engine()
+
+    with connectable.connect() as connection:
+        context.configure(
+            connection=connection,
+            target_metadata=target_metadata,
+            process_revision_directives=process_revision_directives,
+            **current_app.extensions['migrate'].configure_args
+        )
+
+        with context.begin_transaction():
+            context.run_migrations()
+
+
+if context.is_offline_mode():
+    run_migrations_offline()
+else:
+    run_migrations_online()

+ 24 - 0
migrations/script.py.mako

@@ -0,0 +1,24 @@
+"""${message}
+
+Revision ID: ${up_revision}
+Revises: ${down_revision | comma,n}
+Create Date: ${create_date}
+
+"""
+from alembic import op
+import sqlalchemy as sa
+${imports if imports else ""}
+
+# revision identifiers, used by Alembic.
+revision = ${repr(up_revision)}
+down_revision = ${repr(down_revision)}
+branch_labels = ${repr(branch_labels)}
+depends_on = ${repr(depends_on)}
+
+
+def upgrade():
+    ${upgrades if upgrades else "pass"}
+
+
+def downgrade():
+    ${downgrades if downgrades else "pass"}

+ 125 - 0
migrations/versions/b03df3e31b8d_.py

@@ -0,0 +1,125 @@
+"""empty message
+
+Revision ID: b03df3e31b8d
+Revises: 
+Create Date: 2021-08-11 12:46:17.757283
+
+"""
+from alembic import op
+import sqlalchemy as sa
+
+
+# revision identifiers, used by Alembic.
+revision = 'b03df3e31b8d'
+down_revision = None
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+    # ### commands auto generated by Alembic - please adjust! ###
+    op.create_table('label_provider',
+    sa.Column('id', sa.Integer(), nullable=False),
+    sa.Column('name', sa.String(), nullable=False),
+    sa.Column('description', sa.String(), nullable=True),
+    sa.Column('root_folder', sa.String(), nullable=False),
+    sa.Column('configuration_file', sa.String(), nullable=False),
+    sa.PrimaryKeyConstraint('id'),
+    sa.UniqueConstraint('root_folder', 'configuration_file')
+    )
+    op.create_table('model',
+    sa.Column('id', sa.Integer(), nullable=False),
+    sa.Column('name', sa.String(), nullable=False),
+    sa.Column('description', sa.String(), nullable=True),
+    sa.Column('root_folder', sa.String(), nullable=False),
+    sa.Column('supports_encoded', sa.String(), nullable=False),
+    sa.PrimaryKeyConstraint('id'),
+    sa.UniqueConstraint('root_folder')
+    )
+    op.create_table('project',
+    sa.Column('id', sa.Integer(), nullable=False),
+    sa.Column('name', sa.String(), nullable=False),
+    sa.Column('description', sa.String(), nullable=True),
+    sa.Column('created', sa.DateTime(), nullable=False),
+    sa.Column('model_id', sa.Integer(), nullable=True),
+    sa.Column('label_provider_id', sa.Integer(), nullable=True),
+    sa.Column('root_folder', sa.String(), nullable=False),
+    sa.Column('external_data', sa.Boolean(), nullable=False),
+    sa.Column('data_folder', sa.String(), nullable=False),
+    sa.ForeignKeyConstraint(['label_provider_id'], ['label_provider.id'], ondelete='SET NULL'),
+    sa.ForeignKeyConstraint(['model_id'], ['model.id'], ondelete='SET NULL'),
+    sa.PrimaryKeyConstraint('id'),
+    sa.UniqueConstraint('root_folder')
+    )
+    op.create_index(op.f('ix_project_created'), 'project', ['created'], unique=False)
+    op.create_table('collection',
+    sa.Column('id', sa.Integer(), nullable=False),
+    sa.Column('name', sa.String(), nullable=False),
+    sa.Column('project_id', sa.Integer(), nullable=False),
+    sa.Column('reference', sa.String(), nullable=False),
+    sa.Column('description', sa.String(), nullable=True),
+    sa.Column('position', sa.Integer(), nullable=False),
+    sa.Column('autoselect', sa.Boolean(), nullable=False),
+    sa.ForeignKeyConstraint(['project_id'], ['project.id'], ondelete='CASCADE'),
+    sa.PrimaryKeyConstraint('id'),
+    sa.UniqueConstraint('project_id', 'reference')
+    )
+    op.create_table('label',
+    sa.Column('name', sa.String(), nullable=False),
+    sa.Column('id', sa.Integer(), nullable=False),
+    sa.Column('project_id', sa.Integer(), nullable=False),
+    sa.Column('parent_id', sa.Integer(), nullable=True),
+    sa.Column('created', sa.DateTime(), nullable=False),
+    sa.Column('reference', sa.String(), nullable=True),
+    sa.ForeignKeyConstraint(['parent_id'], ['label.id'], ondelete='SET NULL'),
+    sa.ForeignKeyConstraint(['project_id'], ['project.id'], ondelete='CASCADE'),
+    sa.PrimaryKeyConstraint('id'),
+    sa.UniqueConstraint('project_id', 'reference')
+    )
+    op.create_index(op.f('ix_label_created'), 'label', ['created'], unique=False)
+    op.create_table('file',
+    sa.Column('id', sa.Integer(), nullable=False),
+    sa.Column('name', sa.String(), nullable=False),
+    sa.Column('uuid', sa.String(), nullable=False),
+    sa.Column('extension', sa.String(), nullable=False),
+    sa.Column('type', sa.String(), nullable=False),
+    sa.Column('size', sa.String(), nullable=False),
+    sa.Column('created', sa.DateTime(), nullable=False),
+    sa.Column('path', sa.String(), nullable=False),
+    sa.Column('frames', sa.Integer(), nullable=True),
+    sa.Column('fps', sa.Float(), nullable=True),
+    sa.Column('project_id', sa.Integer(), nullable=False),
+    sa.Column('collection_id', sa.Integer(), nullable=True),
+    sa.ForeignKeyConstraint(['collection_id'], ['collection.id'], ondelete='SET NULL'),
+    sa.ForeignKeyConstraint(['project_id'], ['project.id'], ondelete='CASCADE'),
+    sa.PrimaryKeyConstraint('id'),
+    sa.UniqueConstraint('project_id', 'path')
+    )
+    op.create_index(op.f('ix_file_created'), 'file', ['created'], unique=False)
+    op.create_table('result',
+    sa.Column('id', sa.Integer(), nullable=False),
+    sa.Column('file_id', sa.Integer(), nullable=False),
+    sa.Column('origin', sa.String(), nullable=False),
+    sa.Column('type', sa.String(), nullable=False),
+    sa.Column('label_id', sa.Integer(), nullable=True),
+    sa.Column('data_encoded', sa.String(), nullable=True),
+    sa.ForeignKeyConstraint(['file_id'], ['file.id'], ondelete='CASCADE'),
+    sa.ForeignKeyConstraint(['label_id'], ['label.id'], ondelete='SET NULL'),
+    sa.PrimaryKeyConstraint('id')
+    )
+    # ### end Alembic commands ###
+
+
+def downgrade():
+    # ### commands auto generated by Alembic - please adjust! ###
+    op.drop_table('result')
+    op.drop_index(op.f('ix_file_created'), table_name='file')
+    op.drop_table('file')
+    op.drop_index(op.f('ix_label_created'), table_name='label')
+    op.drop_table('label')
+    op.drop_table('collection')
+    op.drop_index(op.f('ix_project_created'), table_name='project')
+    op.drop_table('project')
+    op.drop_table('model')
+    op.drop_table('label_provider')
+    # ### end Alembic commands ###

+ 8 - 2
pycs/__init__.py

@@ -1,6 +1,8 @@
 import json
 import json
+import os
 
 
 from pathlib import Path
 from pathlib import Path
+from munch import munchify
 
 
 from flask import Flask
 from flask import Flask
 from flask_migrate import Migrate
 from flask_migrate import Migrate
@@ -10,11 +12,15 @@ from sqlalchemy.engine import Engine
 
 
 print('- Loading settings')
 print('- Loading settings')
 with open('settings.json') as file:
 with open('settings.json') as file:
-    settings = json.load(file)
+    settings = munchify(json.load(file))
 
 
 
 
+# create projects folder
+if not os.path.exists(settings.projects_folder):
+    os.mkdir(settings.projects_folder)
+
 app = Flask(__name__)
 app = Flask(__name__)
-app.config["SQLALCHEMY_DATABASE_URI"] = f"sqlite:///{Path.cwd() / settings['database']}"
+app.config["SQLALCHEMY_DATABASE_URI"] = f"sqlite:///{Path.cwd() / settings.database}"
 app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
 app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
 
 
 @event.listens_for(Engine, "connect")
 @event.listens_for(Engine, "connect")

+ 5 - 4
pycs/database/Collection.py

@@ -1,5 +1,7 @@
+from __future__ import annotations
+
 from contextlib import closing
 from contextlib import closing
-from typing import Iterator
+from typing import List
 
 
 from pycs import db
 from pycs import db
 from pycs.database.base import NamedBaseModel
 from pycs.database.base import NamedBaseModel
@@ -41,8 +43,6 @@ class Collection(NamedBaseModel):
         "autoselect",
         "autoselect",
     )
     )
 
 
-    def count_files(self) -> int:
-        return self.files.count()
 
 
     def get_files(self, offset: int = 0, limit: int = -1):
     def get_files(self, offset: int = 0, limit: int = -1):
         """
         """
@@ -55,6 +55,7 @@ class Collection(NamedBaseModel):
         from pycs.database.File import File
         from pycs.database.File import File
         return self.files.order_by(File.id).offset(offset).limit(limit)
         return self.files.order_by(File.id).offset(offset).limit(limit)
 
 
+
     @staticmethod
     @staticmethod
     def update_autoselect(collections: List[Collection]) -> List[Collection]:
     def update_autoselect(collections: List[Collection]) -> List[Collection]:
         """ disable autoselect if there are no elements in the collection """
         """ disable autoselect if there are no elements in the collection """
@@ -68,7 +69,7 @@ class Collection(NamedBaseModel):
             if found:
             if found:
                 collection.autoselect = False
                 collection.autoselect = False
 
 
-            elif collection.count_files() == 0:
+            elif collection.files.count() == 0:
                 collection.autoselect = False
                 collection.autoselect = False
                 found = True
                 found = True
 
 

+ 0 - 201
pycs/database/Database.py

@@ -1,201 +0,0 @@
-import sqlite3
-from contextlib import closing
-from time import time
-from typing import Optional, Iterator
-
-from pycs import db
-from pycs.database.Collection import Collection
-from pycs.database.File import File
-from pycs.database.LabelProvider import LabelProvider
-from pycs.database.Model import Model
-from pycs.database.Project import Project
-from pycs.database.Result import Result
-from pycs.database.discovery.LabelProviderDiscovery import discover as discover_label_providers
-from pycs.database.discovery.ModelDiscovery import discover as discover_models
-
-
-class Database:
-    """
-    opens an sqlite database and allows to access several objects
-    """
-
-    def __init__(self, discovery: bool = True):
-        """
-        wrapper for some DB-related runctions. TODO: remove it!
-
-        """
-
-        if discovery:
-            discover_models()
-            discover_label_providers()
-
-    def close(self):
-        """
-        close database file
-        """
-        return
-
-    def copy(self):
-        """
-        Create a copy of this database object. This can be used to access the database
-        from another thread. Table initialization and model and label provider discovery is
-        disabled to speedup this function.
-
-        :return: Database
-        """
-        return self
-
-    def commit(self):
-        """
-        commit changes
-        """
-        db.session.commit()
-
-    def get_object_by_id(self, table_name: str, identifier: int, cls):
-        """
-        create an object from cls and a row fetched from table_name and
-        identified by the identifier
-
-        :param table_name: table name
-        :param identifier: unique identifier
-        :param cls: class that is used to create the object
-        :return: object of type cls
-        """
-        with closing(self.con.cursor()) as cursor:
-            cursor.execute(f'SELECT * FROM {table_name} WHERE id = ?', [identifier])
-            row = cursor.fetchone()
-
-            if row is not None:
-                return cls(self, row)
-
-            return None
-
-    def get_objects(self, table_name: str, cls):
-        """
-        get a list of all available objects in the table
-
-        :param table_name: table name
-        :param cls: class that is used to create the objects
-        :return: list of object of type cls
-        """
-
-        with closing(self.con.cursor()) as cursor:
-            cursor.execute(f'SELECT * FROM {table_name}')
-            for row in cursor:
-                yield cls(self, row)
-
-
-    def models(self) -> Iterator[Model]:
-        """
-        get a list of all available models
-
-        :return: iterator of models
-        """
-        return self.get_objects("models", Model)
-
-    def model(self, identifier: int) -> Optional[Model]:
-        """
-        get a model using its unique identifier
-
-        :param identifier: unique identifier
-        :return: model
-        """
-        return self.get_object_by_id("models", identifier, Model)
-
-    def label_providers(self) -> Iterator[LabelProvider]:
-        """
-        get a list of all available label providers
-
-        :return: iterator over label providers
-        """
-        return self.get_objects("label_providers", LabelProvider)
-
-    def label_provider(self, identifier: int) -> Optional[LabelProvider]:
-        """
-        get a label provider using its unique identifier
-
-        :param identifier: unique identifier
-        :return: label provider
-        """
-        return self.get_object_by_id("label_providers", identifier, LabelProvider)
-
-    def projects(self) -> Iterator[Project]:
-        """
-        get a list of all available projects
-
-        :return: iterator over projects
-        """
-        return self.get_objects("projects", Project)
-
-    def project(self, identifier: int) -> Optional[Project]:
-        """
-        get a project using its unique identifier
-
-        :param identifier: unique identifier
-        :return: project
-        """
-        return self.get_object_by_id("projects", identifier, Project)
-
-    def create_project(self,
-                       name: str,
-                       description: str,
-                       model: Model,
-                       label_provider: Optional[LabelProvider],
-                       root_folder: str,
-                       external_data: bool,
-                       data_folder: str):
-        """
-        insert a project into the database
-
-        :param name: project name
-        :param description: project description
-        :param model: used model
-        :param label_provider: used label provider (optional)
-        :param root_folder: path to project folder
-        :param external_data: whether an external data directory is used
-        :param data_folder: path to data folder
-        :return: created project
-        """
-        # prepare some values
-        created = int(time())
-        label_provider_id = label_provider.identifier if label_provider is not None else None
-
-        # insert statement
-        with closing(self.con.cursor()) as cursor:
-            cursor.execute('''
-                INSERT INTO projects (
-                    name, description, created, model, label_provider, root_folder, external_data, 
-                    data_folder
-                )
-                VALUES (?, ?, ?, ?, ?, ?, ?, ?)
-            ''', (name, description, created, model.identifier, label_provider_id, root_folder,
-                  external_data, data_folder))
-
-            return self.project(cursor.lastrowid)
-
-    def collection(self, identifier: int) -> Optional[Collection]:
-        """
-        get a collection using its unique identifier
-
-        :param identifier: unique identifier
-        :return: collection
-        """
-        return self.get_object_by_id("collections", identifier, Collection)
-
-    def file(self, identifier) -> Optional[File]:
-        """
-        get a file using its unique identifier
-
-        :param identifier: unique identifier
-        :return: file
-        """
-        return self.get_object_by_id("files", identifier, File)
-
-    def result(self, identifier) -> Optional[Result]:
-        """
-        get a result using its unique identifier
-
-        :param identifier: unique identifier
-        :return: result
-        """
-        return self.get_object_by_id("results", identifier, Result)

+ 9 - 0
pycs/database/File.py

@@ -1,6 +1,8 @@
 from __future__ import annotations
 from __future__ import annotations
 
 
+import os
 import typing as T
 import typing as T
+import warnings
 
 
 from datetime import datetime
 from datetime import datetime
 from pathlib import Path
 from pathlib import Path
@@ -77,6 +79,13 @@ class File(NamedBaseModel):
 
 
         return str(Path.cwd() / path)
         return str(Path.cwd() / path)
 
 
+    def delete(self, commit: bool = True):
+        super().delete(commit=commit)
+        # remove file from folder
+        os.remove(self.path)
+        # TODO: remove temp files
+        warnings.warn("Temporary files may still exist!")
+
     @commit_on_return
     @commit_on_return
     def set_collection(self, collection_id: T.Optional[int]):
     def set_collection(self, collection_id: T.Optional[int]):
         """
         """

+ 18 - 4
pycs/database/Label.py

@@ -1,3 +1,5 @@
+from __future__ import annotations
+
 from datetime import datetime
 from datetime import datetime
 
 
 from pycs import db
 from pycs import db
@@ -63,14 +65,26 @@ class Label(NamedBaseModel):
     )
     )
 
 
     @commit_on_return
     @commit_on_return
-    def set_parent(self, parent_id: int) -> None:
+    def set_parent(self, parent: T.Optional[T.Union[int, str, Label]] = None) -> None:
+
         """
         """
         set this labels parent
         set this labels parent
 
 
-        :param parent_id: parent's id
+        :param parent: parent label. Can be a reference, an id or a Label instance
         :return:
         :return:
         """
         """
-        if not compare_children(self, parent_id):
-            raise ValueError('Cyclic relationship detected!')
+        parent_id = None
+        if parent is not None:
+            if isinstance(parent, Label):
+                parent_id = parent.id
+
+            elif isinstance(parent, str):
+                parent_id = Label.query.filter(Label.reference == parent).one().id
+
+            elif isinstance(parent, int):
+                parent_id = parent
+
+            if not compare_children(self, parent_id):
+                raise ValueError('Cyclic relationship detected!')
 
 
         self.parent_id = parent_id
         self.parent_id = parent_id

+ 2 - 2
pycs/database/LabelProvider.py

@@ -14,7 +14,7 @@ class LabelProvider(NamedBaseModel):
     """
     """
 
 
     description = db.Column(db.String)
     description = db.Column(db.String)
-    root_folder = db.Column(db.String, nullable=False, unique=True)
+    root_folder = db.Column(db.String, nullable=False)
     configuration_file = db.Column(db.String, nullable=False)
     configuration_file = db.Column(db.String, nullable=False)
 
 
     # relationships to other models
     # relationships to other models
@@ -48,7 +48,7 @@ class LabelProvider(NamedBaseModel):
             # returns None if not present
             # returns None if not present
             provider.description = config.get('description')
             provider.description = config.get('description')
 
 
-            db.session.flush()
+            provider.flush()
         db.session.commit()
         db.session.commit()
 
 
     @property
     @property

+ 6 - 4
pycs/database/Model.py

@@ -40,13 +40,15 @@ class Model(NamedBaseModel):
             description = config.get('description', None)
             description = config.get('description', None)
             supports = config['supports']
             supports = config['supports']
 
 
-            model, _ = cls.get_or_create(root_folder=str(folder))
+            model, is_new = cls.get_or_create(root_folder=str(folder))
+
+            assert model is not None
 
 
             model.name = name
             model.name = name
             model.description = description
             model.description = description
             model.supports = supports
             model.supports = supports
 
 
-            db.session.flush()
+            model.flush()
         db.session.commit()
         db.session.commit()
 
 
     @property
     @property
@@ -65,9 +67,9 @@ class Model(NamedBaseModel):
             raise ValueError(f"Not supported type: {type(value)}")
             raise ValueError(f"Not supported type: {type(value)}")
 
 
     @commit_on_return
     @commit_on_return
-    def copy_to(self, new_name: str, new_root_folder: str):
+    def copy_to(self, name: str, root_folder: str):
 
 
-        model, is_new = Model.get_or_create(root_folder=new_root_folder)
+        model, is_new = Model.get_or_create(root_folder=root_folder)
 
 
         model.name = name
         model.name = name
         model.description = self.description
         model.description = self.description

+ 21 - 15
pycs/database/Project.py

@@ -1,4 +1,5 @@
 import os
 import os
+import shutil
 import typing as T
 import typing as T
 import warnings
 import warnings
 
 
@@ -63,6 +64,20 @@ class Project(NamedBaseModel):
         "data_folder",
         "data_folder",
     )
     )
 
 
+    @commit_on_return
+    def delete(self) -> T.Tuple[dict, dict]:
+        dump = super().delete(commit=False)
+
+        model_dump = {}
+        if self.model_id is not None:
+            model_dump = self.model.delete(commit=False)
+
+        # remove from file system
+        shutil.rmtree(self.root_folder)
+
+        return dump, model_dump
+
+
 
 
     def label(self, identifier: int) -> T.Optional[Label]:
     def label(self, identifier: int) -> T.Optional[Label]:
         """
         """
@@ -122,14 +137,14 @@ class Project(NamedBaseModel):
                             JOIN tree ON labels.parent = tree.id
                             JOIN tree ON labels.parent = tree.id
                     )
                     )
                 SELECT * FROM tree
                 SELECT * FROM tree
-            ''', [self.identifier])
+            ''', [self.id])
 
 
             result = []
             result = []
             lookup = {}
             lookup = {}
 
 
             for row in cursor.fetchall():
             for row in cursor.fetchall():
                 label = TreeNodeLabel(self.database, row)
                 label = TreeNodeLabel(self.database, row)
-                lookup[label.identifier] = label
+                lookup[label.id] = label
 
 
                 if label.parent_id is None:
                 if label.parent_id is None:
                     result.append(label)
                     result.append(label)
@@ -162,7 +177,7 @@ class Project(NamedBaseModel):
     @commit_on_return
     @commit_on_return
     def create_label(self, name: str,
     def create_label(self, name: str,
                      reference: str = None,
                      reference: str = None,
-                     parent_id: int = None,
+                     parent: T.Optional[T.Union[int, str, Label]] = None,
                      hierarchy_level: str = None) -> T.Tuple[T.Optional[Label], bool]:
                      hierarchy_level: str = None) -> T.Tuple[T.Optional[Label], bool]:
         """
         """
         create a label for this project. If there is already a label with the same reference
         create a label for this project. If there is already a label with the same reference
@@ -170,7 +185,7 @@ class Project(NamedBaseModel):
 
 
         :param name: label name
         :param name: label name
         :param reference: label reference
         :param reference: label reference
-        :param parent_id: parent's identifier
+        :param parent: parent label. Either a reference string, a Label id or a Label instance
         :param hierarchy_level: hierarchy level name
         :param hierarchy_level: hierarchy level name
         :return: created or edited label, insert
         :return: created or edited label, insert
         """
         """
@@ -183,7 +198,7 @@ class Project(NamedBaseModel):
             is_new = True
             is_new = True
 
 
         label.set_name(name, commit=False)
         label.set_name(name, commit=False)
-        label.set_parent(parent_id, commit=False)
+        label.set_parent(parent, commit=False)
         label.hierarchy_level = hierarchy_level
         label.hierarchy_level = hierarchy_level
 
 
         return label, is_new
         return label, is_new
@@ -258,15 +273,6 @@ class Project(NamedBaseModel):
         return file, is_new
         return file, is_new
 
 
 
 
-    def count_files(self) -> int:
-        """
-        count files associated with this project
-
-        :return: count
-        """
-        return self.files.count()
-
-
     def get_files(self, offset: int = 0, limit: int = -1) -> T.List[File]:
     def get_files(self, offset: int = 0, limit: int = -1) -> T.List[File]:
         """
         """
         get an iterator of files associated with this project
         get an iterator of files associated with this project
@@ -275,7 +281,7 @@ class Project(NamedBaseModel):
         :param limit: file limit
         :param limit: file limit
         :return: iterator of files
         :return: iterator of files
         """
         """
-        return self.files.order_by(File.id).offset(offset).limit(limit).all()
+        return self.files.order_by(File.id).offset(offset).limit(limit)
 
 
 
 
     def _files_without_results(self):
     def _files_without_results(self):

+ 6 - 2
pycs/database/base.py

@@ -30,7 +30,9 @@ class BaseModel(db.Model, SerializerMixin):
 
 
 
 
     def serialize(self) -> dict:
     def serialize(self) -> dict:
-        return self.to_dict()
+        res = self.to_dict()
+        res["identifier"] = self.id
+        return res
 
 
 
 
     @commit_on_return
     @commit_on_return
@@ -56,7 +58,9 @@ class BaseModel(db.Model, SerializerMixin):
         db.session.add(obj)
         db.session.add(obj)
 
 
         if commit:
         if commit:
-            self.commit()
+            obj.commit()
+
+        return obj
 
 
     @classmethod
     @classmethod
     def get_or_create(cls, **kwargs) -> T.Tuple[BaseModel, bool]:
     def get_or_create(cls, **kwargs) -> T.Tuple[BaseModel, bool]:

+ 0 - 49
pycs/database/discovery/LabelProviderDiscovery.py

@@ -1,49 +0,0 @@
-import re
-from contextlib import closing
-from glob import glob
-from json import load
-from os import path, listdir
-
-
-def __find_files():
-    # list folders in labels/
-    for folder in glob('labels/*'):
-        # list files
-        for filename in listdir(folder):
-            file_path = path.join(folder, filename)
-
-            # filter configuration files
-            if not path.isfile(file_path):
-                continue
-
-            if not re.match(r'^configuration(\d+)?\.json$', filename):
-                continue
-
-            # yield element
-            yield folder, filename, file_path
-
-
-def discover(database):
-    """
-    find label providers in the corresponding folder and add them to the database
-
-    :param database:
-    :return:
-    """
-    with closing(database.cursor()) as cursor:
-        for folder, configuration_file, configuration_path in __find_files():
-            # load configuration file
-            with open(configuration_path, 'r') as file:
-                label = load(file)
-
-            # extract data
-            name = label['name']
-            description = label['description'] if 'description' in label else None
-
-            # save to database
-            cursor.execute('''
-                INSERT INTO label_providers (name, description, root_folder, configuration_file)
-                VALUES (?, ?, ?, ?)
-                ON CONFLICT (root_folder, configuration_file)
-                DO UPDATE SET name = ?, description = ?
-            ''', (name, description, folder, configuration_file, name, description))

+ 0 - 32
pycs/database/discovery/ModelDiscovery.py

@@ -1,32 +0,0 @@
-from contextlib import closing
-from glob import glob
-from json import load, dumps
-from os import path
-
-
-def discover(database):
-    """
-    find models in the corresponding folder and add them to the database
-
-    :param database:
-    :return:
-    """
-    with closing(database.cursor()) as cursor:
-        # list folders in models/
-        for folder in glob('models/*'):
-            # load distribution.json
-            with open(path.join(folder, 'configuration.json'), 'r') as file:
-                model = load(file)
-
-            # extract data
-            name = model['name']
-            description = model['description'] if 'description' in model else None
-            supports = dumps(model['supports'])
-
-            # save to database
-            cursor.execute('''
-                INSERT INTO models (name, description, root_folder, supports)
-                VALUES (?, ?, ?, ?)
-                ON CONFLICT (root_folder)
-                DO UPDATE SET name = ?, description = ?, supports = ?
-            ''', (name, description, folder, supports, name, description, supports))

+ 0 - 0
pycs/database/discovery/__init__.py


+ 6 - 3
pycs/database/util/JSONEncoder.py

@@ -2,6 +2,7 @@ from typing import Any
 
 
 from flask.json import JSONEncoder as Base
 from flask.json import JSONEncoder as Base
 
 
+from pycs.database.base import BaseModel
 
 
 class JSONEncoder(Base):
 class JSONEncoder(Base):
     """
     """
@@ -9,6 +10,8 @@ class JSONEncoder(Base):
     """
     """
 
 
     def default(self, o: Any) -> Any:
     def default(self, o: Any) -> Any:
-        copy = o.__dict__.copy()
-        del copy['database']
-        return copy
+        if isinstance(o, BaseModel):
+            return o.serialize()
+
+        else:
+            return o.__dict__.copy()

+ 1 - 1
pycs/database/util/__init__.py

@@ -4,7 +4,7 @@ from pycs import db
 
 
 def commit_on_return(method):
 def commit_on_return(method):
 
 
-	@warps(method)
+	@wraps(method)
 	def inner(self, *args, commit: bool = True, **kwargs):
 	def inner(self, *args, commit: bool = True, **kwargs):
 
 
 		res = method(self, *args, **kwargs)
 		res = method(self, *args, **kwargs)

+ 108 - 102
pycs/frontend/WebServer.py

@@ -11,7 +11,8 @@ from flask import Flask
 from flask import send_from_directory
 from flask import send_from_directory
 
 
 from pycs import app
 from pycs import app
-from pycs.database.Database import Database
+from pycs.database.Model import Model
+from pycs.database.LabelProvider import LabelProvider
 from pycs.frontend.endpoints.ListJobs import ListJobs
 from pycs.frontend.endpoints.ListJobs import ListJobs
 from pycs.frontend.endpoints.ListLabelProviders import ListLabelProviders
 from pycs.frontend.endpoints.ListLabelProviders import ListLabelProviders
 from pycs.frontend.endpoints.ListModels import ListModels
 from pycs.frontend.endpoints.ListModels import ListModels
@@ -38,8 +39,8 @@ from pycs.frontend.endpoints.projects.EditProjectName import EditProjectName
 from pycs.frontend.endpoints.projects.ExecuteExternalStorage import ExecuteExternalStorage
 from pycs.frontend.endpoints.projects.ExecuteExternalStorage import ExecuteExternalStorage
 from pycs.frontend.endpoints.projects.ExecuteLabelProvider import ExecuteLabelProvider
 from pycs.frontend.endpoints.projects.ExecuteLabelProvider import ExecuteLabelProvider
 from pycs.frontend.endpoints.projects.GetProjectModel import GetProjectModel
 from pycs.frontend.endpoints.projects.GetProjectModel import GetProjectModel
-from pycs.frontend.endpoints.projects.ListCollections import ListCollections
-from pycs.frontend.endpoints.projects.ListFiles import ListFiles
+from pycs.frontend.endpoints.projects.ListProjectCollections import ListProjectCollections
+from pycs.frontend.endpoints.projects.ListProjectFiles import ListProjectFiles
 from pycs.frontend.endpoints.projects.RemoveProject import RemoveProject
 from pycs.frontend.endpoints.projects.RemoveProject import RemoveProject
 from pycs.frontend.endpoints.results.ConfirmResult import ConfirmResult
 from pycs.frontend.endpoints.results.ConfirmResult import ConfirmResult
 from pycs.frontend.endpoints.results.CreateResult import CreateResult
 from pycs.frontend.endpoints.results.CreateResult import CreateResult
@@ -64,7 +65,6 @@ class WebServer:
     # pylint: disable=line-too-long
     # pylint: disable=line-too-long
     # pylint: disable=too-many-statements
     # pylint: disable=too-many-statements
     def __init__(self, app, settings: dict):
     def __init__(self, app, settings: dict):
-        self.database = Database()
 
 
         PRODUCTION = os.path.exists('webui/index.html')
         PRODUCTION = os.path.exists('webui/index.html')
 
 
@@ -104,19 +104,25 @@ class WebServer:
 
 
             # create service objects
             # create service objects
             self.__sio = socketio.Server(cors_allowed_origins='*', async_mode='eventlet')
             self.__sio = socketio.Server(cors_allowed_origins='*', async_mode='eventlet')
-            self.__app = socketio.WSGIApp(self.__sio, app)
+            self.app = app
+            self.wsgi_app = socketio.WSGIApp(self.__sio, app)
 
 
             # set access control header to allow requests from Vue.js development server
             # set access control header to allow requests from Vue.js development server
-            @app.after_request
+            @self.app.after_request
             def after_request(response):
             def after_request(response):
                 # pylint: disable=unused-variable
                 # pylint: disable=unused-variable
                 response.headers['Access-Control-Allow-Origin'] = '*'
                 response.headers['Access-Control-Allow-Origin'] = '*'
                 return response
                 return response
 
 
         # set json encoder so database objects are serialized correctly
         # set json encoder so database objects are serialized correctly
-        app.json_encoder = JSONEncoder
+        self.app.json_encoder = JSONEncoder
+
+        self.host = settings.host
+        self.port = settings.port
 
 
         # create notification manager
         # create notification manager
+        jobs = JobRunner()
+        pipelines = PipelineCache(jobs)
         notifications = NotificationManager(self.__sio)
         notifications = NotificationManager(self.__sio)
 
 
         jobs.on_create(notifications.create_job)
         jobs.on_create(notifications.create_job)
@@ -127,195 +133,195 @@ class WebServer:
 
 
         self.define_routes(jobs, notifications, pipelines)
         self.define_routes(jobs, notifications, pipelines)
 
 
+        Model.discover("models/")
+        LabelProvider.discover("labels/")
+
 
 
     def define_routes(self, jobs, notifications, pipelines):
     def define_routes(self, jobs, notifications, pipelines):
 
 
         # additional
         # additional
-        app.add_url_rule(
+        self.app.add_url_rule(
             '/folder',
             '/folder',
             view_func=FolderInformation.as_view('folder_information')
             view_func=FolderInformation.as_view('folder_information')
         )
         )
 
 
         # jobs
         # jobs
-        app.add_url_rule(
+        self.app.add_url_rule(
             '/jobs',
             '/jobs',
             view_func=ListJobs.as_view('list_jobs', jobs)
             view_func=ListJobs.as_view('list_jobs', jobs)
         )
         )
-        app.add_url_rule(
-            '/jobs/<identifier>/remove',
+        self.app.add_url_rule(
+            '/jobs/<int:job_id>/remove',
             view_func=RemoveJob.as_view('remove_job', jobs)
             view_func=RemoveJob.as_view('remove_job', jobs)
         )
         )
 
 
         # models
         # models
-        app.add_url_rule(
+        self.app.add_url_rule(
             '/models',
             '/models',
-            view_func=ListModels.as_view('list_models', self.database)
+            view_func=ListModels.as_view('list_models')
         )
         )
-        app.add_url_rule(
-            '/projects/<int:identifier>/model',
-            view_func=GetProjectModel.as_view('get_project_model', self.database)
+        self.app.add_url_rule(
+            '/projects/<int:project_id>/model',
+            view_func=GetProjectModel.as_view('get_project_model')
         )
         )
 
 
         # labels
         # labels
-        app.add_url_rule(
+        self.app.add_url_rule(
             '/label_providers',
             '/label_providers',
-            view_func=ListLabelProviders.as_view('label_providers', self.database)
+            view_func=ListLabelProviders.as_view('label_providers')
         )
         )
-        app.add_url_rule(
-            '/projects/<int:identifier>/labels',
-            view_func=ListLabels.as_view('list_labels', self.database)
+        self.app.add_url_rule(
+            '/projects/<int:project_id>/labels',
+            view_func=ListLabels.as_view('list_labels')
         )
         )
-        app.add_url_rule(
-            '/projects/<int:identifier>/labels/tree',
-            view_func=ListLabelTree.as_view('list_label_tree', self.database)
+        self.app.add_url_rule(
+            '/projects/<int:project_id>/labels/tree',
+            view_func=ListLabelTree.as_view('list_label_tree')
         )
         )
-        app.add_url_rule(
-            '/projects/<int:identifier>/labels',
-            view_func=CreateLabel.as_view('create_label', self.database, notifications)
+        self.app.add_url_rule(
+            '/projects/<int:project_id>/labels',
+            view_func=CreateLabel.as_view('create_label', notifications)
         )
         )
-        app.add_url_rule(
+        self.app.add_url_rule(
             '/projects/<int:project_id>/labels/<int:label_id>/remove',
             '/projects/<int:project_id>/labels/<int:label_id>/remove',
-            view_func=RemoveLabel.as_view('remove_label', self.database, notifications)
+            view_func=RemoveLabel.as_view('remove_label', notifications)
         )
         )
-        app.add_url_rule(
+        self.app.add_url_rule(
             '/projects/<int:project_id>/labels/<int:label_id>/name',
             '/projects/<int:project_id>/labels/<int:label_id>/name',
-            view_func=EditLabelName.as_view('edit_label_name', self.database, notifications)
+            view_func=EditLabelName.as_view('edit_label_name', notifications)
         )
         )
-        app.add_url_rule(
+        self.app.add_url_rule(
             '/projects/<int:project_id>/labels/<int:label_id>/parent',
             '/projects/<int:project_id>/labels/<int:label_id>/parent',
-            view_func=EditLabelParent.as_view('edit_label_parent', self.database, notifications)
+            view_func=EditLabelParent.as_view('edit_label_parent', notifications)
         )
         )
 
 
         # collections
         # collections
-        app.add_url_rule(
+        self.app.add_url_rule(
             '/projects/<int:project_id>/collections',
             '/projects/<int:project_id>/collections',
-            view_func=ListCollections.as_view('list_collections', self.database)
+            view_func=ListProjectCollections.as_view('list_collections')
         )
         )
-        app.add_url_rule(
+        self.app.add_url_rule(
             '/projects/<int:project_id>/data/<int:collection_id>/<int:start>/<int:length>',
             '/projects/<int:project_id>/data/<int:collection_id>/<int:start>/<int:length>',
-            view_func=ListFiles.as_view('list_collection_files', self.database)
+            view_func=ListProjectFiles.as_view('list_collection_files')
         )
         )
 
 
         # data
         # data
-        app.add_url_rule(
-            '/projects/<int:identifier>/data',
-            view_func=UploadFile.as_view('upload_file', self.database, notifications)
+        self.app.add_url_rule(
+            '/projects/<int:project_id>/data',
+            view_func=UploadFile.as_view('upload_file', notifications)
         )
         )
-        app.add_url_rule(
+        self.app.add_url_rule(
             '/projects/<int:project_id>/data/<int:start>/<int:length>',
             '/projects/<int:project_id>/data/<int:start>/<int:length>',
-            view_func=ListFiles.as_view('list_files', self.database)
+            view_func=ListProjectFiles.as_view('list_files')
         )
         )
-        app.add_url_rule(
-            '/data/<int:identifier>/remove',
-            view_func=RemoveFile.as_view('remove_file', self.database, notifications)
+        self.app.add_url_rule(
+            '/data/<int:file_id>/remove',
+            view_func=RemoveFile.as_view('remove_file', notifications)
         )
         )
-        app.add_url_rule(
+        self.app.add_url_rule(
             '/data/<int:file_id>',
             '/data/<int:file_id>',
-            view_func=GetFile.as_view('get_file', self.database)
+            view_func=GetFile.as_view('get_file')
         )
         )
-        app.add_url_rule(
+        self.app.add_url_rule(
             '/data/<int:file_id>/<resolution>',
             '/data/<int:file_id>/<resolution>',
-            view_func=GetResizedFile.as_view('get_resized_file', self.database)
+            view_func=GetResizedFile.as_view('get_resized_file')
         )
         )
-        app.add_url_rule(
+        self.app.add_url_rule(
             '/data/<int:file_id>/previous_next',
             '/data/<int:file_id>/previous_next',
-            view_func=GetPreviousAndNextFile.as_view('get_previous_and_next_file', self.database)
+            view_func=GetPreviousAndNextFile.as_view('get_previous_and_next_file')
         )
         )
 
 
         # results
         # results
-        app.add_url_rule(
+        self.app.add_url_rule(
             '/projects/<int:project_id>/results',
             '/projects/<int:project_id>/results',
-            view_func=GetProjectResults.as_view('get_project_results', self.database)
+            view_func=GetProjectResults.as_view('get_project_results')
         )
         )
-        app.add_url_rule(
+        self.app.add_url_rule(
             '/data/<int:file_id>/results',
             '/data/<int:file_id>/results',
-            view_func=GetResults.as_view('get_results', self.database)
+            view_func=GetResults.as_view('get_results')
         )
         )
-        app.add_url_rule(
+        self.app.add_url_rule(
             '/data/<int:file_id>/results',
             '/data/<int:file_id>/results',
-            view_func=CreateResult.as_view('create_result', self.database, notifications)
+            view_func=CreateResult.as_view('create_result', notifications)
         )
         )
-        app.add_url_rule(
+        self.app.add_url_rule(
             '/data/<int:file_id>/reset',
             '/data/<int:file_id>/reset',
-            view_func=ResetResults.as_view('reset_results', self.database, notifications)
+            view_func=ResetResults.as_view('reset_results', notifications)
         )
         )
-        app.add_url_rule(
+        self.app.add_url_rule(
             '/results/<int:result_id>/remove',
             '/results/<int:result_id>/remove',
-            view_func=RemoveResult.as_view('remove_result', self.database, notifications)
+            view_func=RemoveResult.as_view('remove_result', notifications)
         )
         )
-        app.add_url_rule(
+        self.app.add_url_rule(
             '/results/<int:result_id>/confirm',
             '/results/<int:result_id>/confirm',
-            view_func=ConfirmResult.as_view('confirm_result', self.database, notifications)
+            view_func=ConfirmResult.as_view('confirm_result', notifications)
         )
         )
-        app.add_url_rule(
+        self.app.add_url_rule(
             '/results/<int:result_id>/crop',
             '/results/<int:result_id>/crop',
-            view_func=ResultAsCrop.as_view('crop_result', self.database)
+            view_func=ResultAsCrop.as_view('crop_result')
         )
         )
-        app.add_url_rule(
+        self.app.add_url_rule(
             '/results/<int:result_id>/crop/<int:max_width>',
             '/results/<int:result_id>/crop/<int:max_width>',
-            view_func=ResultAsCrop.as_view('crop_result_resized_by_width', self.database)
+            view_func=ResultAsCrop.as_view('crop_result_resized_by_width')
         )
         )
-        app.add_url_rule(
+        self.app.add_url_rule(
             '/results/<int:result_id>/crop/<int:max_width>x<int:max_height>',
             '/results/<int:result_id>/crop/<int:max_width>x<int:max_height>',
-            view_func=ResultAsCrop.as_view('crop_result_resized', self.database)
+            view_func=ResultAsCrop.as_view('crop_result_resized')
         )
         )
-        app.add_url_rule(
+        self.app.add_url_rule(
             '/results/<int:result_id>/label',
             '/results/<int:result_id>/label',
-            view_func=EditResultLabel.as_view('edit_result_label', self.database, notifications)
+            view_func=EditResultLabel.as_view('edit_result_label', notifications)
         )
         )
-        app.add_url_rule(
+        self.app.add_url_rule(
             '/results/<int:result_id>/data',
             '/results/<int:result_id>/data',
-            view_func=EditResultData.as_view('edit_result_data', self.database, notifications)
+            view_func=EditResultData.as_view('edit_result_data', notifications)
         )
         )
 
 
         # projects
         # projects
-        app.add_url_rule(
+        self.app.add_url_rule(
             '/projects',
             '/projects',
-            view_func=ListProjects.as_view('list_projects', self.database)
+            view_func=ListProjects.as_view('list_projects')
         )
         )
-        app.add_url_rule(
+        self.app.add_url_rule(
             '/projects',
             '/projects',
-            view_func=CreateProject.as_view('create_project', self.database, notifications, jobs)
+            view_func=CreateProject.as_view('create_project', notifications, jobs)
         )
         )
-        app.add_url_rule(
-            '/projects/<int:identifier>/label_provider',
-            view_func=ExecuteLabelProvider.as_view('execute_label_provider', self.database,
-                                                   notifications, jobs)
+        self.app.add_url_rule(
+            '/projects/<int:project_id>/label_provider',
+            view_func=ExecuteLabelProvider.as_view('execute_label_provider', notifications, jobs)
         )
         )
-        app.add_url_rule(
-            '/projects/<int:identifier>/external_storage',
-            view_func=ExecuteExternalStorage.as_view('execute_external_storage', self.database,
-                                                     notifications, jobs)
+        self.app.add_url_rule(
+            '/projects/<int:project_id>/external_storage',
+            view_func=ExecuteExternalStorage.as_view('execute_external_storage', notifications, jobs)
         )
         )
-        app.add_url_rule(
-            '/projects/<int:identifier>/remove',
-            view_func=RemoveProject.as_view('remove_project', self.database, notifications)
+        self.app.add_url_rule(
+            '/projects/<int:project_id>/remove',
+            view_func=RemoveProject.as_view('remove_project', notifications)
         )
         )
-        app.add_url_rule(
-            '/projects/<int:identifier>/name',
-            view_func=EditProjectName.as_view('edit_project_name', self.database, notifications)
+        self.app.add_url_rule(
+            '/projects/<int:project_id>/name',
+            view_func=EditProjectName.as_view('edit_project_name', notifications)
         )
         )
-        app.add_url_rule(
-            '/projects/<int:identifier>/description',
-            view_func=EditProjectDescription.as_view('edit_project_description', self.database,
-                                                     notifications)
+        self.app.add_url_rule(
+            '/projects/<int:project_id>/description',
+            view_func=EditProjectDescription.as_view('edit_project_description', notifications)
         )
         )
 
 
         # pipelines
         # pipelines
-        app.add_url_rule(
+        self.app.add_url_rule(
             '/projects/<int:project_id>/pipelines/fit',
             '/projects/<int:project_id>/pipelines/fit',
-            view_func=FitModel.as_view('fit_model', self.database, jobs, pipelines)
+            view_func=FitModel.as_view('fit_model', jobs, pipelines)
         )
         )
-        app.add_url_rule(
+        self.app.add_url_rule(
             '/projects/<int:project_id>/pipelines/predict',
             '/projects/<int:project_id>/pipelines/predict',
-            view_func=PredictModel.as_view('predict_model', self.database, notifications, jobs,
+            view_func=PredictModel.as_view('predict_model', notifications, jobs,
                                            pipelines)
                                            pipelines)
         )
         )
-        app.add_url_rule(
+        self.app.add_url_rule(
             '/data/<int:file_id>/predict',
             '/data/<int:file_id>/predict',
-            view_func=PredictFile.as_view('predict_file', self.database, notifications, jobs, pipelines)
+            view_func=PredictFile.as_view('predict_file', notifications, jobs, pipelines)
         )
         )
 
 
     def run(self):
     def run(self):
         # finally start web server
         # finally start web server
-        eventlet.wsgi.server(eventlet.listen((self.host, self.port)), app)
+        eventlet.wsgi.server(eventlet.listen((self.host, self.port)), self.wsgi_app)

+ 2 - 6
pycs/frontend/endpoints/ListLabelProviders.py

@@ -1,7 +1,7 @@
 from flask import jsonify
 from flask import jsonify
 from flask.views import View
 from flask.views import View
 
 
-from pycs.database.Database import Database
+from pycs.database.LabelProvider import LabelProvider
 
 
 
 
 class ListLabelProviders(View):
 class ListLabelProviders(View):
@@ -11,10 +11,6 @@ class ListLabelProviders(View):
     # pylint: disable=arguments-differ
     # pylint: disable=arguments-differ
     methods = ['GET']
     methods = ['GET']
 
 
-    def __init__(self, db: Database):
-        # pylint: disable=invalid-name
-        self.db = db
 
 
     def dispatch_request(self):
     def dispatch_request(self):
-        label_providers = list(self.db.label_providers())
-        return jsonify(label_providers)
+        return jsonify(LabelProvider.query.all())

+ 2 - 6
pycs/frontend/endpoints/ListModels.py

@@ -1,7 +1,7 @@
 from flask import jsonify
 from flask import jsonify
 from flask.views import View
 from flask.views import View
 
 
-from pycs.database.Database import Database
+from pycs.database.Model import Model
 
 
 
 
 class ListModels(View):
 class ListModels(View):
@@ -11,10 +11,6 @@ class ListModels(View):
     # pylint: disable=arguments-differ
     # pylint: disable=arguments-differ
     methods = ['GET']
     methods = ['GET']
 
 
-    def __init__(self, db: Database):
-        # pylint: disable=invalid-name
-        self.db = db
 
 
     def dispatch_request(self):
     def dispatch_request(self):
-        models = list(self.db.models())
-        return jsonify(models)
+        return jsonify(Model.query.all())

+ 2 - 6
pycs/frontend/endpoints/ListProjects.py

@@ -1,7 +1,7 @@
 from flask import jsonify
 from flask import jsonify
 from flask.views import View
 from flask.views import View
 
 
-from pycs.database.Database import Database
+from pycs.database.Project import Project
 
 
 
 
 class ListProjects(View):
 class ListProjects(View):
@@ -11,10 +11,6 @@ class ListProjects(View):
     # pylint: disable=arguments-differ
     # pylint: disable=arguments-differ
     methods = ['GET']
     methods = ['GET']
 
 
-    def __init__(self, db: Database):
-        # pylint: disable=invalid-name
-        self.db = db
 
 
     def dispatch_request(self):
     def dispatch_request(self):
-        projects = list(self.db.projects())
-        return jsonify(projects)
+        return jsonify(Project.query.all())

+ 6 - 4
pycs/frontend/endpoints/additional/FolderInformation.py

@@ -1,6 +1,8 @@
-from os import path, listdir
+import os
 
 
-from flask import request, abort, jsonify
+from flask import abort
+from flask import jsonify
+from flask import request
 from flask.views import View
 from flask.views import View
 
 
 
 
@@ -21,12 +23,12 @@ class FolderInformation(View):
 
 
         # check if directory exists
         # check if directory exists
         result = {
         result = {
-            'exists': path.exists(folder)
+            'exists': os.path.exists(folder)
         }
         }
 
 
         # count files
         # count files
         if result['exists']:
         if result['exists']:
-            result['count'] = len(listdir(folder))
+            result['count'] = len(os.listdir(folder))
 
 
         # send result
         # send result
         return jsonify(result)
         return jsonify(result)

+ 6 - 18
pycs/frontend/endpoints/data/GetFile.py

@@ -1,9 +1,9 @@
-from os import path, getcwd
+import os
 
 
-from flask import abort, send_from_directory
+from flask import send_from_directory
 from flask.views import View
 from flask.views import View
 
 
-from pycs.database.Database import Database
+from pycs.database.File import File
 
 
 
 
 class GetFile(View):
 class GetFile(View):
@@ -13,23 +13,11 @@ class GetFile(View):
     # pylint: disable=arguments-differ
     # pylint: disable=arguments-differ
     methods = ['GET']
     methods = ['GET']
 
 
-    def __init__(self, db: Database):
-        # pylint: disable=invalid-name
-        self.db = db
-
     def dispatch_request(self, file_id: int):
     def dispatch_request(self, file_id: int):
-        # get file from database
-        file = self.db.file(file_id)
-
-        if file is None:
-            return abort(404)
+        file = File.get_or_404(file_id)
 
 
-        # get absolute path
-        if path.isabs(file.path):
-            abs_file_path = file.path
-        else:
-            abs_file_path = path.join(getcwd(), file.path)
+        abs_file_path = file.absolute_path
 
 
         # return data
         # return data
-        file_directory, file_name = path.split(abs_file_path)
+        file_directory, file_name = os.path.split(abs_file_path)
         return send_from_directory(file_directory, file_name)
         return send_from_directory(file_directory, file_name)

+ 4 - 9
pycs/frontend/endpoints/data/GetPreviousAndNextFile.py

@@ -1,7 +1,8 @@
-from flask import abort, jsonify
+from flask import abort
+from flask import jsonify
 from flask.views import View
 from flask.views import View
 
 
-from pycs.database.Database import Database
+from pycs.database.File import File
 
 
 
 
 class GetPreviousAndNextFile(View):
 class GetPreviousAndNextFile(View):
@@ -11,16 +12,10 @@ class GetPreviousAndNextFile(View):
     # pylint: disable=arguments-differ
     # pylint: disable=arguments-differ
     methods = ['GET']
     methods = ['GET']
 
 
-    def __init__(self, db: Database):
-        # pylint: disable=invalid-name
-        self.db = db
 
 
     def dispatch_request(self, file_id: int):
     def dispatch_request(self, file_id: int):
         # get file from database
         # get file from database
-        file = self.db.file(file_id)
-
-        if file is None:
-            return abort(404)
+        file = File.get_or_404(file_id)
 
 
         # get previous and next
         # get previous and next
         result = {
         result = {

+ 5 - 14
pycs/frontend/endpoints/data/GetResizedFile.py

@@ -1,14 +1,9 @@
-import cv2
-import os
-import re
-
-from PIL import Image
 from eventlet import tpool
 from eventlet import tpool
 from flask import abort
 from flask import abort
 from flask import send_from_directory
 from flask import send_from_directory
 from flask.views import View
 from flask.views import View
 
 
-from pycs.database.Database import Database
+from pycs.database.File import File
 from pycs.util import file_ops
 from pycs.util import file_ops
 
 
 
 
@@ -19,17 +14,12 @@ class GetResizedFile(View):
     # pylint: disable=arguments-differ
     # pylint: disable=arguments-differ
     methods = ['GET']
     methods = ['GET']
 
 
-    def __init__(self, db: Database):
-        # pylint: disable=invalid-name
-        self.db = db
 
 
     def dispatch_request(self, file_id: int, resolution: str):
     def dispatch_request(self, file_id: int, resolution: str):
         # get file from database
         # get file from database
-        file = self.db.file(file_id)
-        if file is None:
-            return abort(404)
+        file = File.get_or_404(file_id)
 
 
-        project = file.project()
+        project = file.project
 
 
         if not os.path.exists(file.absolute_path):
         if not os.path.exists(file.absolute_path):
             abort(404, "File not found!")
             abort(404, "File not found!")
@@ -41,5 +31,6 @@ class GetResizedFile(View):
 
 
         # send data
         # send data
         file_directory, file_name = tpool.execute(file_ops.resize_file,
         file_directory, file_name = tpool.execute(file_ops.resize_file,
-                                                  file, project.root_folder, max_width, max_height)
+                                                  file, file.project.root_folder,
+                                                  max_width, max_height)
         return send_from_directory(file_directory, file_name)
         return send_from_directory(file_directory, file_name)

+ 13 - 20
pycs/frontend/endpoints/data/RemoveFile.py

@@ -3,7 +3,8 @@ from os import remove
 from flask import make_response, request, abort
 from flask import make_response, request, abort
 from flask.views import View
 from flask.views import View
 
 
-from pycs.database.Database import Database
+from pycs import db
+from pycs.database.File import File
 from pycs.frontend.notifications.NotificationManager import NotificationManager
 from pycs.frontend.notifications.NotificationManager import NotificationManager
 
 
 
 
@@ -14,38 +15,30 @@ class RemoveFile(View):
     # pylint: disable=arguments-differ
     # pylint: disable=arguments-differ
     methods = ['POST']
     methods = ['POST']
 
 
-    def __init__(self, db: Database, nm: NotificationManager):
+    def __init__(self, nm: NotificationManager):
         # pylint: disable=invalid-name
         # pylint: disable=invalid-name
-        self.db = db
         self.nm = nm
         self.nm = nm
 
 
-    def dispatch_request(self, identifier):
+    def dispatch_request(self, file_id: int):
         # extract request data
         # extract request data
         data = request.get_json(force=True)
         data = request.get_json(force=True)
 
 
-        if 'remove' not in data or data['remove'] is not True:
-            return abort(400)
+        if not data.get('remove', False):
+            abort(400, "remove flag is missing")
 
 
         # start transaction
         # start transaction
-        with self.db:
+        with db.session.begin_nested():
             # find file
             # find file
-            file = self.db.file(identifier)
-            if file is None:
-                return abort(400)
+            file = File.get_or_404(file_id)
 
 
             # check if project uses an external data directory
             # check if project uses an external data directory
-            project = file.project()
-            if project.external_data:
-                return abort(400)
+            if file.project.external_data:
+                abort(400,
+                    "Cannot remove file, project is setup with external data!")
 
 
             # remove file from database
             # remove file from database
-            file.remove()
-
-            # remove file from folder
-            remove(file.path)
-
-            # TODO remove temp files
+            file_dump = file.delete()
 
 
         # send notification
         # send notification
-        self.nm.remove_file(file)
+        self.nm.remove_file(file_dump)
         return make_response()
         return make_response()

+ 24 - 20
pycs/frontend/endpoints/data/UploadFile.py

@@ -1,12 +1,12 @@
-from os import path
-from uuid import uuid1
+import os
+import uuid
 
 
 from eventlet import tpool
 from eventlet import tpool
 from flask import make_response, request, abort
 from flask import make_response, request, abort
 from flask.views import View
 from flask.views import View
 from werkzeug import formparser
 from werkzeug import formparser
 
 
-from pycs.database.Database import Database
+from pycs.database.Project import Project
 from pycs.frontend.notifications.NotificationManager import NotificationManager
 from pycs.frontend.notifications.NotificationManager import NotificationManager
 from pycs.util.FileParser import file_info
 from pycs.util.FileParser import file_info
 
 
@@ -18,23 +18,19 @@ class UploadFile(View):
     # pylint: disable=arguments-differ
     # pylint: disable=arguments-differ
     methods = ['POST']
     methods = ['POST']
 
 
-    def __init__(self, db: Database, nm: NotificationManager):
+    def __init__(self, nm: NotificationManager):
         # pylint: disable=invalid-name
         # pylint: disable=invalid-name
-        self.db = db
         self.nm = nm
         self.nm = nm
 
 
         self.data_folder = None
         self.data_folder = None
-        self.file_id = None
+        self.file_uuid = None
         self.file_name = None
         self.file_name = None
         self.file_extension = None
         self.file_extension = None
         self.file_size = None
         self.file_size = None
 
 
-    def dispatch_request(self, identifier):
+    def dispatch_request(self, project_id: int):
         # find project
         # find project
-        project = self.db.project(identifier)
-
-        if project is None:
-            return abort(404, "Project not found")
+        project = Project.get_or_404(project_id)
 
 
         # abort if external storage is used
         # abort if external storage is used
         if project.external_data:
         if project.external_data:
@@ -42,11 +38,12 @@ class UploadFile(View):
 
 
         # get upload path and id
         # get upload path and id
         self.data_folder = project.data_folder
         self.data_folder = project.data_folder
-        self.file_id = str(uuid1())
+        self.file_uuid = str(uuid.uuid1())
 
 
         # parse upload data
         # parse upload data
         _, _, files = tpool.execute(formparser.parse_form_data,
         _, _, files = tpool.execute(formparser.parse_form_data,
-                                    request.environ, stream_factory=self.custom_stream_factory)
+                                    request.environ,
+                                    stream_factory=self.custom_stream_factory)
 
 
         # abort if there is no file entry in uploaded data
         # abort if there is no file entry in uploaded data
         if 'file' not in files.keys():
         if 'file' not in files.keys():
@@ -55,14 +52,21 @@ class UploadFile(View):
         # detect file type
         # detect file type
         try:
         try:
             ftype, frames, fps = tpool.execute(file_info,
             ftype, frames, fps = tpool.execute(file_info,
-                                               self.data_folder, self.file_id, self.file_extension)
+                                               self.data_folder,
+                                               self.file_uuid,
+                                               self.file_extension)
         except ValueError as exception:
         except ValueError as exception:
             return abort(400, str(exception))
             return abort(400, str(exception))
 
 
-        # add to project files
-        with self.db:
-            file, _ = project.add_file(self.file_id, ftype, self.file_name, self.file_extension,
-                                       self.file_size, self.file_id, frames, fps)
+        file, _ = project.add_file(
+            uuid=self.file_uuid,
+            file_type=ftype,
+            name=self.file_name,
+            extension=self.file_extension,
+            size=self.file_size,
+            filename=self.file_uuid,
+            frames=frames,
+            fps=fps)
 
 
         # send update
         # send update
         self.nm.create_file(file)
         self.nm.create_file(file)
@@ -83,7 +87,7 @@ class UploadFile(View):
         """
         """
         # pylint: disable=unused-argument
         # pylint: disable=unused-argument
         # set relevant properties
         # set relevant properties
-        self.file_name, self.file_extension = path.splitext(filename)
+        self.file_name, self.file_extension = os.path.splitext(filename)
 
 
         if content_length is not None and content_length > 0:
         if content_length is not None and content_length > 0:
             self.file_size = content_length
             self.file_size = content_length
@@ -91,5 +95,5 @@ class UploadFile(View):
             self.file_size = total_content_length
             self.file_size = total_content_length
 
 
         # open file handler
         # open file handler
-        file_path = path.join(self.data_folder, f'{self.file_id}{self.file_extension}')
+        file_path = os.path.join(self.data_folder, f'{self.file_uuid}{self.file_extension}')
         return open(file_path, 'wb')
         return open(file_path, 'wb')

+ 7 - 5
pycs/frontend/endpoints/jobs/RemoveJob.py

@@ -1,4 +1,6 @@
-from flask import make_response, request, abort
+from flask import abort
+from flask import make_response
+from flask import request
 from flask.views import View
 from flask.views import View
 
 
 from pycs.jobs.JobRunner import JobRunner
 from pycs.jobs.JobRunner import JobRunner
@@ -15,15 +17,15 @@ class RemoveJob(View):
         # pylint: disable=invalid-name
         # pylint: disable=invalid-name
         self.jobs = jobs
         self.jobs = jobs
 
 
-    def dispatch_request(self, identifier):
+    def dispatch_request(self, job_id):
         # extract request data
         # extract request data
         data = request.get_json(force=True)
         data = request.get_json(force=True)
 
 
-        if 'remove' not in data or data['remove'] is not True:
-            abort(400)
+        if not data.get('remove', False):
+            abort(400, "remove flag is missing")
 
 
         # remove job
         # remove job
-        self.jobs.remove(identifier)
+        self.jobs.remove(job_id)
 
 
         # return success response
         # return success response
         return make_response()
         return make_response()

+ 18 - 15
pycs/frontend/endpoints/labels/CreateLabel.py

@@ -1,7 +1,10 @@
-from flask import request, abort, make_response
+from flask import abort
+from flask import make_response
+from flask import request
 from flask.views import View
 from flask.views import View
 
 
-from pycs.database.Database import Database
+from pycs import db
+from pycs.database.Project import Project
 from pycs.frontend.notifications.NotificationManager import NotificationManager
 from pycs.frontend.notifications.NotificationManager import NotificationManager
 
 
 
 
@@ -12,30 +15,30 @@ class CreateLabel(View):
     # pylint: disable=arguments-differ
     # pylint: disable=arguments-differ
     methods = ['POST']
     methods = ['POST']
 
 
-    def __init__(self, db: Database, nm: NotificationManager):
+    def __init__(self, nm: NotificationManager):
         # pylint: disable=invalid-name
         # pylint: disable=invalid-name
-        self.db = db
         self.nm = nm
         self.nm = nm
 
 
-    def dispatch_request(self, identifier):
+
+    def dispatch_request(self, project_id):
         # extract request data
         # extract request data
         data = request.get_json(force=True)
         data = request.get_json(force=True)
+        name = data.get('name')
 
 
-        if 'name' not in data:
+        if name is None:
             abort(400)
             abort(400)
 
 
         name = data['name']
         name = data['name']
-        parent = data['parent'] if 'parent' in data else None
+        parent = data.get('parent')
 
 
         # find project
         # find project
-        project = self.db.project(identifier)
-        if project is None:
-            abort(404)
-
-        # start transaction
-        with self.db:
-            # insert label
-            label, _ = project.create_label(name, parent=parent)
+        project = Project.get_or_404(project_id)
+
+        # insert label
+        with db.session.begin_nested():
+            label, is_new = project.create_label(name, parent=parent, commit=False)
+            if is_new:
+                abort(400, f"Label already exists: {label}")
 
 
         # send notification
         # send notification
         self.nm.create_label(label)
         self.nm.create_label(label)

+ 14 - 16
pycs/frontend/endpoints/labels/EditLabelName.py

@@ -1,7 +1,9 @@
-from flask import request, abort, make_response
+from flask import abort
+from flask import make_response
+from flask import request
 from flask.views import View
 from flask.views import View
 
 
-from pycs.database.Database import Database
+from pycs.database.Label import Label
 from pycs.frontend.notifications.NotificationManager import NotificationManager
 from pycs.frontend.notifications.NotificationManager import NotificationManager
 
 
 
 
@@ -12,32 +14,28 @@ class EditLabelName(View):
     # pylint: disable=arguments-differ
     # pylint: disable=arguments-differ
     methods = ['POST']
     methods = ['POST']
 
 
-    def __init__(self, db: Database, nm: NotificationManager):
+    def __init__(self, nm: NotificationManager):
         # pylint: disable=invalid-name
         # pylint: disable=invalid-name
-        self.db = db
         self.nm = nm
         self.nm = nm
 
 
+
     def dispatch_request(self, project_id: int, label_id: int):
     def dispatch_request(self, project_id: int, label_id: int):
         # extract request data
         # extract request data
         data = request.get_json(force=True)
         data = request.get_json(force=True)
+        name = data.get('name')
 
 
-        if 'name' not in data:
-            abort(400)
-
-        # find project
-        project = self.db.project(project_id)
-        if project is None:
-            abort(404)
+        if name is None:
+            abort(400, 'name argument is missing')
 
 
         # find label
         # find label
-        label = project.label(label_id)
+        label = Label.query.filter(
+            Label.project_id == project_id,
+            Label.id == label_id).one_or_none()
+
         if label is None:
         if label is None:
             abort(404)
             abort(404)
 
 
-        # start transaction
-        with self.db:
-            # change name
-            label.set_name(data['name'])
+        label.set_name(name)
 
 
         # send notification
         # send notification
         self.nm.edit_label(label)
         self.nm.edit_label(label)

+ 14 - 16
pycs/frontend/endpoints/labels/EditLabelParent.py

@@ -1,7 +1,9 @@
-from flask import request, abort, make_response
+from flask import abort
+from flask import make_response
+from flask import request
 from flask.views import View
 from flask.views import View
 
 
-from pycs.database.Database import Database
+from pycs.database.Label import Label
 from pycs.frontend.notifications.NotificationManager import NotificationManager
 from pycs.frontend.notifications.NotificationManager import NotificationManager
 
 
 
 
@@ -12,32 +14,28 @@ class EditLabelParent(View):
     # pylint: disable=arguments-differ
     # pylint: disable=arguments-differ
     methods = ['POST']
     methods = ['POST']
 
 
-    def __init__(self, db: Database, nm: NotificationManager):
+    def __init__(self, nm: NotificationManager):
         # pylint: disable=invalid-name
         # pylint: disable=invalid-name
-        self.db = db
         self.nm = nm
         self.nm = nm
 
 
+
     def dispatch_request(self, project_id: int, label_id: int):
     def dispatch_request(self, project_id: int, label_id: int):
         # extract request data
         # extract request data
         data = request.get_json(force=True)
         data = request.get_json(force=True)
+        parent = data.get('parent')
 
 
-        if 'parent' not in data:
-            abort(400)
-
-        # find project
-        project = self.db.project(project_id)
-        if project is None:
-            abort(404)
+        if parent is None:
+            abort(400, 'parent argument is missing')
 
 
         # find label
         # find label
-        label = project.label(label_id)
+        label = Label.query.filter(
+            Label.project_id == project_id,
+            Label.id == label_id).one_or_none()
+
         if label is None:
         if label is None:
             abort(404)
             abort(404)
 
 
-        # start transaction
-        with self.db:
-            # change parent
-            label.set_parent(data['parent'])
+        label.set_parent(parent)
 
 
         # send notification
         # send notification
         self.nm.edit_label(label)
         self.nm.edit_label(label)

+ 5 - 13
pycs/frontend/endpoints/labels/ListLabelTree.py

@@ -1,7 +1,7 @@
-from flask import abort, jsonify
+from flask import jsonify
 from flask.views import View
 from flask.views import View
 
 
-from pycs.database.Database import Database
+from pycs.database.Project import Project
 
 
 
 
 class ListLabelTree(View):
 class ListLabelTree(View):
@@ -11,18 +11,10 @@ class ListLabelTree(View):
     # pylint: disable=arguments-differ
     # pylint: disable=arguments-differ
     methods = ['GET']
     methods = ['GET']
 
 
-    def __init__(self, db: Database):
-        # pylint: disable=invalid-name
-        self.db = db
 
 
-    def dispatch_request(self, identifier):
+    def dispatch_request(self, project_id):
         # find project
         # find project
-        project = self.db.project(identifier)
-        if project is None:
-            abort(404)
-
-        # get labels
-        labels = project.label_tree()
+        project = Project.get_or_404(project_id)
 
 
         # return labels
         # return labels
-        return jsonify(labels)
+        return jsonify(project.label_tree())

+ 5 - 13
pycs/frontend/endpoints/labels/ListLabels.py

@@ -1,7 +1,7 @@
-from flask import abort, jsonify
+from flask import jsonify
 from flask.views import View
 from flask.views import View
 
 
-from pycs.database.Database import Database
+from pycs.database.Project import Project
 
 
 
 
 class ListLabels(View):
 class ListLabels(View):
@@ -11,18 +11,10 @@ class ListLabels(View):
     # pylint: disable=arguments-differ
     # pylint: disable=arguments-differ
     methods = ['GET']
     methods = ['GET']
 
 
-    def __init__(self, db: Database):
-        # pylint: disable=invalid-name
-        self.db = db
 
 
-    def dispatch_request(self, identifier):
+    def dispatch_request(self, project_id):
         # find project
         # find project
-        project = self.db.project(identifier)
-        if project is None:
-            abort(404)
-
-        # get labels
-        labels = project.labels()
+        project = Project.get_or_404(project_id)
 
 
         # return labels
         # return labels
-        return jsonify(labels)
+        return jsonify(project.labels.all())

+ 23 - 22
pycs/frontend/endpoints/labels/RemoveLabel.py

@@ -1,7 +1,10 @@
-from flask import request, abort, make_response
+from flask import abort
+from flask import make_response
+from flask import request
 from flask.views import View
 from flask.views import View
 
 
-from pycs.database.Database import Database
+from pycs import db
+from pycs.database.Label import Label
 from pycs.frontend.notifications.NotificationManager import NotificationManager
 from pycs.frontend.notifications.NotificationManager import NotificationManager
 
 
 
 
@@ -12,41 +15,39 @@ class RemoveLabel(View):
     # pylint: disable=arguments-differ
     # pylint: disable=arguments-differ
     methods = ['POST']
     methods = ['POST']
 
 
-    def __init__(self, db: Database, nm: NotificationManager):
+    def __init__(self, nm: NotificationManager):
         # pylint: disable=invalid-name
         # pylint: disable=invalid-name
-        self.db = db
         self.nm = nm
         self.nm = nm
 
 
     def dispatch_request(self, project_id: int, label_id: int):
     def dispatch_request(self, project_id: int, label_id: int):
         # extract request data
         # extract request data
         data = request.get_json(force=True)
         data = request.get_json(force=True)
 
 
-        if 'remove' not in data or data['remove'] is not True:
-            abort(400)
-
-        # find project
-        project = self.db.project(project_id)
-        if project is None:
-            abort(404)
+        if not data.get('remove', False):
+            abort(400, "remove flag is missing")
 
 
         # find label
         # find label
-        label = project.label(label_id)
+        label = Label.query.filter(
+            Label.project_id == project_id,
+            Label.id == label_id).one_or_none()
+
         if label is None:
         if label is None:
             abort(404)
             abort(404)
 
 
-        # find children
-        children = label.children()
-
         # start transaction
         # start transaction
-        with self.db:
-            # remove children's parent entry
-            for child in children:
-                child.set_parent(None)
-                self.nm.edit_label(child)
+        with db.session.begin_nested():
+
+            # update children's parent entry
+            label.children.update({Label.parent: label.parent},
+                synchronize_session=False)
 
 
             # remove label
             # remove label
-            label.remove()
-            self.nm.remove_label(label)
+            label_dump = label.delete(commit=False)
+
+            # notify about changes
+            for child in children:
+                self.nm.edit_label(child)
+            self.nm.remove_label(label_dump)
 
 
         # return success response
         # return success response
         return make_response()
         return make_response()

+ 11 - 14
pycs/frontend/endpoints/pipelines/FitModel.py

@@ -1,7 +1,7 @@
 from flask import make_response, request, abort
 from flask import make_response, request, abort
 from flask.views import View
 from flask.views import View
 
 
-from pycs.database.Database import Database
+from pycs.database.Project import Project
 from pycs.interfaces.MediaStorage import MediaStorage
 from pycs.interfaces.MediaStorage import MediaStorage
 from pycs.jobs.JobGroupBusyException import JobGroupBusyException
 from pycs.jobs.JobGroupBusyException import JobGroupBusyException
 from pycs.jobs.JobRunner import JobRunner
 from pycs.jobs.JobRunner import JobRunner
@@ -15,9 +15,8 @@ class FitModel(View):
     # pylint: disable=arguments-differ
     # pylint: disable=arguments-differ
     methods = ['POST']
     methods = ['POST']
 
 
-    def __init__(self, db: Database, jobs: JobRunner, pipelines: PipelineCache):
+    def __init__(self, jobs: JobRunner, pipelines: PipelineCache):
         # pylint: disable=invalid-name
         # pylint: disable=invalid-name
-        self.db = db
         self.jobs = jobs
         self.jobs = jobs
         self.pipelines = pipelines
         self.pipelines = pipelines
 
 
@@ -25,13 +24,11 @@ class FitModel(View):
         # extract request data
         # extract request data
         data = request.get_json(force=True)
         data = request.get_json(force=True)
 
 
-        if 'fit' not in data or data['fit'] is not True:
-            return abort(400)
+        if not data.get('fit', False):
+            abort(400, "fit flag is missing")
 
 
         # find project
         # find project
-        project = self.db.project(project_id)
-        if project is None:
-            return abort(404)
+        project = Project.get_or_404(project_id)
 
 
         # create job
         # create job
         try:
         try:
@@ -39,18 +36,18 @@ class FitModel(View):
                           'Model Interaction',
                           'Model Interaction',
                           f'{project.name} (fit model with new data)',
                           f'{project.name} (fit model with new data)',
                           f'{project.name}/model-interaction',
                           f'{project.name}/model-interaction',
-                          self.load_and_fit, self.db, project.identifier)
+                          FitModel.load_and_fit, project.id)
+
         except JobGroupBusyException:
         except JobGroupBusyException:
-            return abort(400)
+            return abort(400, "Model fitting already running")
 
 
         return make_response()
         return make_response()
 
 
     @staticmethod
     @staticmethod
-    def load_and_fit(database: Database, pipelines: PipelineCache, project_id: int):
+    def load_and_fit(pipelines: PipelineCache, project_id: int):
         """
         """
         load the pipeline and call the fit function
         load the pipeline and call the fit function
 
 
-        :param database: database object
         :param pipelines: pipeline cache
         :param pipelines: pipeline cache
         :param project_id: project id
         :param project_id: project id
         """
         """
@@ -60,8 +57,8 @@ class FitModel(View):
         # create new database instance
         # create new database instance
         try:
         try:
             database_copy = database.copy()
             database_copy = database.copy()
-            project = database_copy.project(project_id)
-            model = project.model()
+            project = Project.query.get(project_id)
+            model = project.model
             storage = MediaStorage(database_copy, project_id)
             storage = MediaStorage(database_copy, project_id)
 
 
             # load pipeline
             # load pipeline

+ 9 - 13
pycs/frontend/endpoints/pipelines/PredictFile.py

@@ -1,7 +1,6 @@
 from flask import make_response, request, abort
 from flask import make_response, request, abort
 from flask.views import View
 from flask.views import View
 
 
-from pycs.database.Database import Database
 from pycs.frontend.endpoints.pipelines.PredictModel import PredictModel as Predict
 from pycs.frontend.endpoints.pipelines.PredictModel import PredictModel as Predict
 from pycs.frontend.notifications.NotificationList import NotificationList
 from pycs.frontend.notifications.NotificationList import NotificationList
 from pycs.frontend.notifications.NotificationManager import NotificationManager
 from pycs.frontend.notifications.NotificationManager import NotificationManager
@@ -17,10 +16,8 @@ class PredictFile(View):
     # pylint: disable=arguments-differ
     # pylint: disable=arguments-differ
     methods = ['POST']
     methods = ['POST']
 
 
-    def __init__(self,
-                 db: Database, nm: NotificationManager, jobs: JobRunner, pipelines: PipelineCache):
+    def __init__(self, nm: NotificationManager, jobs: JobRunner, pipelines: PipelineCache):
         # pylint: disable=invalid-name
         # pylint: disable=invalid-name
-        self.db = db
         self.nm = nm
         self.nm = nm
         self.jobs = jobs
         self.jobs = jobs
         self.pipelines = pipelines
         self.pipelines = pipelines
@@ -29,16 +26,14 @@ class PredictFile(View):
         # extract request data
         # extract request data
         data = request.get_json(force=True)
         data = request.get_json(force=True)
 
 
-        if 'predict' not in data or data['predict'] is not True:
-            return abort(400)
+        if not data.get('predict', False):
+            abort(400, "predict flag is missing")
 
 
         # find file
         # find file
-        file = self.db.file(file_id)
-        if file is None:
-            return abort(404)
+        file = File.get_or_404(file_id)
 
 
         # get project and model
         # get project and model
-        project = file.project()
+        project = file.project
 
 
         # create job
         # create job
         try:
         try:
@@ -47,11 +42,12 @@ class PredictFile(View):
             self.jobs.run(project,
             self.jobs.run(project,
                           'Model Interaction',
                           'Model Interaction',
                           f'{project.name} (create predictions)',
                           f'{project.name} (create predictions)',
-                          f'{project.name}/model-interaction',
+                          f'{project.id}/model-interaction',
                           Predict.load_and_predict,
                           Predict.load_and_predict,
-                          self.db, self.pipelines, notifications, project.identifier, [file],
+                          self.pipelines, notifications, project.id, [file.id],
                           progress=Predict.progress)
                           progress=Predict.progress)
+
         except JobGroupBusyException:
         except JobGroupBusyException:
-            return abort(400)
+            abort(400, "File prediction is already running")
 
 
         return make_response()
         return make_response()

+ 66 - 63
pycs/frontend/endpoints/pipelines/PredictModel.py

@@ -1,10 +1,14 @@
-from typing import Union, List
+from typing import List
+from typing import Union
 
 
-from flask import make_response, request, abort
+from flask import abort
+from flask import make_response
+from flask import request
 from flask.views import View
 from flask.views import View
 
 
-from pycs.database.Database import Database
+from pycs import db
 from pycs.database.File import File
 from pycs.database.File import File
+from pycs.database.Project import Project
 from pycs.frontend.notifications.NotificationList import NotificationList
 from pycs.frontend.notifications.NotificationList import NotificationList
 from pycs.frontend.notifications.NotificationManager import NotificationManager
 from pycs.frontend.notifications.NotificationManager import NotificationManager
 from pycs.interfaces.MediaFile import MediaFile
 from pycs.interfaces.MediaFile import MediaFile
@@ -21,10 +25,8 @@ class PredictModel(View):
     # pylint: disable=arguments-differ
     # pylint: disable=arguments-differ
     methods = ['POST']
     methods = ['POST']
 
 
-    def __init__(self,
-                 db: Database, nm: NotificationManager, jobs: JobRunner, pipelines: PipelineCache):
+    def __init__(self, nm: NotificationManager, jobs: JobRunner, pipelines: PipelineCache):
         # pylint: disable=invalid-name
         # pylint: disable=invalid-name
-        self.db = db
         self.nm = nm
         self.nm = nm
         self.jobs = jobs
         self.jobs = jobs
         self.pipelines = pipelines
         self.pipelines = pipelines
@@ -33,13 +35,16 @@ class PredictModel(View):
         # extract request data
         # extract request data
         data = request.get_json(force=True)
         data = request.get_json(force=True)
 
 
-        if 'predict' not in data or data['predict'] not in ['all', 'new']:
-            return abort(400)
+        predict = data.get('predict')
+
+        if predict is None:
+            abort(400, "predict argument is missing")
+
+        if predict not in ['all', 'new']:
+            abort(400, "predict must be either 'all' or 'new'")
 
 
         # find project
         # find project
-        project = self.db.project(project_id)
-        if project is None:
-            return abort(404)
+        project = Project.get_or_404(project_id)
 
 
         # create job
         # create job
         try:
         try:
@@ -48,20 +53,21 @@ class PredictModel(View):
             self.jobs.run(project,
             self.jobs.run(project,
                           'Model Interaction',
                           'Model Interaction',
                           f'{project.name} (create predictions)',
                           f'{project.name} (create predictions)',
-                          f'{project.name}/model-interaction',
-                          self.load_and_predict,
-                          self.db, self.pipelines, notifications,
-                          project.identifier, data['predict'],
+                          f'{project.id}/model-interaction',
+                          PredictModel.load_and_predict,
+                          self.pipelines, notifications,
+                          project.id, predict,
                           progress=self.progress)
                           progress=self.progress)
+
         except JobGroupBusyException:
         except JobGroupBusyException:
-            return abort(400)
+            abort(400, "Model prediction is already running")
 
 
         return make_response()
         return make_response()
 
 
     @staticmethod
     @staticmethod
-    def load_and_predict(database: Database, pipelines: PipelineCache,
+    def load_and_predict(pipelines: PipelineCache,
                          notifications: NotificationList,
                          notifications: NotificationList,
-                         project_id: int, file_filter: Union[str, List[File]]):
+                         project_id: int, file_filter: Union[str, List[int]]):
         """
         """
         load the pipeline and call the execute function
         load the pipeline and call the execute function
 
 
@@ -69,58 +75,55 @@ class PredictModel(View):
         :param pipelines: pipeline cache
         :param pipelines: pipeline cache
         :param notifications: notification object
         :param notifications: notification object
         :param project_id: project id
         :param project_id: project id
-        :param file_filter: list of files or 'new' / 'all'
+        :param file_filter: list of file ids or 'new' / 'all'
         :return:
         :return:
         """
         """
-        database_copy = None
         pipeline = None
         pipeline = None
 
 
         # create new database instance
         # create new database instance
-        try:
-            database_copy = database.copy()
-            project = database_copy.project(project_id)
-            model = project.model()
-            storage = MediaStorage(database_copy, project_id, notifications)
-
-            # create a list of MediaFile
-            if isinstance(file_filter, str):
-                if file_filter == 'new':
-                    length = project.count_files_without_results()
-                    files = map(lambda f: MediaFile(f, notifications),
-                                project.files_without_results())
-                else:
-                    length = project.count_files()
-                    files = map(lambda f: MediaFile(f, notifications),
-                                project.files())
+        project = Project.query.get(project_id)
+        model = project.model
+        storage = MediaStorage(project_id, notifications)
+
+        # create a list of MediaFile
+        if isinstance(file_filter, str):
+            if file_filter == 'new':
+                files = project.files_without_results()
+                length = project.count_files_without_results()
+
             else:
             else:
-                files = map(lambda f: MediaFile(project.file(f.identifier), notifications),
-                            file_filter)
-                length = len(file_filter)
-
-            # load pipeline
-            try:
-                pipeline = pipelines.load_from_root_folder(project, model.root_folder)
-
-                # iterate over files
-                index = 0
-                for file in files:
-                    # remove old predictions
-                    file.remove_predictions()
-
-                    # create new predictions
-                    pipeline.execute(storage, file)
-
-                    # commit changes and yield progress
-                    database_copy.commit()
-                    yield index / length, notifications
-
-                    index += 1
-            finally:
-                if pipeline is not None:
-                    pipelines.free_instance(model.root_folder)
+                files = project.files.all()
+                length = project.files.count()
+
+        else:
+            files = [project.file(identifier) for identifier in file_filter]
+            length = len(files)
+
+
+        media_files = map(lambda f: MediaFile(f, notifications), files)
+        # load pipeline
+        try:
+            pipeline = pipelines.load_from_root_folder(project, model.root_folder)
+
+            # iterate over media files
+            index = 0
+            for file in media_files:
+                # remove old predictions
+                file.remove_predictions()
+
+                # create new predictions
+                pipeline.execute(storage, file)
+
+                # commit changes and yield progress
+                db.session.commit()
+                yield index / length, notifications
+
+                index += 1
+
         finally:
         finally:
-            if database_copy is not None:
-                database_copy.close()
+            if pipeline is not None:
+                pipelines.free_instance(model.root_folder)
+
 
 
     @staticmethod
     @staticmethod
     def progress(progress: float, notifications: NotificationList):
     def progress(progress: float, notifications: NotificationList):

+ 73 - 64
pycs/frontend/endpoints/projects/CreateProject.py

@@ -1,13 +1,20 @@
+import os
+import shutil
+import uuid
+
 from contextlib import closing
 from contextlib import closing
-from os import mkdir
-from os import path
-from shutil import copytree
-from uuid import uuid1
+from pathlib import Path
 
 
-from flask import make_response, request, abort
+from flask import abort
+from flask import make_response
+from flask import request
 from flask.views import View
 from flask.views import View
 
 
-from pycs.database.Database import Database
+from pycs import db
+from pycs import settings
+from pycs.database.LabelProvider import LabelProvider
+from pycs.database.Model import Model
+from pycs.database.Project import Project
 from pycs.frontend.endpoints.projects.ExecuteExternalStorage import ExecuteExternalStorage
 from pycs.frontend.endpoints.projects.ExecuteExternalStorage import ExecuteExternalStorage
 from pycs.frontend.endpoints.projects.ExecuteLabelProvider import ExecuteLabelProvider
 from pycs.frontend.endpoints.projects.ExecuteLabelProvider import ExecuteLabelProvider
 from pycs.frontend.notifications.NotificationManager import NotificationManager
 from pycs.frontend.notifications.NotificationManager import NotificationManager
@@ -22,9 +29,8 @@ class CreateProject(View):
     # pylint: disable=arguments-differ
     # pylint: disable=arguments-differ
     methods = ['POST']
     methods = ['POST']
 
 
-    def __init__(self, db: Database, nm: NotificationManager, jobs: JobRunner):
+    def __init__(self, nm: NotificationManager, jobs: JobRunner):
         # pylint: disable=invalid-name
         # pylint: disable=invalid-name
-        self.db = db
         self.nm = nm
         self.nm = nm
         self.jobs = jobs
         self.jobs = jobs
 
 
@@ -32,65 +38,70 @@ class CreateProject(View):
         # extract request data
         # extract request data
         data = request.get_json(force=True)
         data = request.get_json(force=True)
 
 
-        if 'name' not in data or 'description' not in data:
-            return abort(400)
+        name = data.get('name')
+        description = data.get('description')
+
+        if name is None:
+            abort(400, "name argument is missing!")
 
 
-        name = data['name']
-        description = data['description']
+        if description is None:
+            abort(400, "description argument is missing!")
 
 
-        # start transaction
-        with self.db:
-            # find model
-            model_id = int(data['model'])
-            model = self.db.model(model_id)
 
 
-            if model is None:
-                return abort(404)
+        model_id = int(data['model'])
+        model = Model.get_or_404(model_id)
 
 
-            # find label provider
-            if data['label'] is None:
-                label_provider = None
-            else:
-                label_provider_id = int(data['label'])
-                label_provider = self.db.label_provider(label_provider_id)
+        label_provider_id = data.get('label')
+        label_provider = None
 
 
-                if label_provider is None:
-                    return abort(404)
+        if label_provider_id is not None:
+            label_provider = LabelProvider.get_or_404(label_provider_id)
 
 
-            # create project folder
-            project_folder = path.join('projects', str(uuid1()))
-            mkdir(project_folder)
+        # create project folder
+        project_folder = Path(settings.projects_folder, str(uuid.uuid1()))
+        project_folder.mkdir()
 
 
-            temp_folder = path.join(project_folder, 'temp')
-            mkdir(temp_folder)
+        temp_folder = project_folder / 'temp'
+        temp_folder.mkdir()
 
 
-            # check project data directory
-            if data['external'] is None:
-                external_data = False
-                data_folder = path.join(project_folder, 'data')
+        # check project data directory
+        if data['external'] is None:
+            external_data = False
+            data_folder = project_folder / 'data'
+            data_folder.mkdir()
 
 
-                mkdir(data_folder)
-            else:
-                external_data = True
-                data_folder = data['external']
+        else:
+            external_data = True
+            data_folder = Path(data['external'])
 
 
-                # check if exists
-                if not path.exists(data_folder):
-                    return abort(400)
+            # check if exists
+            if not data_folder.exists():
+                return abort(400, f"External folder does not exist: {data_folder}")
 
 
-            # copy model to project folder
-            model_folder = path.join(project_folder, 'model')
-            copytree(model.root_folder, model_folder)
+        # copy model to project folder
+        model_folder = project_folder / 'model'
+        shutil.copytree(model.root_folder, str(model_folder))
 
 
-            model, _ = model.copy_to(f'{model.name} ({name})', model_folder)
+        with db.session.begin_nested():
+            model, is_new = model.copy_to(
+                name=f'{model.name} ({name})',
+                root_folder=str(model_folder),
+                commit=False)
 
 
-            # create entry in database
-            created = self.db.create_project(name, description, model, label_provider,
-                                             project_folder, external_data, data_folder)
+            if not is_new:
+                abort(400, f"Could not copy model! Model in \"{model_folder}\" already exists!")
+
+            project = Project.new(name=name,
+                                  description=description,
+                                  model_id=model_id,
+                                  label_provider_id=label_provider_id,
+                                  root_folder=str(project_folder),
+                                  external_data=external_data,
+                                  data_folder=str(data_folder))
 
 
         # execute label provider and add labels to project
         # execute label provider and add labels to project
         if label_provider is not None:
         if label_provider is not None:
-            ExecuteLabelProvider.execute_label_provider(self.db, self.nm, self.jobs, created,
+            ExecuteLabelProvider.execute_label_provider(self.nm, self.jobs, project,
                                                         label_provider)
                                                         label_provider)
 
 
         # load model and add collections to the project
         # load model and add collections to the project
@@ -99,28 +110,26 @@ class CreateProject(View):
                 return pipeline.collections()
                 return pipeline.collections()
 
 
         def add_collections_to_project(provided_collections):
         def add_collections_to_project(provided_collections):
-            with self.db:
-                for position, collection in enumerate(provided_collections):
-                    created.create_collection(collection['reference'],
-                                              collection['name'],
-                                              collection['description'],
-                                              position + 1,
-                                              collection['autoselect'])
-
-        self.jobs.run(created,
+            with db.session.begin_nested():
+                for position, collection in enumerate(provided_collections, 1):
+                    project.create_collection(commit=False,
+                                              position=position,
+                                              **collection)
+
+        self.jobs.run(project,
                       'Media Collections',
                       'Media Collections',
-                      f'{created.name}',
-                      f'{created.identifier}/media-collections',
+                      f'{project.name}',
+                      f'{project.id}/media-collections',
                       executable=load_model_and_get_collections,
                       executable=load_model_and_get_collections,
                       result=add_collections_to_project)
                       result=add_collections_to_project)
 
 
         # find media files
         # find media files
         if external_data:
         if external_data:
-            ExecuteExternalStorage.find_media_files(self.db, self.nm, self.jobs, created)
+            ExecuteExternalStorage.find_media_files(self.nm, self.jobs, project)
 
 
         # fire event
         # fire event
         self.nm.create_model(model)
         self.nm.create_model(model)
-        self.nm.create_project(created)
+        self.nm.create_project(project)
 
 
         # return success response
         # return success response
         return make_response()
         return make_response()

+ 16 - 17
pycs/frontend/endpoints/projects/EditProjectDescription.py

@@ -1,7 +1,9 @@
-from flask import make_response, abort
-from flask.views import View, request
+from flask import abort
+from flask import make_response
+from flask.views import View
+from flask.views import request
 
 
-from pycs.database.Database import Database
+from pycs.database.Project import Project
 from pycs.frontend.notifications.NotificationManager import NotificationManager
 from pycs.frontend.notifications.NotificationManager import NotificationManager
 
 
 
 
@@ -12,27 +14,24 @@ class EditProjectDescription(View):
     # pylint: disable=arguments-differ
     # pylint: disable=arguments-differ
     methods = ['POST']
     methods = ['POST']
 
 
-    def __init__(self, db: Database, nm: NotificationManager):
+    def __init__(self, nm: NotificationManager):
         # pylint: disable=invalid-name
         # pylint: disable=invalid-name
-        self.db = db
         self.nm = nm
         self.nm = nm
 
 
-    def dispatch_request(self, identifier):
+
+    def dispatch_request(self, project_id: int):
         # extract request data
         # extract request data
         data = request.get_json(force=True)
         data = request.get_json(force=True)
+        description = data.get('description')
+
+        if description is None:
+            abort(400, 'description argument is missing')
 
 
-        if 'description' not in data or not data['description']:
-            return abort(400)
+        project = Project.get_or_404(project_id)
 
 
-        # start transaction
-        with self.db:
-            # find project
-            project = self.db.project(identifier)
-            if project is None:
-                return abort(404)
+        project.description = description
+        project.commit()
 
 
-            # set description
-            project.set_description(data['description'])
-            self.nm.edit_project(project)
+        self.nm.edit_project(project)
 
 
         return make_response()
         return make_response()

+ 17 - 18
pycs/frontend/endpoints/projects/EditProjectName.py

@@ -1,7 +1,9 @@
-from flask import make_response, abort
-from flask.views import View, request
+from flask import abort
+from flask import make_response
+from flask.views import View
+from flask.views import request
 
 
-from pycs.database.Database import Database
+from pycs.database.Project import Project
 from pycs.frontend.notifications.NotificationManager import NotificationManager
 from pycs.frontend.notifications.NotificationManager import NotificationManager
 
 
 
 
@@ -12,27 +14,24 @@ class EditProjectName(View):
     # pylint: disable=arguments-differ
     # pylint: disable=arguments-differ
     methods = ['POST']
     methods = ['POST']
 
 
-    def __init__(self, db: Database, nm: NotificationManager):
+    def __init__(self, nm: NotificationManager):
         # pylint: disable=invalid-name
         # pylint: disable=invalid-name
-        self.db = db
         self.nm = nm
         self.nm = nm
 
 
-    def dispatch_request(self, identifier):
+
+    def dispatch_request(self, project_id: int):
         # extract request data
         # extract request data
         data = request.get_json(force=True)
         data = request.get_json(force=True)
+        name = data.get('name')
+
+        if name is None:
+            abort(400, 'name argument is missing')
 
 
-        if 'name' not in data or not data['name']:
-            return abort(400)
+        project = Project.get_or_404(project_id)
 
 
-        # start transaction
-        with self.db:
-            # find project
-            project = self.db.project(identifier)
-            if project is None:
-                return abort(404)
+        project.name = name
+        project.commit()
 
 
-            # set name
-            project.set_name(data['name'])
-            self.nm.edit_project(project)
+        self.nm.edit_project(project)
 
 
-            return make_response()
+        return make_response()

+ 37 - 34
pycs/frontend/endpoints/projects/ExecuteExternalStorage.py

@@ -1,12 +1,12 @@
-from os import listdir
-from os import path
-from os.path import isfile
-from uuid import uuid1
+import os
+import uuid
 
 
-from flask import make_response, request, abort
+from flask import abort
+from flask import make_response
+from flask import request
 from flask.views import View
 from flask.views import View
 
 
-from pycs.database.Database import Database
+from pycs import db
 from pycs.database.Project import Project
 from pycs.database.Project import Project
 from pycs.frontend.notifications.NotificationManager import NotificationManager
 from pycs.frontend.notifications.NotificationManager import NotificationManager
 from pycs.jobs.JobGroupBusyException import JobGroupBusyException
 from pycs.jobs.JobGroupBusyException import JobGroupBusyException
@@ -21,42 +21,38 @@ class ExecuteExternalStorage(View):
     # pylint: disable=arguments-differ
     # pylint: disable=arguments-differ
     methods = ['POST']
     methods = ['POST']
 
 
-    def __init__(self, db: Database, nm: NotificationManager, jobs: JobRunner):
+    def __init__(self, nm: NotificationManager, jobs: JobRunner):
         # pylint: disable=invalid-name
         # pylint: disable=invalid-name
-        self.db = db
         self.nm = nm
         self.nm = nm
         self.jobs = jobs
         self.jobs = jobs
 
 
-    def dispatch_request(self, identifier):
+    def dispatch_request(self, project_id: int):
         # extract request data
         # extract request data
         data = request.get_json(force=True)
         data = request.get_json(force=True)
 
 
-        if 'execute' not in data or data['execute'] is not True:
+        if not data.get('execute', False):
             return abort(400)
             return abort(400)
 
 
         # find project
         # find project
-        project = self.db.project(identifier)
-        if project is None:
-            return abort(404)
+        project = Project.get_or_404(project_id)
 
 
         if not project.external_data:
         if not project.external_data:
-            return abort(400)
+            return abort(400, "External data is not set!")
 
 
         # execute label provider and add labels to project
         # execute label provider and add labels to project
         try:
         try:
-            self.find_media_files(self.db, self.nm, self.jobs, project)
+            ExecuteExternalStorage.find_media_files(self.nm, self.jobs, project)
         except JobGroupBusyException:
         except JobGroupBusyException:
-            return abort(400)
+            return abort(400, "Job is already running!")
 
 
         return make_response()
         return make_response()
 
 
     @staticmethod
     @staticmethod
-    def find_media_files(db: Database, nm: NotificationManager, jobs: JobRunner, project: Project):
+    def find_media_files(nm: NotificationManager, jobs: JobRunner, project: Project):
         """
         """
         start a job that finds media files in the projects data_folder and adds them to the
         start a job that finds media files in the projects data_folder and adds them to the
         database afterwards
         database afterwards
 
 
-        :param db: database object
         :param nm: notification manager object
         :param nm: notification manager object
         :param jobs: job runner object
         :param jobs: job runner object
         :param project: project
         :param project: project
@@ -65,27 +61,36 @@ class ExecuteExternalStorage(View):
 
 
         # pylint: disable=invalid-name
         # pylint: disable=invalid-name
         # find lists the given data folder and prepares item dictionaries
         # find lists the given data folder and prepares item dictionaries
-        def find():
-            files = listdir(project.data_folder)
+        def find(data_folder):
+            files = os.listdir(data_folder)
             length = len(files)
             length = len(files)
 
 
             elements = []
             elements = []
             current = 0
             current = 0
 
 
             for file_name in files:
             for file_name in files:
-                file_path = path.join(project.data_folder, file_name)
-                if not isfile(file_path):
+                file_path = os.path.join(data_folder, file_name)
+                if not os.path.isfile(file_path):
                     continue
                     continue
 
 
-                file_name, file_extension = path.splitext(file_name)
-                file_size = path.getsize(file_path)
+                file_name, file_extension = os.path.splitext(file_name)
+                file_size = os.path.getsize(file_path)
 
 
                 try:
                 try:
-                    ftype, frames, fps = file_info(project.data_folder, file_name, file_extension)
+                    ftype, frames, fps = file_info(data_folder, file_name, file_extension)
                 except ValueError:
                 except ValueError:
                     continue
                     continue
 
 
-                elements.append((ftype, file_name, file_extension, file_size, frames, fps))
+                file_attrs = dict(
+                    uuid=str(uuid.uuid1()),
+                    file_type=ftype,
+                    name=file_name,
+                    extension=file_extension,
+                    size=file_size,
+                    frames=frames,
+                    fps=fps)
+
+                elements.append(file_attrs)
                 current += 1
                 current += 1
 
 
                 if len(elements) >= 200:
                 if len(elements) >= 200:
@@ -97,13 +102,11 @@ class ExecuteExternalStorage(View):
 
 
         # progress inserts elements into the database and fires events
         # progress inserts elements into the database and fires events
         def progress(elements, current, length):
         def progress(elements, current, length):
-            with db:
-                for ftype, file_name, file_extension, file_size, frames, fps in elements:
-                    uuid = str(uuid1())
-                    file, insert = project.add_file(uuid, ftype, file_name, file_extension,
-                                                    file_size, file_name, frames, fps)
+            with db.session.begin_nested():
+                for file_attrs in elements:
+                    file, is_new = project.add_file(commit=False, **file_attrs)
 
 
-                    if insert:
+                    if is_new:
                         nm.create_file(file)
                         nm.create_file(file)
 
 
             return current / length
             return current / length
@@ -112,6 +115,6 @@ class ExecuteExternalStorage(View):
         jobs.run(project,
         jobs.run(project,
                  'Find Media Files',
                  'Find Media Files',
                  project.name,
                  project.name,
-                 f'{project.identifier}/find-files',
-                 find,
+                 f'{project.id}/find-files',
+                 find, project.data_folder,
                  progress=progress)
                  progress=progress)

+ 20 - 24
pycs/frontend/endpoints/projects/ExecuteLabelProvider.py

@@ -1,9 +1,11 @@
 from contextlib import closing
 from contextlib import closing
 
 
-from flask import make_response, request, abort
+from flask import abort
+from flask import make_response
+from flask import request
 from flask.views import View
 from flask.views import View
 
 
-from pycs.database.Database import Database
+from pycs import db
 from pycs.database.LabelProvider import LabelProvider
 from pycs.database.LabelProvider import LabelProvider
 from pycs.database.Project import Project
 from pycs.database.Project import Project
 from pycs.frontend.notifications.NotificationManager import NotificationManager
 from pycs.frontend.notifications.NotificationManager import NotificationManager
@@ -18,45 +20,41 @@ class ExecuteLabelProvider(View):
     # pylint: disable=arguments-differ
     # pylint: disable=arguments-differ
     methods = ['POST']
     methods = ['POST']
 
 
-    def __init__(self, db: Database, nm: NotificationManager, jobs: JobRunner):
+    def __init__(self, nm: NotificationManager, jobs: JobRunner):
         # pylint: disable=invalid-name
         # pylint: disable=invalid-name
-        self.db = db
         self.nm = nm
         self.nm = nm
         self.jobs = jobs
         self.jobs = jobs
 
 
-    def dispatch_request(self, identifier):
+    def dispatch_request(self, project_id: int):
         # extract request data
         # extract request data
         data = request.get_json(force=True)
         data = request.get_json(force=True)
 
 
-        if 'execute' not in data or data['execute'] is not True:
-            return abort(400)
+        if not data.get('execute', False):
+            abort(400, "execute flag is missing")
 
 
         # find project
         # find project
-        project = self.db.project(identifier)
-        if project is None:
-            return abort(404)
+        project = Project.get_or_404(project_id)
 
 
         # get label provider
         # get label provider
-        label_provider = project.label_provider()
+        label_provider = project.label_provider
         if label_provider is None:
         if label_provider is None:
-            return abort(400)
+            abort(400, "This project does not have a label provider.")
 
 
         # execute label provider and add labels to project
         # execute label provider and add labels to project
         try:
         try:
-            self.execute_label_provider(self.db, self.nm, self.jobs, project, label_provider)
+            self.execute_label_provider(self.nm, self.jobs, project, label_provider)
         except JobGroupBusyException:
         except JobGroupBusyException:
-            return abort(400)
+            abort(400, "Label provider already running.")
 
 
         return make_response()
         return make_response()
 
 
     @staticmethod
     @staticmethod
-    def execute_label_provider(db: Database, nm: NotificationManager, jobs: JobRunner,
+    def execute_label_provider(nm: NotificationManager, jobs: JobRunner,
                                project: Project, label_provider: LabelProvider):
                                project: Project, label_provider: LabelProvider):
         """
         """
         start a job that loads and executes a label provider and saves its results to the
         start a job that loads and executes a label provider and saves its results to the
         database afterwards
         database afterwards
 
 
-        :param db: database object
         :param nm: notification manager object
         :param nm: notification manager object
         :param jobs: job runner object
         :param jobs: job runner object
         :param project: project
         :param project: project
@@ -68,18 +66,16 @@ class ExecuteLabelProvider(View):
         # receive loads and executes the given label provider
         # receive loads and executes the given label provider
         def receive():
         def receive():
             with closing(label_provider.load()) as label_provider_impl:
             with closing(label_provider.load()) as label_provider_impl:
-                provided_labels = label_provider_impl.get_labels()
-                return provided_labels
+                return label_provider_impl.get_labels()
 
 
         # result adds the received labels to the database and fires events
         # result adds the received labels to the database and fires events
         def result(provided_labels):
         def result(provided_labels):
-            with db:
+            with db.session.begin_nested():
                 for label in provided_labels:
                 for label in provided_labels:
-                    created_label, insert = project.create_label(
-                        label['name'], label['reference'], label['parent'], label['hierarchy_level']
-                    )
+                    created_label, is_new = project.create_label(commit=False, **label)
+                    project.flush()
 
 
-                    if insert:
+                    if is_new:
                         nm.create_label(created_label)
                         nm.create_label(created_label)
                     else:
                     else:
                         nm.edit_label(created_label)
                         nm.edit_label(created_label)
@@ -88,6 +84,6 @@ class ExecuteLabelProvider(View):
         jobs.run(project,
         jobs.run(project,
                  'Label Provider',
                  'Label Provider',
                  f'{project.name} ({label_provider.name})',
                  f'{project.name} ({label_provider.name})',
-                 f'{project.identifier}/label-provider',
+                 f'{project.id}/label-provider',
                  receive,
                  receive,
                  result=result)
                  result=result)

+ 5 - 13
pycs/frontend/endpoints/projects/GetProjectModel.py

@@ -1,7 +1,7 @@
-from flask import abort, jsonify
+from flask import jsonify
 from flask.views import View
 from flask.views import View
 
 
-from pycs.database.Database import Database
+from pycs.database.Project import Project
 
 
 
 
 class GetProjectModel(View):
 class GetProjectModel(View):
@@ -11,18 +11,10 @@ class GetProjectModel(View):
     # pylint: disable=arguments-differ
     # pylint: disable=arguments-differ
     methods = ['GET']
     methods = ['GET']
 
 
-    def __init__(self, db: Database):
-        # pylint: disable=invalid-name
-        self.db = db
 
 
-    def dispatch_request(self, identifier):
+    def dispatch_request(self, project_id: int):
         # find project
         # find project
-        project = self.db.project(identifier)
-        if project is None:
-            abort(404)
-
-        # get model
-        model = project.model()
+        project = Project.get_or_404(project_id)
 
 
         # return model
         # return model
-        return jsonify(model)
+        return jsonify(project.model)

+ 0 - 40
pycs/frontend/endpoints/projects/ListCollections.py

@@ -1,40 +0,0 @@
-from flask import abort, jsonify
-from flask.views import View
-
-from pycs.database.Database import Database
-
-
-class ListCollections(View):
-    """
-    return a list of collections for a given project
-    """
-    # pylint: disable=arguments-differ
-    methods = ['GET']
-
-    def __init__(self, db: Database):
-        # pylint: disable=invalid-name
-        self.db = db
-
-    def dispatch_request(self, project_id: int):
-        # find project
-        project = self.db.project(project_id)
-
-        if project is None:
-            return abort(404)
-
-        # get collection list
-        collections = project.collections()
-
-        # disable autoselect if there are no elements in the collection
-        found = False
-
-        for collection in collections:
-            if collection.autoselect:
-                if found:
-                    collection.autoselect = False
-                elif collection.count_files() == 0:
-                    collection.autoselect = False
-                    found = True
-
-        # return files
-        return jsonify(collections)

+ 24 - 0
pycs/frontend/endpoints/projects/ListProjectCollections.py

@@ -0,0 +1,24 @@
+from flask import abort, jsonify
+from flask.views import View
+
+from pycs.database.Project import Project
+from pycs.database.Collection import Collection
+
+
+class ListProjectCollections(View):
+    """
+    return a list of collections for a given project
+    """
+    # pylint: disable=arguments-differ
+    methods = ['GET']
+
+
+    def dispatch_request(self, project_id: int):
+        # find project
+        project = Project.get_or_404(project_id)
+
+        # get collection list
+        collections = Collection.update_autoselect(project.collections)
+
+        # return files
+        return jsonify(collections)

+ 14 - 15
pycs/frontend/endpoints/projects/ListFiles.py → pycs/frontend/endpoints/projects/ListProjectFiles.py

@@ -1,41 +1,40 @@
-from flask import abort, jsonify
+from flask import abort
+from flask import jsonify
 from flask.views import View
 from flask.views import View
 
 
-from pycs.database.Database import Database
+from pycs.database.Project import Project
 
 
 
 
-class ListFiles(View):
+class ListProjectFiles(View):
     """
     """
     return a list of files for a given project
     return a list of files for a given project
     """
     """
     # pylint: disable=arguments-differ
     # pylint: disable=arguments-differ
     methods = ['GET']
     methods = ['GET']
 
 
-    def __init__(self, db: Database):
-        # pylint: disable=invalid-name
-        self.db = db
 
 
     def dispatch_request(self, project_id: int, start: int, length: int, collection_id: int = None):
     def dispatch_request(self, project_id: int, start: int, length: int, collection_id: int = None):
         # find project
         # find project
-        project = self.db.project(project_id)
-        if project is None:
-            return abort(404)
+
+        project = Project.get_or_404(project_id)
 
 
         # get count and files
         # get count and files
         if collection_id is not None:
         if collection_id is not None:
             if collection_id == 0:
             if collection_id == 0:
                 count = project.count_files_without_collection()
                 count = project.count_files_without_collection()
-                files = list(project.files_without_collection(start, length))
+                files = project.files_without_collection(start, length)
+
             else:
             else:
                 collection = project.collection(collection_id)
                 collection = project.collection(collection_id)
                 if collection is None:
                 if collection is None:
-                    return abort(404)
+                    abort(404)
+
+                count = collection.files.count()
+                files = collection.get_files(start, length).all()
 
 
-                count = collection.count_files()
-                files = list(collection.files(start, length))
         else:
         else:
-            count = project.count_files()
-            files = list(project.files(start, length))
+            count = project.files.count()
+            files = project.get_files(start, length).all()
 
 
         # return files
         # return files
         return jsonify({
         return jsonify({

+ 16 - 28
pycs/frontend/endpoints/projects/RemoveProject.py

@@ -1,9 +1,9 @@
-from shutil import rmtree
-
-from flask import make_response, request, abort
+from flask import abort
+from flask import make_response
+from flask import request
 from flask.views import View
 from flask.views import View
 
 
-from pycs.database.Database import Database
+from pycs.database.Project import Project
 from pycs.frontend.notifications.NotificationManager import NotificationManager
 from pycs.frontend.notifications.NotificationManager import NotificationManager
 
 
 
 
@@ -14,37 +14,25 @@ class RemoveProject(View):
     # pylint: disable=arguments-differ
     # pylint: disable=arguments-differ
     methods = ['POST']
     methods = ['POST']
 
 
-    def __init__(self, db: Database, nm: NotificationManager):
+    def __init__(self,nm: NotificationManager):
         # pylint: disable=invalid-name
         # pylint: disable=invalid-name
-        self.db = db
         self.nm = nm
         self.nm = nm
 
 
-    def dispatch_request(self, identifier):
+    def dispatch_request(self, project_id: int):
         # extract request data
         # extract request data
         data = request.get_json(force=True)
         data = request.get_json(force=True)
 
 
-        if 'remove' not in data or data['remove'] is not True:
-            abort(400)
-
-        # start transaction
-        with self.db:
-            # find project
-            project = self.db.project(identifier)
-            if project is None:
-                abort(404)
-
-            # remove from database
-            project.remove()
+        if not data.get('remove', False):
+            abort(400, "remove flag is missing")
 
 
-            # remove model from database
-            model = project.model()
-            model.remove()
+        # find project
+        project = Project.get_or_404(project_id)
 
 
-            # remove from file system
-            rmtree(project.root_folder)
+        # remove from database
+        project_dump, model_dump = project.delete()
 
 
-            # send update
-            self.nm.remove_model(model)
-            self.nm.remove_project(project)
+        # send update
+        self.nm.remove_model(model_dump)
+        self.nm.remove_project(project_dump)
 
 
-            return make_response()
+        return make_response()

+ 6 - 11
pycs/frontend/endpoints/results/ConfirmResult.py

@@ -1,7 +1,7 @@
 from flask import make_response, request, abort
 from flask import make_response, request, abort
 from flask.views import View
 from flask.views import View
 
 
-from pycs.database.Database import Database
+from pycs.database.Result import Result
 from pycs.frontend.notifications.NotificationManager import NotificationManager
 from pycs.frontend.notifications.NotificationManager import NotificationManager
 
 
 
 
@@ -12,26 +12,21 @@ class ConfirmResult(View):
     # pylint: disable=arguments-differ
     # pylint: disable=arguments-differ
     methods = ['POST']
     methods = ['POST']
 
 
-    def __init__(self, db: Database, nm: NotificationManager):
+    def __init__(self, nm: NotificationManager):
         # pylint: disable=invalid-name
         # pylint: disable=invalid-name
-        self.db = db
         self.nm = nm
         self.nm = nm
 
 
     def dispatch_request(self, result_id: int):
     def dispatch_request(self, result_id: int):
         # extract request data
         # extract request data
         data = request.get_json(force=True)
         data = request.get_json(force=True)
 
 
-        if 'confirm' not in data or data['confirm'] is not True:
-            return abort(400)
+        if not data.get('confirm', False):
+            return abort(400, "confirm flag is missing")
 
 
         # find result
         # find result
-        result = self.db.result(result_id)
-        if result is None:
-            return abort(404)
+        result = Result.get_or_404(result_id)
 
 
-        # start transaction
-        with self.db:
-            result.set_origin('user')
+        result.set_origin('user')
 
 
         self.nm.edit_result(result)
         self.nm.edit_result(result)
         return make_response()
         return make_response()

+ 43 - 33
pycs/frontend/endpoints/results/CreateResult.py

@@ -1,7 +1,7 @@
 from flask import request, abort, jsonify
 from flask import request, abort, jsonify
 from flask.views import View
 from flask.views import View
 
 
-from pycs.database.Database import Database
+from pycs.database.File import File
 from pycs.frontend.notifications.NotificationManager import NotificationManager
 from pycs.frontend.notifications.NotificationManager import NotificationManager
 
 
 
 
@@ -12,51 +12,61 @@ class CreateResult(View):
     # pylint: disable=arguments-differ
     # pylint: disable=arguments-differ
     methods = ['POST']
     methods = ['POST']
 
 
-    def __init__(self, db: Database, nm: NotificationManager):
+    def __init__(self, nm: NotificationManager):
         # pylint: disable=invalid-name
         # pylint: disable=invalid-name
-        self.db = db
         self.nm = nm
         self.nm = nm
 
 
-    def dispatch_request(self, file_id: int):
-        # extract request data
+    def extract_request_data(self):
         request_data = request.get_json(force=True)
         request_data = request.get_json(force=True)
 
 
         if 'type' not in request_data:
         if 'type' not in request_data:
-            return abort(400)
-        if request_data['type'] not in ['labeled-image', 'bounding-box']:
-            return abort(400)
-
-        rtype = request_data['type']
-
-        if 'label' in request_data and request_data['label']:
-            label = request_data['label']
-        elif request_data['type'] == 'labeled-image':
-            return abort(400)
-        else:
-            label = None
-
-        if 'data' in request_data and request_data['data']:
-            data = request_data['data']
-        elif request_data['type'] == 'bounding-box':
-            return abort(400)
-        else:
-            data = {}
+            abort(400, "result type argument is missing")
+
+        result_type = request_data.get('type')
+        if result_type not in ['labeled-image', 'bounding-box']:
+            abort(400, "result type must be either 'labeled-image' or 'bounding-box'")
+
+        label = None
+        data = {}
+
+        if result_type == 'labeled-image':
+            label = request_data.get('label')
+            if label is None:
+                abort(400, f"Could not find label argument ({result_type=})")
+
+        elif result_type == 'bounding-box':
+            data = request_data.get('data')
+            if data is None:
+                abort(400, f"Could not find data argument ({result_type=})")
+
+        return result_type, label, data
+
+
+    def dispatch_request(self, file_id: int):
+
+        result_type, label, data = self.extract_request_data()
 
 
         # find file
         # find file
-        file = self.db.file(file_id)
-        if file is None:
-            return abort(404)
+        file = File.get_or_404(file_id)
 
 
         # start transaction
         # start transaction
-        with self.db:
+        with db.session.begin_nested():
             # find full-image labels and remove them
             # find full-image labels and remove them
-            for result in file.results():
-                if result.type == 'labeled-image':
-                    result.remove()
-                    self.nm.remove_result(result)
+            image_results = file.results.filter_by(type='labeled-image')
+
+            removed = [result.serialize() for result in image_results.all()]
+            image_results.delete()
+
+            for result in removed:
+                self.nm.remove_result(result)
 
 
             # insert into database
             # insert into database
-            result = file.create_result('user', rtype, label, data)
+            result = file.create_result(
+                origin='user',
+                result_type=result_type,
+                label=label,
+                data=data)
+
             self.nm.create_result(result)
             self.nm.create_result(result)
 
 
         return jsonify(result)
         return jsonify(result)

+ 14 - 16
pycs/frontend/endpoints/results/EditResultData.py

@@ -1,7 +1,9 @@
-from flask import make_response, abort
-from flask.views import View, request
+from flask import abort
+from flask import make_response
+from flask.views import View
+from flask.views import request
 
 
-from pycs.database.Database import Database
+from pycs.database.Result import Result
 from pycs.frontend.notifications.NotificationManager import NotificationManager
 from pycs.frontend.notifications.NotificationManager import NotificationManager
 
 
 
 
@@ -12,27 +14,23 @@ class EditResultData(View):
     # pylint: disable=arguments-differ
     # pylint: disable=arguments-differ
     methods = ['POST']
     methods = ['POST']
 
 
-    def __init__(self, db: Database, nm: NotificationManager):
+    def __init__(self, nm: NotificationManager):
         # pylint: disable=invalid-name
         # pylint: disable=invalid-name
-        self.db = db
         self.nm = nm
         self.nm = nm
 
 
     def dispatch_request(self, result_id: int):
     def dispatch_request(self, result_id: int):
         # extract request data
         # extract request data
-        data = request.get_json(force=True)
+        request_data = request.get_json(force=True)
+        data = request_data.get('data')
 
 
-        if 'data' not in data:
-            return abort(400)
+        if data is None:
+            abort(400, "Could not find data argument!")
 
 
         # find result
         # find result
-        result = self.db.result(result_id)
-        if result is None:
-            return abort(404)
-
-        # start transaction and set label
-        with self.db:
-            result.set_data(data['data'])
-            result.set_origin('user')
+        result = Result.get_or_404(result_id)
+
+        result.data = data
+        result.set_origin('user', commit=True)
 
 
         self.nm.edit_result(result)
         self.nm.edit_result(result)
         return make_response()
         return make_response()

+ 15 - 17
pycs/frontend/endpoints/results/EditResultLabel.py

@@ -1,7 +1,9 @@
-from flask import make_response, abort
-from flask.views import View, request
+from flask import abort
+from flask import make_response
+from flask.views import View
+from flask.views import request
 
 
-from pycs.database.Database import Database
+from pycs.database.Result import Result
 from pycs.frontend.notifications.NotificationManager import NotificationManager
 from pycs.frontend.notifications.NotificationManager import NotificationManager
 
 
 
 
@@ -12,31 +14,27 @@ class EditResultLabel(View):
     # pylint: disable=arguments-differ
     # pylint: disable=arguments-differ
     methods = ['POST']
     methods = ['POST']
 
 
-    def __init__(self, db: Database, nm: NotificationManager):
+    def __init__(self, nm: NotificationManager):
         # pylint: disable=invalid-name
         # pylint: disable=invalid-name
-        self.db = db
         self.nm = nm
         self.nm = nm
 
 
     def dispatch_request(self, result_id: int):
     def dispatch_request(self, result_id: int):
         # extract request data
         # extract request data
-        data = request.get_json(force=True)
+        request_data = request.get_json(force=True)
 
 
-        if 'label' not in data:
-            return abort(400)
+        if 'label' not in request_data:
+            abort(400, "Could not find label argument!")
 
 
         # find result
         # find result
-        result = self.db.result(result_id)
-        if result is None:
-            return abort(404)
+        result = Result.get_or_404(result_id)
+        label = request_data.get('label')
 
 
         # abort if label is empty for labeled-images
         # abort if label is empty for labeled-images
-        if result.type == 'labeled-image' and not data['label']:
-            return abort(400)
+        if result.type == 'labeled-image' and label is None:
+            return abort(400, "Label is required for 'labeled-images' results")
 
 
-        # start transaction and set label
-        with self.db:
-            result.set_label(data['label'])
-            result.set_origin('user')
+        result.label = label
+        result.set_origin('user', commit=True)
 
 
         self.nm.edit_result(result)
         self.nm.edit_result(result)
         return make_response()
         return make_response()

+ 5 - 9
pycs/frontend/endpoints/results/GetProjectResults.py

@@ -1,7 +1,8 @@
-from flask import abort, jsonify
+from flask import abort
+from flask import jsonify
 from flask.views import View
 from flask.views import View
 
 
-from pycs.database.Database import Database
+from pycs.database.Project import Project
 from pycs.interfaces.MediaStorage import MediaStorage
 from pycs.interfaces.MediaStorage import MediaStorage
 
 
 
 
@@ -12,18 +13,13 @@ class GetProjectResults(View):
     # pylint: disable=arguments-differ
     # pylint: disable=arguments-differ
     methods = ['GET']
     methods = ['GET']
 
 
-    def __init__(self, db: Database):
-        # pylint: disable=invalid-name
-        self.db = db
 
 
     def dispatch_request(self, project_id: int):
     def dispatch_request(self, project_id: int):
         # get project from database
         # get project from database
-        project = self.db.project(project_id)
-        if project is None:
-            return abort(404)
+        project = Project.get_or_404(project_id)
 
 
         # map media files to a dict
         # map media files to a dict
-        storage = MediaStorage(self.db, project.identifier, None)
+        storage = MediaStorage(project, None)
         files = list(map(lambda f: f.serialize(), storage.files().iter()))
         files = list(map(lambda f: f.serialize(), storage.files().iter()))
 
 
         # return result
         # return result

+ 3 - 11
pycs/frontend/endpoints/results/GetResults.py

@@ -1,7 +1,7 @@
 from flask import abort, jsonify
 from flask import abort, jsonify
 from flask.views import View
 from flask.views import View
 
 
-from pycs.database.Database import Database
+from pycs.database.File import File
 
 
 
 
 class GetResults(View):
 class GetResults(View):
@@ -11,18 +11,10 @@ class GetResults(View):
     # pylint: disable=arguments-differ
     # pylint: disable=arguments-differ
     methods = ['GET']
     methods = ['GET']
 
 
-    def __init__(self, db: Database):
-        # pylint: disable=invalid-name
-        self.db = db
 
 
     def dispatch_request(self, file_id: int):
     def dispatch_request(self, file_id: int):
         # get file from database
         # get file from database
-        file = self.db.file(file_id)
-        if file is None:
-            return abort(404)
-
-        # get results
-        results = file.results()
+        file = File.get_or_404(file_id)
 
 
         # return result
         # return result
-        return jsonify(results)
+        return jsonify(file.results.all())

+ 10 - 13
pycs/frontend/endpoints/results/RemoveResult.py

@@ -1,7 +1,9 @@
-from flask import make_response, request, abort
+from flask import abort
+from flask import make_response
+from flask import request
 from flask.views import View
 from flask.views import View
 
 
-from pycs.database.Database import Database
+from pycs.database.Result import Result
 from pycs.frontend.notifications.NotificationManager import NotificationManager
 from pycs.frontend.notifications.NotificationManager import NotificationManager
 
 
 
 
@@ -12,26 +14,21 @@ class RemoveResult(View):
     # pylint: disable=arguments-differ
     # pylint: disable=arguments-differ
     methods = ['POST']
     methods = ['POST']
 
 
-    def __init__(self, db: Database, nm: NotificationManager):
+    def __init__(self, nm: NotificationManager):
         # pylint: disable=invalid-name
         # pylint: disable=invalid-name
-        self.db = db
         self.nm = nm
         self.nm = nm
 
 
     def dispatch_request(self, result_id: int):
     def dispatch_request(self, result_id: int):
         # extract request data
         # extract request data
         data = request.get_json(force=True)
         data = request.get_json(force=True)
 
 
-        if 'remove' not in data or data['remove'] is not True:
-            abort(400)
+        if not data.get('remove', False):
+            abort(400, "remove flag is missing")
 
 
         # find result
         # find result
-        result = self.db.result(result_id)
-        if result is None:
-            return abort(404)
+        result = Result.get_or_404(result_id)
 
 
-        # start transaction
-        with self.db:
-            result.remove()
+        dump = result.delete()
 
 
-        self.nm.remove_result(result)
+        self.nm.remove_result(dump)
         return make_response()
         return make_response()

+ 14 - 16
pycs/frontend/endpoints/results/ResetResults.py

@@ -1,7 +1,9 @@
-from flask import make_response, abort
-from flask.views import View, request
+from flask import abort
+from flask import make_response
+from flask.views import View
+from flask.views import request
 
 
-from pycs.database.Database import Database
+from pycs.database.File import File
 from pycs.frontend.notifications.NotificationManager import NotificationManager
 from pycs.frontend.notifications.NotificationManager import NotificationManager
 
 
 
 
@@ -12,32 +14,28 @@ class ResetResults(View):
     # pylint: disable=arguments-differ
     # pylint: disable=arguments-differ
     methods = ['POST']
     methods = ['POST']
 
 
-    def __init__(self, db: Database, nm: NotificationManager):
+    def __init__(self, nm: NotificationManager):
         # pylint: disable=invalid-name
         # pylint: disable=invalid-name
-        self.db = db
         self.nm = nm
         self.nm = nm
 
 
     def dispatch_request(self, file_id: int):
     def dispatch_request(self, file_id: int):
         # extract request data
         # extract request data
         data = request.get_json(force=True)
         data = request.get_json(force=True)
 
 
-        if 'reset' not in data or data['reset'] is not True:
+        if not data.get('reset', False):
             abort(400)
             abort(400)
 
 
         # find file
         # find file
-        file = self.db.file(file_id)
-        if file is None:
-            return abort(404)
+        file = File.get_or_404(file_id)
 
 
-        # get results
-        results = file.results()
+        removed = []
 
 
-        # start transaction
-        with self.db:
-            for result in results:
-                result.remove()
+        for result in file.results.all():
+            removed.append(result.serialize())
 
 
-        for result in results:
+        file.results.delete()
+
+        for result in removed:
             self.nm.remove_result(result)
             self.nm.remove_result(result)
 
 
         return make_response()
         return make_response()

+ 4 - 13
pycs/frontend/endpoints/results/ResultAsCrop.py

@@ -4,7 +4,7 @@ from flask import abort
 from flask import send_from_directory
 from flask import send_from_directory
 from flask.views import View
 from flask.views import View
 
 
-from pycs.database.Database import Database
+from pycs.database.Result import Result
 from pycs.util import file_ops
 from pycs.util import file_ops
 
 
 
 
@@ -15,23 +15,16 @@ class ResultAsCrop(View):
     # pylint: disable=arguments-differ
     # pylint: disable=arguments-differ
     methods = ['GET']
     methods = ['GET']
 
 
-    def __init__(self, db: Database):
-        # pylint: disable=invalid-name
-        self.db = db
-
 
 
     def dispatch_request(self, result_id: int, max_width: int = 2**24, max_height: int = 2**24):
     def dispatch_request(self, result_id: int, max_width: int = 2**24, max_height: int = 2**24):
 
 
         # find result
         # find result
-        result = self.db.result(result_id)
-
-        if result is None:
-            abort(404)
+        result = Result.get_or_404(result_id)
 
 
         if result.type != "bounding-box":
         if result.type != "bounding-box":
             abort(400, f"The type of the queried result was not \"bounding-box\"! It was {result.type}")
             abort(400, f"The type of the queried result was not \"bounding-box\"! It was {result.type}")
 
 
-        file = result.file()
+        file = result.file
 
 
         if file.type != "image":
         if file.type != "image":
             abort(400, f"Currently only supporting images!")
             abort(400, f"Currently only supporting images!")
@@ -48,9 +41,7 @@ class ResultAsCrop(View):
 
 
         x, y, w, h = xywh
         x, y, w, h = xywh
 
 
-        project = file.project()
-
-        crop_path, crop_fname = file_ops.crop_file(file, project.root_folder, x, y, w, h)
+        crop_path, crop_fname = file_ops.crop_file(file, file.project.root_folder, x, y, w, h)
 
 
         parts = os.path.splitext(crop_fname)
         parts = os.path.splitext(crop_fname)
 
 

+ 14 - 6
pycs/frontend/util/JSONEncoder.py

@@ -1,22 +1,30 @@
-from typing import Any
+import datetime
 
 
 from flask.json import JSONEncoder as Base
 from flask.json import JSONEncoder as Base
+from typing import Any
 
 
-from pycs.database.util.JSONEncoder import JSONEncoder as Database
-from pycs.jobs.util.JSONEncoder import JSONEncoder as Jobs
+from pycs.database.util.JSONEncoder import JSONEncoder as DatabaseEncoder
+from pycs.jobs.util.JSONEncoder import JSONEncoder as JobsEncoder
 
 
 
 
 class JSONEncoder(Base):
 class JSONEncoder(Base):
     """
     """
-    prepares job objects to be json encoded
+    prepares job and DB objects to be json encoded
     """
     """
 
 
     def default(self, o: Any) -> Any:
     def default(self, o: Any) -> Any:
         module = o.__class__.__module__
         module = o.__class__.__module__
 
 
         if module.startswith('pycs.database'):
         if module.startswith('pycs.database'):
-            return Database().default(o)
+            return DatabaseEncoder().default(o)
+
         if module.startswith('pycs.jobs'):
         if module.startswith('pycs.jobs'):
-            return Jobs().default(o)
+            return JobsEncoder().default(o)
+
+        if isinstance(o, datetime.datetime):
+            return str(o)
+
+        if isinstance(o, dict):
+            return o
 
 
         return o.__dict__
         return o.__dict__

+ 5 - 8
pycs/interfaces/MediaFile.py

@@ -89,15 +89,12 @@ class MediaFile:
             self.__notifications.add(self.__notifications.notifications.remove_result, result)
             self.__notifications.add(self.__notifications.notifications.remove_result, result)
 
 
     def __get_results(self, origin: str) -> List[Union[MediaImageLabel, MediaBoundingBox]]:
     def __get_results(self, origin: str) -> List[Union[MediaImageLabel, MediaBoundingBox]]:
-        def map_r(result: Result) -> Union[MediaImageLabel, MediaBoundingBox]:
-            if result.type == 'labeled-image':
-                return MediaImageLabel(result)
 
 
-            return MediaBoundingBox(result)
+        def result_to_media(result: Result) -> Union[MediaImageLabel, MediaBoundingBox]:
+            cls = MediaImageLabel if result.type == 'labeled-image' else MediaBoundingBox
+            return cls(result)
 
 
-        return list(map(map_r,
-                        filter(lambda r: r.origin == origin,
-                               self.__file.results())))
+        return [result_to_media(r) for r in self.__file.results.filter_by(origin=origin).all()]
 
 
     def results(self) -> List[Union[MediaImageLabel, MediaBoundingBox]]:
     def results(self) -> List[Union[MediaImageLabel, MediaBoundingBox]]:
         """
         """
@@ -127,7 +124,7 @@ class MediaFile:
             'frames': self.frames,
             'frames': self.frames,
             'fps': self.fps,
             'fps': self.fps,
             'path': self.path,
             'path': self.path,
-            'filename': self.__file.name + self.__file.extension,
+            'filename': self.__file.filename,
             'results': list(map(lambda r: r.serialize(), self.results())),
             'results': list(map(lambda r: r.serialize(), self.results())),
             'predictions': list(map(lambda r: r.serialize(), self.predictions())),
             'predictions': list(map(lambda r: r.serialize(), self.predictions())),
         }
         }

+ 3 - 3
pycs/interfaces/MediaFileList.py

@@ -49,11 +49,11 @@ class MediaFileList:
             source = self.__project
             source = self.__project
 
 
         if self.__label is None:
         if self.__label is None:
-            for file in source.files():
+            for file in source.files.all():
                 yield MediaFile(file, self.__notifications)
                 yield MediaFile(file, self.__notifications)
         else:
         else:
-            for file in source.files():
-                for result in file.results():
+            for file in source.files.all():
+                for result in file.results.all():
                     if result.label == self.__label:
                     if result.label == self.__label:
                         yield MediaFile(file, self.__notifications)
                         yield MediaFile(file, self.__notifications)
                         break
                         break

+ 1 - 1
pycs/interfaces/MediaLabel.py

@@ -7,7 +7,7 @@ class MediaLabel:
     """
     """
 
 
     def __init__(self, label: Label):
     def __init__(self, label: Label):
-        self.identifier = label.identifier
+        self.identifier = label.id
         self.parent = None
         self.parent = None
         self.children = []
         self.children = []
         self.reference = label.reference
         self.reference = label.reference

+ 9 - 12
pycs/interfaces/MediaStorage.py

@@ -1,6 +1,6 @@
 from typing import List
 from typing import List
 
 
-from pycs.database.Database import Database
+from pycs.database.Project import Project
 from pycs.frontend.notifications.NotificationList import NotificationList
 from pycs.frontend.notifications.NotificationList import NotificationList
 from pycs.interfaces.MediaFileList import MediaFileList
 from pycs.interfaces.MediaFileList import MediaFileList
 from pycs.interfaces.MediaLabel import MediaLabel
 from pycs.interfaces.MediaLabel import MediaLabel
@@ -11,13 +11,13 @@ class MediaStorage:
     helper class for pipelines to interact with database entities
     helper class for pipelines to interact with database entities
     """
     """
 
 
-    def __init__(self, db: Database, project_id: int, notifications: NotificationList = None):
-        self.__db = db
+    def __init__(self, project_id: int, notifications: NotificationList = None):
         self.__project_id = project_id
         self.__project_id = project_id
         self.__notifications = notifications
         self.__notifications = notifications
 
 
-        self.__project = self.__db.project(self.__project_id)
-        self.__collections = self.__project.collections()
+        self.__project = Project.query.get(self.__project_id)
+        # this one is not used anywhere
+        # self.__collections = self.__project.collections.all()
 
 
     def labels(self) -> List[MediaLabel]:
     def labels(self) -> List[MediaLabel]:
         """
         """
@@ -25,12 +25,12 @@ class MediaStorage:
 
 
         :return: list of labels
         :return: list of labels
         """
         """
-        label_list = self.__project.labels()
-        label_dict = {la.identifier: MediaLabel(la) for la in label_list}
+        label_list = self.__project.labels.all()
+        label_dict = {la.label: MediaLabel(la) for la in label_list}
         result = []
         result = []
 
 
         for label in label_list:
         for label in label_list:
-            medial_label = label_dict[label.identifier]
+            medial_label = label_dict[label.id]
 
 
             if label.parent_id is not None:
             if label.parent_id is not None:
                 medial_label.parent = label_dict[label.parent_id]
                 medial_label.parent = label_dict[label.parent_id]
@@ -46,10 +46,7 @@ class MediaStorage:
 
 
         :return: list of root-level labels (parent is None)
         :return: list of root-level labels (parent is None)
         """
         """
-        return list(filter(
-            lambda ml: ml.parent is None,
-            self.labels()
-        ))
+        return [label for label in self.labels() if label.parent is None]
 
 
     def files(self) -> MediaFileList:
     def files(self) -> MediaFileList:
         """
         """

+ 2 - 2
pycs/jobs/Job.py

@@ -11,8 +11,8 @@ class Job:
 
 
     # pylint: disable=too-few-public-methods
     # pylint: disable=too-few-public-methods
     def __init__(self, project: Project, job_type: str, name: str):
     def __init__(self, project: Project, job_type: str, name: str):
-        self.identifier = str(uuid1())
-        self.project_id = project.identifier
+        self.uuid = self.id = str(uuid1())
+        self.project_id = project.id
         self.type = job_type
         self.type = job_type
         self.name = name
         self.name = name
         self.exception = None
         self.exception = None

+ 1 - 1
pycs/jobs/JobRunner.py

@@ -94,7 +94,7 @@ class JobRunner:
         :return:
         :return:
         """
         """
         for i in range(len(self.__jobs)):
         for i in range(len(self.__jobs)):
-            if self.__jobs[i].identifier == identifier:
+            if self.__jobs[i].id == identifier:
                 if self.__jobs[i].finished is not None:
                 if self.__jobs[i].finished is not None:
                     job = self.__jobs[i]
                     job = self.__jobs[i]
                     del self.__jobs[i]
                     del self.__jobs[i]

+ 2 - 0
settings.json

@@ -2,5 +2,7 @@
   "host": "",
   "host": "",
   "port": 5000,
   "port": 5000,
   "allowedOrigins": [],
   "allowedOrigins": [],
+  "projects_folder": "projects",
   "database": "data2.sqlite3"
   "database": "data2.sqlite3"
+
 }
 }

+ 5 - 5
test/test_database.py

@@ -52,7 +52,7 @@ class TestDatabase(unittest.TestCase):
 
 
         # test insert
         # test insert
         for i in range(2):
         for i in range(2):
-            self.assertEqual(models[i].identifier, i + 1)
+            self.assertEqual(models[i].id, i + 1)
             self.assertEqual(models[i].name, f'Model {i + 1}')
             self.assertEqual(models[i].name, f'Model {i + 1}')
             self.assertEqual(models[i].description, f'Description for Model {i + 1}')
             self.assertEqual(models[i].description, f'Description for Model {i + 1}')
             self.assertEqual(models[i].root_folder, f'modeldir{i + 1}')
             self.assertEqual(models[i].root_folder, f'modeldir{i + 1}')
@@ -62,7 +62,7 @@ class TestDatabase(unittest.TestCase):
 
 
         # test copy
         # test copy
         copy, _ = models[0].copy_to('Copied Model', 'modeldir3')
         copy, _ = models[0].copy_to('Copied Model', 'modeldir3')
-        self.assertEqual(copy.identifier, 3)
+        self.assertEqual(copy.id, 3)
         self.assertEqual(copy.name, 'Copied Model')
         self.assertEqual(copy.name, 'Copied Model')
         self.assertEqual(copy.description, 'Description for Model 1')
         self.assertEqual(copy.description, 'Description for Model 1')
         self.assertEqual(copy.root_folder, 'modeldir3')
         self.assertEqual(copy.root_folder, 'modeldir3')
@@ -75,7 +75,7 @@ class TestDatabase(unittest.TestCase):
         self.assertEqual(len(label_providers), 2)
         self.assertEqual(len(label_providers), 2)
 
 
         for i in range(2):
         for i in range(2):
-            self.assertEqual(label_providers[i].identifier, i + 1)
+            self.assertEqual(label_providers[i].id, i + 1)
             self.assertEqual(label_providers[i].name, f'Label Provider {i + 1}')
             self.assertEqual(label_providers[i].name, f'Label Provider {i + 1}')
             self.assertEqual(label_providers[i].description, f'Description for Label Provider {i + 1}')
             self.assertEqual(label_providers[i].description, f'Description for Label Provider {i + 1}')
             self.assertEqual(label_providers[i].root_folder, f'labeldir{i + 1}')
             self.assertEqual(label_providers[i].root_folder, f'labeldir{i + 1}')
@@ -90,12 +90,12 @@ class TestDatabase(unittest.TestCase):
         for i in range(3):
         for i in range(3):
             project = projects[i]
             project = projects[i]
 
 
-            self.assertEqual(project.identifier, i + 1)
+            self.assertEqual(project.id, i + 1)
             self.assertEqual(project.name, f'Project {i + 1}')
             self.assertEqual(project.name, f'Project {i + 1}')
             self.assertEqual(project.description, f'Project Description {i + 1}')
             self.assertEqual(project.description, f'Project Description {i + 1}')
             self.assertEqual(project.model_id, i + 1)
             self.assertEqual(project.model_id, i + 1)
             self.assertEqual(project.model().__dict__, models[i].__dict__)
             self.assertEqual(project.model().__dict__, models[i].__dict__)
-            self.assertEqual(project.label_provider_id, label_providers[i].identifier if i < 2 else None)
+            self.assertEqual(project.label_provider_id, label_providers[i].id if i < 2 else None)
             self.assertEqual(
             self.assertEqual(
                 project.label_provider().__dict__ if project.label_provider() is not None else None,
                 project.label_provider().__dict__ if project.label_provider() is not None else None,
                 label_providers[i].__dict__ if i < 2 else None
                 label_providers[i].__dict__ if i < 2 else None