6
0

Project.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413
  1. import os
  2. import shutil
  3. import typing as T
  4. import warnings
  5. from datetime import datetime
  6. from pycs import app
  7. from pycs import db
  8. from pycs.database.base import NamedBaseModel
  9. from pycs.database.Collection import Collection
  10. from pycs.database.File import File
  11. from pycs.database.Label import Label
  12. from pycs.database.util import commit_on_return
  13. class Project(NamedBaseModel):
  14. """ DB Model for projects """
  15. description = db.Column(db.String)
  16. created = db.Column(db.DateTime, default=datetime.utcnow,
  17. index=True, nullable=False)
  18. model_id = db.Column(
  19. db.Integer,
  20. db.ForeignKey("model.id", ondelete="SET NULL"))
  21. label_provider_id = db.Column(
  22. db.Integer,
  23. db.ForeignKey("label_provider.id", ondelete="SET NULL"))
  24. root_folder = db.Column(db.String, nullable=False, unique=True)
  25. external_data = db.Column(db.Boolean, nullable=False)
  26. data_folder = db.Column(db.String, nullable=False)
  27. # contraints
  28. __table_args__ = ()
  29. # relationships to other models
  30. files = db.relationship(
  31. "File",
  32. backref="project",
  33. lazy="dynamic",
  34. passive_deletes=True,
  35. )
  36. labels = db.relationship(
  37. "Label",
  38. backref="project",
  39. lazy="dynamic",
  40. passive_deletes=True,
  41. )
  42. collections = db.relationship(
  43. "Collection",
  44. backref="project",
  45. lazy="dynamic",
  46. passive_deletes=True,
  47. )
  48. serialize_only = NamedBaseModel.serialize_only + (
  49. "created",
  50. "description",
  51. "model_id",
  52. "label_provider_id",
  53. "root_folder",
  54. "external_data",
  55. "data_folder",
  56. )
  57. @commit_on_return
  58. def delete(self) -> T.Tuple[dict, dict]:
  59. # pylint: disable=unexpected-keyword-arg
  60. dump = super().delete(commit=False)
  61. model_dump = {}
  62. if self.model_id is not None:
  63. # pylint: disable=unexpected-keyword-arg
  64. model_dump = self.model.delete(commit=False)
  65. if os.path.exists(self.root_folder):
  66. # remove from file system
  67. shutil.rmtree(self.root_folder)
  68. return dump, model_dump
  69. def label(self, identifier: int) -> T.Optional[Label]:
  70. """
  71. get a label using its unique identifier
  72. :param identifier: unique identifier
  73. :return: label
  74. """
  75. return self.labels.filter(Label.id == identifier).one_or_none()
  76. def label_by_reference(self, reference: str) -> T.Optional[Label]:
  77. """
  78. get a label using its reference string
  79. :param reference: reference string
  80. :return: label
  81. """
  82. return self.labels.filter(Label.reference == reference).one_or_none()
  83. def file(self, identifier: int) -> T.Optional[Label]:
  84. """
  85. get a file using its unique identifier
  86. :param identifier: unique identifier
  87. :return: file
  88. """
  89. return self.files.filter(File.id == identifier).one_or_none()
  90. def label_tree(self) -> T.List[Label]:
  91. """
  92. get a list of root labels associated with this project
  93. :return: list of labels
  94. """
  95. warnings.warn("Check performance of this method!")
  96. # pylint: disable=no-member
  97. return self.labels.filter(Label.parent_id.is_(None)).all()
  98. def label_tree_original(self):
  99. """
  100. get a list of root labels associated with this project
  101. :return: list of labels
  102. """
  103. raise NotImplementedError
  104. # pylint: disable=unreachable
  105. # pylint: disable=pointless-string-statement
  106. """
  107. with closing(self.database.con.cursor()) as cursor:
  108. cursor.execute('''
  109. WITH RECURSIVE
  110. tree AS (
  111. SELECT labels.* FROM labels
  112. WHERE project = ? AND parent IS NULL
  113. UNION ALL
  114. SELECT labels.* FROM labels
  115. JOIN tree ON labels.parent = tree.id
  116. )
  117. SELECT * FROM tree
  118. ''', [self.id])
  119. result = []
  120. lookup = {}
  121. for row in cursor.fetchall():
  122. label = TreeNodeLabel(self.database, row)
  123. lookup[label.id] = label
  124. if label.parent_id is None:
  125. result.append(label)
  126. else:
  127. lookup[label.parent_id].children.append(label)
  128. return result
  129. """
  130. def collection(self, identifier: int) -> T.Optional[Collection]:
  131. """
  132. get a collection using its unique identifier
  133. :param identifier: unique identifier
  134. :return: collection
  135. """
  136. return self.collections.filter(Collection.id == identifier).one_or_none()
  137. def collection_by_reference(self, reference: str) -> T.Optional[Collection]:
  138. """
  139. get a collection using its unique identifier
  140. :param identifier: unique identifier
  141. :return: collection
  142. """
  143. return self.collections.filter(Collection.reference == reference).one_or_none()
  144. @commit_on_return
  145. def create_label(self, name: str,
  146. reference: str = None,
  147. parent: T.Optional[T.Union[int, str, Label]] = None,
  148. hierarchy_level: str = None) -> T.Tuple[T.Optional[Label], bool]:
  149. """
  150. create a label for this project. If there is already a label with the same reference
  151. in the database its name is updated.
  152. :param name: label name
  153. :param reference: label reference
  154. :param parent: parent label. Either a reference string, a Label id or a Label instance
  155. :param hierarchy_level: hierarchy level name
  156. :return: created or edited label, insert
  157. """
  158. label = None
  159. is_new = False
  160. if reference is not None:
  161. label = Label.query.filter_by(project_id=self.id, reference=reference).one_or_none()
  162. if label is None:
  163. label = Label.new(commit=False, project_id=self.id, reference=reference)
  164. is_new = True
  165. label.set_name(name, commit=False)
  166. label.set_parent(parent, commit=False)
  167. label.hierarchy_level = hierarchy_level
  168. return label, is_new
  169. @commit_on_return
  170. def bulk_create_labels(self, labels: T.List[T.Dict], clean_old_labels: bool = True):
  171. """
  172. Inserts a all labels at once.
  173. :raises:
  174. - AssertionError if project_id and reference are not unique
  175. - ValueError if a cycle in the hierarchy is found
  176. """
  177. if clean_old_labels:
  178. self.labels.delete()
  179. for label in labels:
  180. label["project_id"] = self.id
  181. self.__check_labels(labels)
  182. app.logger.info(f"Inserting {len(labels):,d} labels")
  183. db.engine.execute(Label.__table__.insert(), labels)
  184. self.__set_parents(labels)
  185. return labels
  186. def __set_parents(self, labels):
  187. """ after the bul insert, we need to set correct parent_ids """
  188. app.logger.info("Setting parents of the labels")
  189. self.flush()
  190. for label in labels:
  191. if label["parent"] is None:
  192. continue
  193. label_obj = self.label_by_reference(label["reference"])
  194. parent_label_obj = self.label_by_reference(label["parent"])
  195. label_obj.parent_id = parent_label_obj.id
  196. # pylint: disable=no-self-use
  197. def __check_labels(self, labels):
  198. """ check labels for unique keys and cycles """
  199. unique_keys = dict()
  200. for label in labels:
  201. key = (label["project_id"], label["reference"])
  202. assert key not in unique_keys, \
  203. f"{key} was not unique: ({label=} vs {unique_keys[key]=})!"
  204. unique_keys[key] = label
  205. # pylint: disable=too-many-arguments
  206. @commit_on_return
  207. def create_collection(self,
  208. reference: str,
  209. name: str,
  210. description: str,
  211. position: int,
  212. autoselect: bool) -> T.Tuple[Collection, bool]:
  213. """
  214. create a new collection associated with this project
  215. :param reference: collection reference string
  216. :param name: collection name
  217. :param description: collection description
  218. :param position: position in menus
  219. :param autoselect: automatically select this collection on session load
  220. :return: collection object, insert
  221. """
  222. collection, is_new = Collection.get_or_create(
  223. project_id=self.id, reference=reference)
  224. collection.name = name
  225. collection.description = description
  226. collection.position = position
  227. collection.autoselect = autoselect
  228. return collection, is_new
  229. # pylint: disable=too-many-arguments
  230. @commit_on_return
  231. def add_file(self,
  232. uuid: str,
  233. file_type: str,
  234. name: str,
  235. extension: str,
  236. size: int,
  237. filename: str,
  238. frames: int = None,
  239. fps: float = None) -> T.Tuple[File, bool]:
  240. """
  241. add a file to this project
  242. :param uuid: unique identifier which is used for temporary files
  243. :param file_type: file type (either image or video)
  244. :param name: file name
  245. :param extension: file extension
  246. :param size: file size
  247. :param filename: actual name in filesystem
  248. :param frames: frame count
  249. :param fps: frames per second
  250. :return: file
  251. """
  252. path = os.path.join(self.data_folder, f"{filename}{extension}")
  253. file, is_new = File.get_or_create(
  254. project_id=self.id, path=path)
  255. file.uuid = uuid
  256. file.type = file_type
  257. file.name = name
  258. file.extension = extension
  259. file.size = size
  260. file.frames = frames
  261. file.fps = fps
  262. return file, is_new
  263. def get_files(self, *filters, offset: int = 0, limit: int = -1) -> T.List[File]:
  264. """
  265. get an iterator of files associated with this project
  266. :param offset: file offset
  267. :param limit: file limit
  268. :return: iterator of files
  269. """
  270. return self.files.filter(*filters).order_by(File.id).offset(offset).limit(limit)
  271. def _files_without_results(self):
  272. """
  273. get files without any results
  274. :return: a query object
  275. """
  276. # pylint: disable=no-member
  277. return self.files.filter(~File.results.any())
  278. def count_files_without_results(self) -> int:
  279. """
  280. count files without associated results
  281. :return: count
  282. """
  283. return self._files_without_results().count()
  284. def files_without_results(self) -> T.List[File]:
  285. """
  286. get a list of files without associated results
  287. :return: list of files
  288. """
  289. return self._files_without_results().all()
  290. def _files_without_collection(self, offset: int = 0, limit: int = -1):
  291. """
  292. get files without a collection
  293. :return: a query object
  294. """
  295. # pylint: disable=no-member
  296. return self.get_files(File.collection_id.is_(None), offset=offset, limit=limit)
  297. def files_without_collection(self, offset: int = 0, limit: int = -1) -> T.List[File]:
  298. """
  299. get a list of files without a collection
  300. :return: list of files
  301. """
  302. return self._files_without_collection(offset=offset, limit=limit).all()
  303. def count_files_without_collection(self) -> int:
  304. """
  305. count files associated with this project but without a collection
  306. :return: count
  307. """
  308. return self._files_without_collection().count()