Project.py 9.1 KB

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