|
@@ -1,164 +1,108 @@
|
|
|
+import typing as T
|
|
|
+
|
|
|
from contextlib import closing
|
|
|
+from datetime import datetime
|
|
|
from os.path import join
|
|
|
-from time import time
|
|
|
from typing import List, Optional, Tuple, Iterator
|
|
|
|
|
|
+from pycs import db
|
|
|
+from pycs.database.base import NamedBaseModel
|
|
|
+
|
|
|
from pycs.database.Collection import Collection
|
|
|
from pycs.database.File import File
|
|
|
from pycs.database.Label import Label
|
|
|
-from pycs.database.LabelProvider import LabelProvider
|
|
|
-from pycs.database.Model import Model
|
|
|
|
|
|
|
|
|
-class Project:
|
|
|
- """
|
|
|
- database class for projects
|
|
|
- """
|
|
|
+class Project(NamedBaseModel):
|
|
|
+ description = db.Column(db.String)
|
|
|
|
|
|
- def __init__(self, database, row):
|
|
|
- self.database = database
|
|
|
+ created = db.Column(db.DateTime, default=datetime.utcnow,
|
|
|
+ index=True, nullable=False)
|
|
|
|
|
|
- self.identifier = row[0]
|
|
|
- self.name = row[1]
|
|
|
- self.description = row[2]
|
|
|
- self.created = row[3]
|
|
|
- self.model_id = row[4]
|
|
|
- self.label_provider_id = row[5]
|
|
|
- self.root_folder = row[6]
|
|
|
- self.external_data = bool(row[7])
|
|
|
- self.data_folder = row[8]
|
|
|
+ model_id = db.Column(
|
|
|
+ db.Integer,
|
|
|
+ db.ForeignKey("model.id", ondelete="SET NULL"))
|
|
|
|
|
|
- def model(self) -> Model:
|
|
|
- """
|
|
|
- get the model this project is associated with
|
|
|
+ label_provider_id = db.Column(
|
|
|
+ db.Integer,
|
|
|
+ db.ForeignKey("label_provider.id", ondelete="SET NULL"))
|
|
|
|
|
|
- :return: model
|
|
|
- """
|
|
|
- return self.database.model(self.model_id)
|
|
|
+ root_folder = db.Column(db.String, nullable=False, unique=True)
|
|
|
|
|
|
- def label_provider(self) -> Optional[LabelProvider]:
|
|
|
- """
|
|
|
- get the label provider this project is associated with
|
|
|
+ external_data = db.Column(db.Boolean, nullable=False)
|
|
|
|
|
|
- :return: label provider
|
|
|
- """
|
|
|
- if self.label_provider_id is not None:
|
|
|
- return self.database.label_provider(self.label_provider_id)
|
|
|
+ data_folder = db.Column(db.String, nullable=False)
|
|
|
|
|
|
- return None
|
|
|
+ # contraints
|
|
|
+ __table_args__ = ()
|
|
|
|
|
|
- def labels(self) -> List[Label]:
|
|
|
- """
|
|
|
- get a list of labels associated with this project
|
|
|
+ # relationships to other models
|
|
|
+ files = db.relationship("File", backref="project", lazy=True)
|
|
|
+ labels = db.relationship("Label", backref="project", lazy=True)
|
|
|
+ collections = db.relationship("Collection", backref="project", lazy=True)
|
|
|
|
|
|
- :return: list of labels
|
|
|
- """
|
|
|
- with closing(self.database.con.cursor()) as cursor:
|
|
|
- cursor.execute('SELECT * FROM labels WHERE project = ?', [self.identifier])
|
|
|
- return list(map(
|
|
|
- lambda row: Label(self.database, row),
|
|
|
- cursor.fetchall()
|
|
|
- ))
|
|
|
|
|
|
- def label(self, identifier: int) -> Optional[Label]:
|
|
|
+ def label(self, id: int) -> T.Optional[Label]:
|
|
|
"""
|
|
|
get a label using its unique identifier
|
|
|
|
|
|
:param identifier: unique identifier
|
|
|
:return: label
|
|
|
"""
|
|
|
- with closing(self.database.con.cursor()) as cursor:
|
|
|
- cursor.execute('SELECT * FROM labels WHERE id = ? AND project = ?',
|
|
|
- (identifier, self.identifier))
|
|
|
- row = cursor.fetchone()
|
|
|
-
|
|
|
- if row is not None:
|
|
|
- return Label(self.database, row)
|
|
|
-
|
|
|
- return None
|
|
|
+ return self.labels.get(id)
|
|
|
|
|
|
- def create_label(self, name: str, reference: str = None,
|
|
|
- parent_id: int = None) -> Tuple[Optional[Label], bool]:
|
|
|
+ def file(self, id: int) -> T.Optional[Label]:
|
|
|
"""
|
|
|
- create a label for this project. If there is already a label with the same reference
|
|
|
- in the database its name is updated.
|
|
|
+ get a file using its unique identifier
|
|
|
|
|
|
- :param name: label name
|
|
|
- :param reference: label reference
|
|
|
- :param parent_id: parent's identifier
|
|
|
- :return: created or edited label, insert
|
|
|
+ :param identifier: unique identifier
|
|
|
+ :return: file
|
|
|
"""
|
|
|
- created = int(time())
|
|
|
+ return self.files.get(id)
|
|
|
|
|
|
- with closing(self.database.con.cursor()) as cursor:
|
|
|
- cursor.execute('''
|
|
|
- INSERT INTO labels (project, parent, created, reference, name)
|
|
|
- VALUES (?, ?, ?, ?, ?)
|
|
|
- ON CONFLICT (project, reference) DO
|
|
|
- UPDATE SET parent = ?, name = ?
|
|
|
- ''', (self.identifier, parent_id, created, reference, name, parent_id, name))
|
|
|
-
|
|
|
- # lastrowid is 0 if on conflict clause applies.
|
|
|
- # If this is the case we do an extra query to receive the row id.
|
|
|
- if cursor.lastrowid > 0:
|
|
|
- row_id = cursor.lastrowid
|
|
|
- insert = True
|
|
|
- else:
|
|
|
- cursor.execute('SELECT id FROM labels WHERE project = ? AND reference = ?',
|
|
|
- (self.identifier, reference))
|
|
|
- row_id = cursor.fetchone()[0]
|
|
|
- insert = False
|
|
|
-
|
|
|
- return self.label(row_id), insert
|
|
|
-
|
|
|
- def collections(self) -> List[Collection]:
|
|
|
+ def collection(self, id: int) -> T.Optional[Collection]:
|
|
|
"""
|
|
|
- get a list of collections associated with this project
|
|
|
+ get a collection using its unique identifier
|
|
|
|
|
|
- :return: list of collections
|
|
|
+ :param identifier: unique identifier
|
|
|
+ :return: collection
|
|
|
"""
|
|
|
- with closing(self.database.con.cursor()) as cursor:
|
|
|
- cursor.execute('SELECT * FROM collections WHERE project = ? ORDER BY position ASC',
|
|
|
- [self.identifier])
|
|
|
-
|
|
|
- return list(map(
|
|
|
- lambda row: Collection(self.database, row),
|
|
|
- cursor.fetchall()
|
|
|
- ))
|
|
|
+ return self.collections.get(id)
|
|
|
|
|
|
- def collection(self, identifier: int) -> Optional[Collection]:
|
|
|
+ def collection_by_reference(self, reference: str) -> T.Optional[Collection]:
|
|
|
"""
|
|
|
get a collection using its unique identifier
|
|
|
|
|
|
:param identifier: unique identifier
|
|
|
:return: collection
|
|
|
"""
|
|
|
- with closing(self.database.con.cursor()) as cursor:
|
|
|
- cursor.execute('SELECT * FROM collections WHERE id = ? AND project = ?',
|
|
|
- (identifier, self.identifier))
|
|
|
- row = cursor.fetchone()
|
|
|
+ return self.collections.filter_by(reference=reference).one()
|
|
|
|
|
|
- if row is not None:
|
|
|
- return Collection(self.database, row)
|
|
|
-
|
|
|
- return None
|
|
|
-
|
|
|
- def collection_by_reference(self, reference: str):
|
|
|
+ def create_label(self, name: str, reference: str = None,
|
|
|
+ parent_id: int = None) -> Tuple[Optional[Label], bool]:
|
|
|
"""
|
|
|
- get a collection using its reference string
|
|
|
+ create a label for this project. If there is already a label with the same reference
|
|
|
+ in the database its name is updated.
|
|
|
|
|
|
- :param reference: reference string
|
|
|
- :return: collection
|
|
|
+ :param name: label name
|
|
|
+ :param reference: label reference
|
|
|
+ :param parent_id: parent's identifier
|
|
|
+ :return: created or edited label, insert
|
|
|
"""
|
|
|
- with closing(self.database.con.cursor()) as cursor:
|
|
|
- cursor.execute('SELECT * FROM collections WHERE reference = ? AND project = ?',
|
|
|
- (reference, self.identifier))
|
|
|
- row = cursor.fetchone()
|
|
|
|
|
|
- if row is not None:
|
|
|
- return Collection(self.database, row)
|
|
|
+ label = Label.query.get(project=self, reference=reference)
|
|
|
+ is_new = False
|
|
|
|
|
|
- return None
|
|
|
+ if label is None:
|
|
|
+ label = Label.new(project=self, reference=reference)
|
|
|
+ is_new = True
|
|
|
+
|
|
|
+ label.set_name(name)
|
|
|
+ label.set_parent(parent_id)
|
|
|
+
|
|
|
+ self.commit()
|
|
|
+
|
|
|
+ return label, is_new
|
|
|
|
|
|
def create_collection(self,
|
|
|
reference: str,
|
|
@@ -166,74 +110,77 @@ class Project:
|
|
|
description: str,
|
|
|
position: int,
|
|
|
autoselect: bool):
|
|
|
- autoselect = 1 if autoselect else 0
|
|
|
|
|
|
- with closing(self.database.con.cursor()) as cursor:
|
|
|
- cursor.execute('''
|
|
|
- INSERT INTO collections
|
|
|
- (project, reference, name, description, position, autoselect)
|
|
|
- VALUES (?, ?, ?, ?, ?, ?)
|
|
|
- ON CONFLICT (project, reference) DO
|
|
|
- UPDATE SET name = ?, description = ?, position = ?, autoselect = ?
|
|
|
- ''', (self.identifier, reference, name, description, position, autoselect,
|
|
|
- name, description, position, autoselect))
|
|
|
-
|
|
|
- # lastrowid is 0 if on conflict clause applies.
|
|
|
- # If this is the case we do an extra query to receive the row id.
|
|
|
- if cursor.lastrowid > 0:
|
|
|
- row_id = cursor.lastrowid
|
|
|
- insert = True
|
|
|
- else:
|
|
|
- cursor.execute('SELECT id FROM collections WHERE project = ? AND reference = ?',
|
|
|
- (self.identifier, reference))
|
|
|
- row_id = cursor.fetchone()[0]
|
|
|
- insert = False
|
|
|
-
|
|
|
- return self.collection(row_id), insert
|
|
|
-
|
|
|
- def remove(self) -> None:
|
|
|
- """
|
|
|
- remove this project from the database
|
|
|
|
|
|
- :return:
|
|
|
- """
|
|
|
- with closing(self.database.con.cursor()) as cursor:
|
|
|
- cursor.execute('DELETE FROM projects WHERE id = ?', [self.identifier])
|
|
|
+ collection = Collection.query.get(project=self, reference=reference)
|
|
|
+ is_new = False
|
|
|
+
|
|
|
+ if collection is None:
|
|
|
+ collection = Collection.new(project=self,
|
|
|
+ reference=reference)
|
|
|
+ is_new = True
|
|
|
+
|
|
|
+ collection.name = name
|
|
|
+ collection.description = description
|
|
|
+ collection.position = position
|
|
|
+ collection.autoselect = autoselect
|
|
|
+ self.commit()
|
|
|
+
|
|
|
+ return collection, is_new
|
|
|
|
|
|
- def set_name(self, name: str) -> None:
|
|
|
+ def add_file(self, uuid: str, file_type: str, name: str, extension: str, size: int,
|
|
|
+ filename: str, frames: int = None, fps: float = None) -> T.Tuple[File, bool]:
|
|
|
"""
|
|
|
- set this projects name
|
|
|
+ add a file to this project
|
|
|
|
|
|
- :param name: new name
|
|
|
- :return:
|
|
|
+ :param uuid: unique identifier which is used for temporary files
|
|
|
+ :param file_type: file type (either image or video)
|
|
|
+ :param name: file name
|
|
|
+ :param extension: file extension
|
|
|
+ :param size: file size
|
|
|
+ :param filename: actual name in filesystem
|
|
|
+ :param frames: frame count
|
|
|
+ :param fps: frames per second
|
|
|
+ :return: file
|
|
|
"""
|
|
|
- with closing(self.database.con.cursor()) as cursor:
|
|
|
- cursor.execute('UPDATE projects SET name = ? WHERE id = ?', (name, self.identifier))
|
|
|
- self.name = name
|
|
|
+ path = join(self.data_folder, filename + extension)
|
|
|
+
|
|
|
+ file = File.objects.get(project=self, path=path)
|
|
|
+ is_new = False
|
|
|
|
|
|
- def set_description(self, description: str) -> None:
|
|
|
+ if file is None:
|
|
|
+ file = File.new(uuid=uuid, project=self, path=path)
|
|
|
+ is_new = True
|
|
|
+
|
|
|
+ file.type = file_type
|
|
|
+ file.name = name
|
|
|
+ file.extension = extension
|
|
|
+ file.size = size
|
|
|
+ file.frames = frames
|
|
|
+ file.fps = fps
|
|
|
+
|
|
|
+ self.commit()
|
|
|
+ return file, is_new
|
|
|
+
|
|
|
+
|
|
|
+ def set_description(self, description: str):
|
|
|
"""
|
|
|
set this projects description
|
|
|
|
|
|
:param description: new description
|
|
|
:return:
|
|
|
"""
|
|
|
- with closing(self.database.con.cursor()) as cursor:
|
|
|
- cursor.execute('UPDATE projects SET description = ? WHERE id = ?',
|
|
|
- (description, self.identifier))
|
|
|
- self.description = description
|
|
|
-
|
|
|
+ self.description = description
|
|
|
+ self
|
|
|
def count_files(self) -> int:
|
|
|
"""
|
|
|
count files associated with this project
|
|
|
|
|
|
:return: count
|
|
|
"""
|
|
|
- with closing(self.database.con.cursor()) as cursor:
|
|
|
- cursor.execute('SELECT COUNT(*) FROM files WHERE project = ?', [self.identifier])
|
|
|
- return cursor.fetchone()[0]
|
|
|
+ return self.files.count()
|
|
|
|
|
|
- def files(self, offset: int = 0, limit: int = -1) -> Iterator[File]:
|
|
|
+ def get_files(self, offset: int = 0, limit: int = -1) -> T.Iterator[File]:
|
|
|
"""
|
|
|
get an iterator of files associated with this project
|
|
|
|
|
@@ -241,14 +188,7 @@ class Project:
|
|
|
:param limit: file limit
|
|
|
:return: iterator of files
|
|
|
"""
|
|
|
- with closing(self.database.con.cursor()) as cursor:
|
|
|
- cursor.execute('SELECT * FROM files WHERE project = ? ORDER BY id ASC LIMIT ? OFFSET ?',
|
|
|
- (self.identifier, limit, offset))
|
|
|
-
|
|
|
- return map(
|
|
|
- lambda row: File(self.database, row),
|
|
|
- cursor.fetchall()
|
|
|
- )
|
|
|
+ return self.files.order_by(File.id.acs()).offset(offset).limit(limit)
|
|
|
|
|
|
def count_files_without_results(self) -> int:
|
|
|
"""
|
|
@@ -256,6 +196,8 @@ class Project:
|
|
|
|
|
|
:return: count
|
|
|
"""
|
|
|
+ raise NotImplementedError
|
|
|
+
|
|
|
with closing(self.database.con.cursor()) as cursor:
|
|
|
cursor.execute('''
|
|
|
SELECT COUNT(*)
|
|
@@ -271,6 +213,8 @@ class Project:
|
|
|
|
|
|
:return: list of files
|
|
|
"""
|
|
|
+ raise NotImplementedError
|
|
|
+
|
|
|
with closing(self.database.con.cursor()) as cursor:
|
|
|
cursor.execute('''
|
|
|
SELECT files.*
|
|
@@ -283,91 +227,19 @@ class Project:
|
|
|
for row in cursor:
|
|
|
yield File(self.database, row)
|
|
|
|
|
|
- def count_files_without_collection(self) -> int:
|
|
|
- """
|
|
|
- count files associated with this project but with no collection
|
|
|
-
|
|
|
- :return: count
|
|
|
- """
|
|
|
- with closing(self.database.con.cursor()) as cursor:
|
|
|
- cursor.execute('SELECT COUNT(*) FROM files WHERE project = ? AND collection IS NULL',
|
|
|
- [self.identifier])
|
|
|
- return cursor.fetchone()[0]
|
|
|
-
|
|
|
- def files_without_collection(self, offset=0, limit=-1) -> Iterator[File]:
|
|
|
+ def files_without_collection(self, offset: int = 0, limit: int = -1) -> Iterator[File]:
|
|
|
"""
|
|
|
get an iterator of files without not associated with any collection
|
|
|
|
|
|
:return: list of files
|
|
|
"""
|
|
|
- with closing(self.database.con.cursor()) as cursor:
|
|
|
- cursor.execute('''
|
|
|
- SELECT * FROM files
|
|
|
- WHERE files.project = ? AND files.collection IS NULL
|
|
|
- ORDER BY id ASC
|
|
|
- LIMIT ? OFFSET ?
|
|
|
- ''', (self.identifier, limit, offset))
|
|
|
-
|
|
|
- for row in cursor:
|
|
|
- yield File(self.database, row)
|
|
|
-
|
|
|
- def file(self, identifier) -> Optional[File]:
|
|
|
- """
|
|
|
- get a file using its unique identifier
|
|
|
-
|
|
|
- :param identifier: unique identifier
|
|
|
- :return: file
|
|
|
- """
|
|
|
- with closing(self.database.con.cursor()) as cursor:
|
|
|
- cursor.execute('SELECT * FROM files WHERE id = ? AND project = ?',
|
|
|
- (identifier, self.identifier))
|
|
|
- row = cursor.fetchone()
|
|
|
+ return self.get_files(offset, limit).filter(File.collection_id == None)
|
|
|
|
|
|
- if row is not None:
|
|
|
- return File(self.database, row)
|
|
|
|
|
|
- return None
|
|
|
-
|
|
|
- def add_file(self, uuid: str, file_type: str, name: str, extension: str, size: int,
|
|
|
- filename: str, frames: int = None, fps: float = None) -> Tuple[File, bool]:
|
|
|
+ def count_files_without_collection(self) -> int:
|
|
|
"""
|
|
|
- add a file to this project
|
|
|
+ count files associated with this project but with no collection
|
|
|
|
|
|
- :param uuid: unique identifier which is used for temporary files
|
|
|
- :param file_type: file type (either image or video)
|
|
|
- :param name: file name
|
|
|
- :param extension: file extension
|
|
|
- :param size: file size
|
|
|
- :param filename: actual name in filesystem
|
|
|
- :param frames: frame count
|
|
|
- :param fps: frames per second
|
|
|
- :return: file
|
|
|
+ :return: count
|
|
|
"""
|
|
|
- created = int(time())
|
|
|
- path = join(self.data_folder, filename + extension)
|
|
|
-
|
|
|
- with closing(self.database.con.cursor()) as cursor:
|
|
|
- cursor.execute('''
|
|
|
- INSERT INTO files (
|
|
|
- uuid, project, type, name, extension, size, created, path, frames, fps
|
|
|
- )
|
|
|
- VALUES (
|
|
|
- ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
|
|
|
- )
|
|
|
- ON CONFLICT (project, path) DO
|
|
|
- UPDATE SET type = ?, name = ?, extension = ?, size = ?, frames = ?, fps = ?
|
|
|
- ''', (uuid, self.identifier, file_type, name, extension, size, created, path, frames,
|
|
|
- fps, file_type, name, extension, size, frames, fps))
|
|
|
-
|
|
|
- # lastrowid is 0 if on conflict clause applies.
|
|
|
- # If this is the case we do an extra query to receive the row id.
|
|
|
- if cursor.lastrowid > 0:
|
|
|
- row_id = cursor.lastrowid
|
|
|
- insert = True
|
|
|
- else:
|
|
|
- cursor.execute('SELECT id FROM files WHERE project = ? AND path = ?',
|
|
|
- (self.identifier, path))
|
|
|
- row_id = cursor.fetchone()[0]
|
|
|
- insert = False
|
|
|
-
|
|
|
- return self.file(row_id), insert
|
|
|
+ return self.files_without_collection().count()
|