1
1

Project.py 10 KB

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