Project.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443
  1. from contextlib import closing
  2. from os.path import join
  3. from time import time
  4. from typing import List, Optional, Tuple, Iterator, Union
  5. from pycs.database.Collection import Collection
  6. from pycs.database.File import File
  7. from pycs.database.Label import Label
  8. from pycs.database.LabelProvider import LabelProvider
  9. from pycs.database.Model import Model
  10. from pycs.database.util.TreeNodeLabel import TreeNodeLabel
  11. class Project:
  12. """
  13. database class for projects
  14. """
  15. def __init__(self, database, row):
  16. self.database = database
  17. self.identifier = row[0]
  18. self.name = row[1]
  19. self.description = row[2]
  20. self.created = row[3]
  21. self.model_id = row[4]
  22. self.label_provider_id = row[5]
  23. self.root_folder = row[6]
  24. self.external_data = bool(row[7])
  25. self.data_folder = row[8]
  26. def model(self) -> Model:
  27. """
  28. get the model this project is associated with
  29. :return: model
  30. """
  31. return self.database.model(self.model_id)
  32. def label_provider(self) -> Optional[LabelProvider]:
  33. """
  34. get the label provider this project is associated with
  35. :return: label provider
  36. """
  37. if self.label_provider_id is not None:
  38. return self.database.label_provider(self.label_provider_id)
  39. return None
  40. def labels(self) -> List[Label]:
  41. """
  42. get a list of labels associated with this project
  43. :return: list of labels
  44. """
  45. with closing(self.database.con.cursor()) as cursor:
  46. cursor.execute('SELECT * FROM labels WHERE project = ?', [self.identifier])
  47. return list(map(
  48. lambda row: Label(self.database, row),
  49. cursor.fetchall()
  50. ))
  51. def label_tree(self) -> List[TreeNodeLabel]:
  52. """
  53. get a list of root labels associated with this project
  54. :return: list of labels
  55. """
  56. with closing(self.database.con.cursor()) as cursor:
  57. cursor.execute('''
  58. WITH RECURSIVE
  59. tree AS (
  60. SELECT labels.* FROM labels
  61. WHERE project = ? AND parent IS NULL
  62. UNION ALL
  63. SELECT labels.* FROM labels
  64. JOIN tree ON labels.parent = tree.id
  65. )
  66. SELECT * FROM tree
  67. ''', [self.identifier])
  68. result = []
  69. lookup = {}
  70. for row in cursor.fetchall():
  71. label = TreeNodeLabel(self.database, row)
  72. lookup[label.identifier] = label
  73. if label.parent_id is None:
  74. result.append(label)
  75. else:
  76. lookup[label.parent_id].children.append(label)
  77. return result
  78. def label(self, identifier: int) -> Optional[Label]:
  79. """
  80. get a label using its unique identifier
  81. :param identifier: unique identifier
  82. :return: label
  83. """
  84. with closing(self.database.con.cursor()) as cursor:
  85. cursor.execute('SELECT * FROM labels WHERE id = ? AND project = ?',
  86. (identifier, self.identifier))
  87. row = cursor.fetchone()
  88. if row is not None:
  89. return Label(self.database, row)
  90. return None
  91. def label_by_reference(self, reference: str) -> Optional[Label]:
  92. """
  93. get a label using its reference string
  94. :param reference: reference string
  95. :return: label
  96. """
  97. with closing(self.database.con.cursor()) as cursor:
  98. cursor.execute('SELECT * FROM labels WHERE reference = ? AND project = ?',
  99. (reference, self.identifier))
  100. row = cursor.fetchone()
  101. if row is not None:
  102. return Label(self.database, row)
  103. return None
  104. def create_label(self, name: str, reference: str = None,
  105. parent: Union[Label, int, str] = None,
  106. hierarchy_level: str = None) -> Tuple[Optional[Label], bool]:
  107. """
  108. create a label for this project. If there is already a label with the same reference
  109. in the database its name is updated.
  110. :param name: label name
  111. :param reference: label reference
  112. :param parent: either parent identifier, parent reference string or `Label` object
  113. :param hierarchy_level: hierarchy level name
  114. :return: created or edited label, insert
  115. """
  116. created = int(time())
  117. if isinstance(parent, str):
  118. parent = self.label_by_reference(parent)
  119. if isinstance(parent, Label):
  120. parent = parent.identifier
  121. with closing(self.database.con.cursor()) as cursor:
  122. cursor.execute('''
  123. INSERT INTO labels (project, parent, created, reference, name, hierarchy_level)
  124. VALUES (?, ?, ?, ?, ?, ?)
  125. ON CONFLICT (project, reference) DO
  126. UPDATE SET parent = ?, name = ?, hierarchy_level = ?
  127. ''', (self.identifier, parent, created, reference, name, hierarchy_level,
  128. parent, name, hierarchy_level))
  129. # lastrowid is 0 if on conflict clause applies.
  130. # If this is the case we do an extra query to receive the row id.
  131. if cursor.lastrowid > 0:
  132. row_id = cursor.lastrowid
  133. insert = True
  134. else:
  135. cursor.execute('SELECT id FROM labels WHERE project = ? AND reference = ?',
  136. (self.identifier, reference))
  137. row_id = cursor.fetchone()[0]
  138. insert = False
  139. return self.label(row_id), insert
  140. def collections(self) -> List[Collection]:
  141. """
  142. get a list of collections associated with this project
  143. :return: list of collections
  144. """
  145. with closing(self.database.con.cursor()) as cursor:
  146. cursor.execute('SELECT * FROM collections WHERE project = ? ORDER BY position ASC',
  147. [self.identifier])
  148. return list(map(
  149. lambda row: Collection(self.database, row),
  150. cursor.fetchall()
  151. ))
  152. def collection(self, identifier: int) -> Optional[Collection]:
  153. """
  154. get a collection using its unique identifier
  155. :param identifier: unique identifier
  156. :return: collection
  157. """
  158. with closing(self.database.con.cursor()) as cursor:
  159. cursor.execute('SELECT * FROM collections WHERE id = ? AND project = ?',
  160. (identifier, self.identifier))
  161. row = cursor.fetchone()
  162. if row is not None:
  163. return Collection(self.database, row)
  164. return None
  165. def collection_by_reference(self, reference: str):
  166. """
  167. get a collection using its reference string
  168. :param reference: reference string
  169. :return: collection
  170. """
  171. with closing(self.database.con.cursor()) as cursor:
  172. cursor.execute('SELECT * FROM collections WHERE reference = ? AND project = ?',
  173. (reference, self.identifier))
  174. row = cursor.fetchone()
  175. if row is not None:
  176. return Collection(self.database, row)
  177. return None
  178. def create_collection(self,
  179. reference: str,
  180. name: str,
  181. description: str,
  182. position: int,
  183. autoselect: bool) -> Tuple[Collection, bool]:
  184. """
  185. create a new collection associated with this project
  186. :param reference: collection reference string
  187. :param name: collection name
  188. :param description: collection description
  189. :param position: position in menus
  190. :param autoselect: automatically select this collection on session load
  191. :return: collection object, insert
  192. """
  193. autoselect = 1 if autoselect else 0
  194. with closing(self.database.con.cursor()) as cursor:
  195. cursor.execute('''
  196. INSERT INTO collections
  197. (project, reference, name, description, position, autoselect)
  198. VALUES (?, ?, ?, ?, ?, ?)
  199. ON CONFLICT (project, reference) DO
  200. UPDATE SET name = ?, description = ?, position = ?, autoselect = ?
  201. ''', (self.identifier, reference, name, description, position, autoselect,
  202. name, description, position, autoselect))
  203. # lastrowid is 0 if on conflict clause applies.
  204. # If this is the case we do an extra query to receive the row id.
  205. if cursor.lastrowid > 0:
  206. row_id = cursor.lastrowid
  207. insert = True
  208. else:
  209. cursor.execute('SELECT id FROM collections WHERE project = ? AND reference = ?',
  210. (self.identifier, reference))
  211. row_id = cursor.fetchone()[0]
  212. insert = False
  213. return self.collection(row_id), insert
  214. def remove(self) -> None:
  215. """
  216. remove this project from the database
  217. :return:
  218. """
  219. with closing(self.database.con.cursor()) as cursor:
  220. cursor.execute('DELETE FROM projects WHERE id = ?', [self.identifier])
  221. def set_name(self, name: str) -> None:
  222. """
  223. set this projects name
  224. :param name: new name
  225. :return:
  226. """
  227. with closing(self.database.con.cursor()) as cursor:
  228. cursor.execute('UPDATE projects SET name = ? WHERE id = ?', (name, self.identifier))
  229. self.name = name
  230. def set_description(self, description: str) -> None:
  231. """
  232. set this projects description
  233. :param description: new description
  234. :return:
  235. """
  236. with closing(self.database.con.cursor()) as cursor:
  237. cursor.execute('UPDATE projects SET description = ? WHERE id = ?',
  238. (description, self.identifier))
  239. self.description = description
  240. def count_files(self) -> int:
  241. """
  242. count files associated with this project
  243. :return: count
  244. """
  245. with closing(self.database.con.cursor()) as cursor:
  246. cursor.execute('SELECT COUNT(*) FROM files WHERE project = ?', [self.identifier])
  247. return cursor.fetchone()[0]
  248. def files(self, offset: int = 0, limit: int = -1) -> Iterator[File]:
  249. """
  250. get an iterator of files associated with this project
  251. :param offset: file offset
  252. :param limit: file limit
  253. :return: iterator of files
  254. """
  255. with closing(self.database.con.cursor()) as cursor:
  256. cursor.execute('SELECT * FROM files WHERE project = ? ORDER BY id ASC LIMIT ? OFFSET ?',
  257. (self.identifier, limit, offset))
  258. return map(
  259. lambda row: File(self.database, row),
  260. cursor.fetchall()
  261. )
  262. def count_files_without_results(self) -> int:
  263. """
  264. count files without associated results
  265. :return: count
  266. """
  267. with closing(self.database.con.cursor()) as cursor:
  268. cursor.execute('''
  269. SELECT COUNT(*)
  270. FROM files
  271. LEFT JOIN results ON files.id = results.file
  272. WHERE files.project = ? AND results.id IS NULL
  273. ''', [self.identifier])
  274. return cursor.fetchone()[0]
  275. def files_without_results(self) -> Iterator[File]:
  276. """
  277. get an iterator of files without associated results
  278. :return: list of files
  279. """
  280. with closing(self.database.con.cursor()) as cursor:
  281. cursor.execute('''
  282. SELECT files.*
  283. FROM files
  284. LEFT JOIN results ON files.id = results.file
  285. WHERE files.project = ? AND results.id IS NULL
  286. ORDER BY id ASC
  287. ''', [self.identifier])
  288. for row in cursor:
  289. yield File(self.database, row)
  290. def count_files_without_collection(self) -> int:
  291. """
  292. count files associated with this project but with no collection
  293. :return: count
  294. """
  295. with closing(self.database.con.cursor()) as cursor:
  296. cursor.execute('SELECT COUNT(*) FROM files WHERE project = ? AND collection IS NULL',
  297. [self.identifier])
  298. return cursor.fetchone()[0]
  299. def files_without_collection(self, offset=0, limit=-1) -> Iterator[File]:
  300. """
  301. get an iterator of files without not associated with any collection
  302. :return: list of files
  303. """
  304. with closing(self.database.con.cursor()) as cursor:
  305. cursor.execute('''
  306. SELECT * FROM files
  307. WHERE files.project = ? AND files.collection IS NULL
  308. ORDER BY id ASC
  309. LIMIT ? OFFSET ?
  310. ''', (self.identifier, limit, offset))
  311. for row in cursor:
  312. yield File(self.database, row)
  313. def file(self, identifier) -> Optional[File]:
  314. """
  315. get a file using its unique identifier
  316. :param identifier: unique identifier
  317. :return: file
  318. """
  319. with closing(self.database.con.cursor()) as cursor:
  320. cursor.execute('SELECT * FROM files WHERE id = ? AND project = ?',
  321. (identifier, self.identifier))
  322. row = cursor.fetchone()
  323. if row is not None:
  324. return File(self.database, row)
  325. return None
  326. def add_file(self, uuid: str, file_type: str, name: str, extension: str, size: int,
  327. filename: str, frames: int = None, fps: float = None) -> Tuple[File, bool]:
  328. """
  329. add a file to this project
  330. :param uuid: unique identifier which is used for temporary files
  331. :param file_type: file type (either image or video)
  332. :param name: file name
  333. :param extension: file extension
  334. :param size: file size
  335. :param filename: actual name in filesystem
  336. :param frames: frame count
  337. :param fps: frames per second
  338. :return: file
  339. """
  340. created = int(time())
  341. path = join(self.data_folder, filename + extension)
  342. with closing(self.database.con.cursor()) as cursor:
  343. cursor.execute('''
  344. INSERT INTO files (
  345. uuid, project, type, name, extension, size, created, path, frames, fps
  346. )
  347. VALUES (
  348. ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
  349. )
  350. ON CONFLICT (project, path) DO
  351. UPDATE SET type = ?, name = ?, extension = ?, size = ?, frames = ?, fps = ?
  352. ''', (uuid, self.identifier, file_type, name, extension, size, created, path, frames,
  353. fps, file_type, name, extension, size, frames, fps))
  354. # lastrowid is 0 if on conflict clause applies.
  355. # If this is the case we do an extra query to receive the row id.
  356. if cursor.lastrowid > 0:
  357. row_id = cursor.lastrowid
  358. insert = True
  359. else:
  360. cursor.execute('SELECT id FROM files WHERE project = ? AND path = ?',
  361. (self.identifier, path))
  362. row_id = cursor.fetchone()[0]
  363. insert = False
  364. return self.file(row_id), insert