Project.py 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338
  1. import os
  2. import shutil
  3. import typing as T
  4. import warnings
  5. from datetime import datetime
  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. from pycs.database.util import commit_on_return
  12. class Project(NamedBaseModel):
  13. description = db.Column(db.String)
  14. created = db.Column(db.DateTime, default=datetime.utcnow,
  15. index=True, nullable=False)
  16. model_id = db.Column(
  17. db.Integer,
  18. db.ForeignKey("model.id", ondelete="SET NULL"))
  19. label_provider_id = db.Column(
  20. db.Integer,
  21. db.ForeignKey("label_provider.id", ondelete="SET NULL"))
  22. root_folder = db.Column(db.String, nullable=False, unique=True)
  23. external_data = db.Column(db.Boolean, nullable=False)
  24. data_folder = db.Column(db.String, nullable=False)
  25. # contraints
  26. __table_args__ = ()
  27. # relationships to other models
  28. files = db.relationship(
  29. "File",
  30. backref="project",
  31. lazy="dynamic")
  32. labels = db.relationship(
  33. "Label",
  34. backref="project",
  35. lazy="dynamic")
  36. collections = db.relationship(
  37. "Collection",
  38. backref="project",
  39. lazy="dynamic")
  40. serialize_only = NamedBaseModel.serialize_only + (
  41. "created",
  42. "description",
  43. "model_id",
  44. "label_provider_id",
  45. "root_folder",
  46. "external_data",
  47. "data_folder",
  48. )
  49. @commit_on_return
  50. def delete(self) -> T.Tuple[dict, dict]:
  51. dump = super().delete(commit=False)
  52. model_dump = {}
  53. if self.model_id is not None:
  54. model_dump = self.model.delete(commit=False)
  55. # remove from file system
  56. shutil.rmtree(self.root_folder)
  57. return dump, model_dump
  58. def label(self, identifier: int) -> T.Optional[Label]:
  59. """
  60. get a label using its unique identifier
  61. :param identifier: unique identifier
  62. :return: label
  63. """
  64. return self.labels.filter(Label.id == identifier).one_or_none()
  65. def label_by_reference(self, reference: str) -> T.Optional[Label]:
  66. """
  67. get a label using its reference string
  68. :param reference: reference string
  69. :return: label
  70. """
  71. return self.labels.filter(Label.reference == reference).one_or_none()
  72. def file(self, identifier: int) -> T.Optional[Label]:
  73. """
  74. get a file using its unique identifier
  75. :param identifier: unique identifier
  76. :return: file
  77. """
  78. return self.files.filter(File.id == identifier).one_or_none()
  79. def label_tree(self) -> T.List[Label]:
  80. """
  81. get a list of root labels associated with this project
  82. :return: list of labels
  83. """
  84. warnings.warn("Check performance of this method!")
  85. return self.labels.filter(Label.parent_id == None).all()
  86. def label_tree_original(self):
  87. """
  88. get a list of root labels associated with this project
  89. :return: list of labels
  90. """
  91. raise NotImplementedError
  92. with closing(self.database.con.cursor()) as cursor:
  93. cursor.execute('''
  94. WITH RECURSIVE
  95. tree AS (
  96. SELECT labels.* FROM labels
  97. WHERE project = ? AND parent IS NULL
  98. UNION ALL
  99. SELECT labels.* FROM labels
  100. JOIN tree ON labels.parent = tree.id
  101. )
  102. SELECT * FROM tree
  103. ''', [self.id])
  104. result = []
  105. lookup = {}
  106. for row in cursor.fetchall():
  107. label = TreeNodeLabel(self.database, row)
  108. lookup[label.id] = label
  109. if label.parent_id is None:
  110. result.append(label)
  111. else:
  112. lookup[label.parent_id].children.append(label)
  113. return result
  114. def collection(self, identifier: int) -> T.Optional[Collection]:
  115. """
  116. get a collection using its unique identifier
  117. :param identifier: unique identifier
  118. :return: collection
  119. """
  120. return self.collections.filter(Collection.id == identifier).one_or_none()
  121. def collection_by_reference(self, reference: str) -> T.Optional[Collection]:
  122. """
  123. get a collection using its unique identifier
  124. :param identifier: unique identifier
  125. :return: collection
  126. """
  127. return self.collections.filter(Collection.reference == reference).one_or_none()
  128. @commit_on_return
  129. def create_label(self, name: str,
  130. reference: str = None,
  131. parent: T.Optional[T.Union[int, str, Label]] = None,
  132. hierarchy_level: str = None) -> T.Tuple[T.Optional[Label], bool]:
  133. """
  134. create a label for this project. If there is already a label with the same reference
  135. in the database its name is updated.
  136. :param name: label name
  137. :param reference: label reference
  138. :param parent: parent label. Either a reference string, a Label id or a Label instance
  139. :param hierarchy_level: hierarchy level name
  140. :return: created or edited label, insert
  141. """
  142. label = Label.query.get(project=self, reference=reference)
  143. is_new = False
  144. if label is None:
  145. label = Label.new(project=self, reference=reference)
  146. is_new = True
  147. label.set_name(name, commit=False)
  148. label.set_parent(parent, commit=False)
  149. label.hierarchy_level = hierarchy_level
  150. return label, is_new
  151. @commit_on_return
  152. def create_collection(self,
  153. reference: str,
  154. name: str,
  155. description: str,
  156. position: int,
  157. autoselect: bool) -> T.Tuple[Collection, bool]:
  158. """
  159. create a new collection associated with this project
  160. :param reference: collection reference string
  161. :param name: collection name
  162. :param description: collection description
  163. :param position: position in menus
  164. :param autoselect: automatically select this collection on session load
  165. :return: collection object, insert
  166. """
  167. collection, is_new = Collection.get_or_create(
  168. project_id=self.id, reference=reference)
  169. collection.name = name
  170. collection.description = description
  171. collection.position = position
  172. collection.autoselect = autoselect
  173. return collection, is_new
  174. @commit_on_return
  175. def add_file(self,
  176. uuid: str,
  177. file_type: str,
  178. name: str,
  179. extension: str,
  180. size: int,
  181. filename: str,
  182. frames: int = None,
  183. fps: float = None) -> T.Tuple[File, bool]:
  184. """
  185. add a file to this project
  186. :param uuid: unique identifier which is used for temporary files
  187. :param file_type: file type (either image or video)
  188. :param name: file name
  189. :param extension: file extension
  190. :param size: file size
  191. :param filename: actual name in filesystem
  192. :param frames: frame count
  193. :param fps: frames per second
  194. :return: file
  195. """
  196. path = os.path.join(self.data_folder, f"{filename}{extension}")
  197. file, is_new = File.get_or_create(
  198. project_id=self.id, path=path)
  199. file.type = file_type
  200. file.name = name
  201. file.extension = extension
  202. file.size = size
  203. file.frames = frames
  204. file.fps = fps
  205. return file, is_new
  206. def get_files(self, offset: int = 0, limit: int = -1) -> T.List[File]:
  207. """
  208. get an iterator of files associated with this project
  209. :param offset: file offset
  210. :param limit: file limit
  211. :return: iterator of files
  212. """
  213. return self.files.order_by(File.id).offset(offset).limit(limit)
  214. def _files_without_results(self):
  215. """
  216. get files without any results
  217. :return: a query object
  218. """
  219. return self.files.filter(~File.results.any())
  220. def count_files_without_results(self) -> int:
  221. """
  222. count files without associated results
  223. :return: count
  224. """
  225. return self._files_without_results().count()
  226. def files_without_results(self) -> T.List[File]:
  227. """
  228. get a list of files without associated results
  229. :return: list of files
  230. """
  231. return self._files_without_results().all()
  232. def _files_without_collection(self, offset: int = 0, limit: int = -1):
  233. """
  234. get files without a collection
  235. :return: a query object
  236. """
  237. return self.get_files(offset, limit).filter(File.collection_id == None)
  238. def files_without_collection(self, offset: int = 0, limit: int = -1) -> T.List[File]:
  239. """
  240. get a list of files without a collection
  241. :return: list of files
  242. """
  243. return self._files_without_collection(offset=offset, limit=limit).all()
  244. def count_files_without_collection(self) -> int:
  245. """
  246. count files associated with this project but without a collection
  247. :return: count
  248. """
  249. return self._files_without_collection().count()