Project.py 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295
  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 Iterator
  6. from typing import List
  7. from typing import Optional
  8. from typing import Tuple
  9. from pycs import db
  10. from pycs.database.base import NamedBaseModel
  11. from pycs.database.Collection import Collection
  12. from pycs.database.File import File
  13. from pycs.database.Label import Label
  14. from pycs.database.util import commit_on_return
  15. from pycs.database.util.TreeNodeLabel import TreeNodeLabel
  16. class Project(NamedBaseModel):
  17. description = db.Column(db.String)
  18. created = db.Column(db.DateTime, default=datetime.utcnow,
  19. index=True, nullable=False)
  20. model_id = db.Column(
  21. db.Integer,
  22. db.ForeignKey("model.id", ondelete="SET NULL"))
  23. label_provider_id = db.Column(
  24. db.Integer,
  25. db.ForeignKey("label_provider.id", ondelete="SET NULL"))
  26. root_folder = db.Column(db.String, nullable=False, unique=True)
  27. external_data = db.Column(db.Boolean, nullable=False)
  28. data_folder = db.Column(db.String, nullable=False)
  29. # contraints
  30. __table_args__ = ()
  31. # relationships to other models
  32. files = db.relationship("File", backref="project", lazy=True)
  33. labels = db.relationship("Label", backref="project", lazy=True)
  34. collections = db.relationship("Collection", backref="project", lazy=True)
  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. @commit_on_return
  64. def create_label(self, name: str, reference: str = None,
  65. parent_id: int = None,
  66. hierarchy_level: str = None) -> Tuple[Optional[Label], bool]:
  67. """
  68. create a label for this project. If there is already a label with the same reference
  69. in the database its name is updated.
  70. :param name: label name
  71. :param reference: label reference
  72. :param parent_id: parent's identifier
  73. :param hierarchy_level: hierarchy level name
  74. :return: created or edited label, insert
  75. """
  76. label = Label.query.get(project=self, reference=reference)
  77. is_new = False
  78. if label is None:
  79. label = Label.new(project=self, reference=reference)
  80. is_new = True
  81. label.set_name(name, commit=False)
  82. label.set_parent(parent_id, commit=False)
  83. label.hierarchy_level = hierarchy_level
  84. return label, is_new
  85. def label_tree(self) -> List[TreeNodeLabel]:
  86. """
  87. get a list of root labels associated with this project
  88. :return: list of labels
  89. """
  90. raise NotImplementedError
  91. with closing(self.database.con.cursor()) as cursor:
  92. cursor.execute('''
  93. WITH RECURSIVE
  94. tree AS (
  95. SELECT labels.* FROM labels
  96. WHERE project = ? AND parent IS NULL
  97. UNION ALL
  98. SELECT labels.* FROM labels
  99. JOIN tree ON labels.parent = tree.id
  100. )
  101. SELECT * FROM tree
  102. ''', [self.identifier])
  103. result = []
  104. lookup = {}
  105. for row in cursor.fetchall():
  106. label = TreeNodeLabel(self.database, row)
  107. lookup[label.identifier] = label
  108. if label.parent_id is None:
  109. result.append(label)
  110. else:
  111. lookup[label.parent_id].children.append(label)
  112. return result
  113. @commit_on_return
  114. def create_collection(self,
  115. reference: str,
  116. name: str,
  117. description: str,
  118. position: int,
  119. autoselect: bool) -> Tuple[Collection, bool]:
  120. """
  121. create a new collection associated with this project
  122. :param reference: collection reference string
  123. :param name: collection name
  124. :param description: collection description
  125. :param position: position in menus
  126. :param autoselect: automatically select this collection on session load
  127. :return: collection object, insert
  128. """
  129. collection = Collection.query.get(project=self, reference=reference)
  130. is_new = False
  131. if collection is None:
  132. collection = Collection.new(project=self,
  133. reference=reference)
  134. is_new = True
  135. collection.name = name
  136. collection.description = description
  137. collection.position = position
  138. collection.autoselect = autoselect
  139. return collection, is_new
  140. @commit_on_return
  141. def add_file(self, uuid: str, file_type: str, name: str, extension: str, size: int,
  142. filename: str, frames: int = None, fps: float = None) -> T.Tuple[File, bool]:
  143. """
  144. add a file to this project
  145. :param uuid: unique identifier which is used for temporary files
  146. :param file_type: file type (either image or video)
  147. :param name: file name
  148. :param extension: file extension
  149. :param size: file size
  150. :param filename: actual name in filesystem
  151. :param frames: frame count
  152. :param fps: frames per second
  153. :return: file
  154. """
  155. path = join(self.data_folder, filename + extension)
  156. file = File.objects.get(project=self, path=path)
  157. is_new = False
  158. if file is None:
  159. file = File.new(uuid=uuid, project=self, path=path)
  160. is_new = True
  161. file.type = file_type
  162. file.name = name
  163. file.extension = extension
  164. file.size = size
  165. file.frames = frames
  166. file.fps = fps
  167. return file, is_new
  168. def set_description(self, description: str):
  169. """
  170. set this projects description
  171. :param description: new description
  172. :return:
  173. """
  174. self.description = description
  175. self
  176. def count_files(self) -> int:
  177. """
  178. count files associated with this project
  179. :return: count
  180. """
  181. return self.files.count()
  182. def get_files(self, offset: int = 0, limit: int = -1) -> T.Iterator[File]:
  183. """
  184. get an iterator of files associated with this project
  185. :param offset: file offset
  186. :param limit: file limit
  187. :return: iterator of files
  188. """
  189. return self.files.order_by(File.id.acs()).offset(offset).limit(limit)
  190. def count_files_without_results(self) -> int:
  191. """
  192. count files without associated results
  193. :return: count
  194. """
  195. raise NotImplementedError
  196. with closing(self.database.con.cursor()) as cursor:
  197. cursor.execute('''
  198. SELECT COUNT(*)
  199. FROM files
  200. LEFT JOIN results ON files.id = results.file
  201. WHERE files.project = ? AND results.id IS NULL
  202. ''', [self.identifier])
  203. return cursor.fetchone()[0]
  204. def files_without_results(self) -> Iterator[File]:
  205. """
  206. get an iterator of files without associated results
  207. :return: list of files
  208. """
  209. raise NotImplementedError
  210. with closing(self.database.con.cursor()) as cursor:
  211. cursor.execute('''
  212. SELECT files.*
  213. FROM files
  214. LEFT JOIN results ON files.id = results.file
  215. WHERE files.project = ? AND results.id IS NULL
  216. ORDER BY id ASC
  217. ''', [self.identifier])
  218. for row in cursor:
  219. yield File(self.database, row)
  220. def files_without_collection(self, offset: int = 0, limit: int = -1) -> Iterator[File]:
  221. """
  222. get an iterator of files without not associated with any collection
  223. :return: list of files
  224. """
  225. return self.get_files(offset, limit).filter(File.collection_id == None)
  226. def count_files_without_collection(self) -> int:
  227. """
  228. count files associated with this project but with no collection
  229. :return: count
  230. """
  231. return self.files_without_collection().count()