Project.py 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233
  1. import typing as T
  2. from contextlib import closing
  3. from datetime import datetime
  4. from os.path import join
  5. from typing import List, Optional, Tuple, Iterator
  6. from pycs import db
  7. from pycs.database.base import NamedBaseModel
  8. from pycs.database.Collection import Collection
  9. from pycs.database.File import File
  10. from pycs.database.Label import Label
  11. class Project(NamedBaseModel):
  12. description = db.Column(db.String)
  13. created = db.Column(db.DateTime, default=datetime.utcnow,
  14. index=True, nullable=False)
  15. model_id = db.Column(
  16. db.Integer,
  17. db.ForeignKey("model.id", ondelete="SET NULL"))
  18. label_provider_id = db.Column(
  19. db.Integer,
  20. db.ForeignKey("label_provider.id", ondelete="SET NULL"))
  21. root_folder = db.Column(db.String, nullable=False, unique=True)
  22. external_data = db.Column(db.Boolean, nullable=False)
  23. data_folder = db.Column(db.String, nullable=False)
  24. # contraints
  25. __table_args__ = ()
  26. # relationships to other models
  27. files = db.relationship("File", backref="project", lazy="dynamic")
  28. labels = db.relationship("Label", backref="project", lazy="dynamic")
  29. collections = db.relationship("Collection", backref="project", lazy="dynamic")
  30. serialize_rules = (
  31. '-files',
  32. '-labels',
  33. '-collections',
  34. )
  35. def label(self, id: int) -> T.Optional[Label]:
  36. """
  37. get a label using its unique identifier
  38. :param identifier: unique identifier
  39. :return: label
  40. """
  41. return self.labels.get(id)
  42. def file(self, id: int) -> T.Optional[Label]:
  43. """
  44. get a file using its unique identifier
  45. :param identifier: unique identifier
  46. :return: file
  47. """
  48. return self.files.get(id)
  49. def collection(self, id: int) -> T.Optional[Collection]:
  50. """
  51. get a collection using its unique identifier
  52. :param identifier: unique identifier
  53. :return: collection
  54. """
  55. return self.collections.get(id)
  56. def collection_by_reference(self, reference: str) -> T.Optional[Collection]:
  57. """
  58. get a collection using its unique identifier
  59. :param identifier: unique identifier
  60. :return: collection
  61. """
  62. return self.collections.filter_by(reference=reference).one()
  63. def create_label(self, name: str, reference: str = None,
  64. parent_id: int = None) -> Tuple[Optional[Label], bool]:
  65. """
  66. create a label for this project. If there is already a label with the same reference
  67. in the database its name is updated.
  68. :param name: label name
  69. :param reference: label reference
  70. :param parent_id: parent's identifier
  71. :return: created or edited label, insert
  72. """
  73. label, is_new = Label.get_or_create(project=self, reference=reference)
  74. label.set_name(name)
  75. label.set_parent(parent_id)
  76. self.commit()
  77. return label, is_new
  78. def create_collection(self,
  79. reference: str,
  80. name: str,
  81. description: str,
  82. position: int,
  83. autoselect: bool):
  84. collection, is_new = Collection.get_or_create(project=self, reference=reference)
  85. collection.name = name
  86. collection.description = description
  87. collection.position = position
  88. collection.autoselect = autoselect
  89. self.commit()
  90. return collection, is_new
  91. def add_file(self, uuid: str, file_type: str, name: str, extension: str, size: int,
  92. filename: str, frames: int = None, fps: float = None) -> T.Tuple[File, bool]:
  93. """
  94. add a file to this project
  95. :param uuid: unique identifier which is used for temporary files
  96. :param file_type: file type (either image or video)
  97. :param name: file name
  98. :param extension: file extension
  99. :param size: file size
  100. :param filename: actual name in filesystem
  101. :param frames: frame count
  102. :param fps: frames per second
  103. :return: file
  104. """
  105. path = join(self.data_folder, filename + extension)
  106. file, is_new = File.get_or_create(project=self, path=path)
  107. file.uuid = uuid
  108. file.type = file_type
  109. file.name = name
  110. file.extension = extension
  111. file.size = size
  112. file.frames = frames
  113. file.fps = fps
  114. self.commit()
  115. return file, is_new
  116. def set_description(self, description: str):
  117. """
  118. set this projects description
  119. :param description: new description
  120. :return:
  121. """
  122. self.description = description
  123. def count_files(self) -> int:
  124. """
  125. count files associated with this project
  126. :return: count
  127. """
  128. return self.files.count()
  129. def get_files(self, offset: int = 0, limit: int = -1) -> T.Iterator[File]:
  130. """
  131. get an iterator of files associated with this project
  132. :param offset: file offset
  133. :param limit: file limit
  134. :return: iterator of files
  135. """
  136. return self.files.order_by(File.id).offset(offset).limit(limit)
  137. def count_files_without_results(self) -> int:
  138. """
  139. count files without associated results
  140. :return: count
  141. """
  142. raise NotImplementedError
  143. with closing(self.database.con.cursor()) as cursor:
  144. cursor.execute('''
  145. SELECT COUNT(*)
  146. FROM files
  147. LEFT JOIN results ON files.id = results.file
  148. WHERE files.project = ? AND results.id IS NULL
  149. ''', [self.identifier])
  150. return cursor.fetchone()[0]
  151. def files_without_results(self) -> Iterator[File]:
  152. """
  153. get an iterator of files without associated results
  154. :return: list of files
  155. """
  156. raise NotImplementedError
  157. with closing(self.database.con.cursor()) as cursor:
  158. cursor.execute('''
  159. SELECT files.*
  160. FROM files
  161. LEFT JOIN results ON files.id = results.file
  162. WHERE files.project = ? AND results.id IS NULL
  163. ORDER BY id ASC
  164. ''', [self.identifier])
  165. for row in cursor:
  166. yield File(self.database, row)
  167. def files_without_collection(self, offset: int = 0, limit: int = -1) -> Iterator[File]:
  168. """
  169. get an iterator of files without not associated with any collection
  170. :return: list of files
  171. """
  172. return self.get_files(offset, limit).filter(File.collection_id == None)
  173. def count_files_without_collection(self) -> int:
  174. """
  175. count files associated with this project but with no collection
  176. :return: count
  177. """
  178. return self.files_without_collection().count()