6
0

Project.py 9.4 KB

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