6
0

Project.py 14 KB

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