Project.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395
  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):
  153. autoselect = 1 if autoselect else 0
  154. with closing(self.database.con.cursor()) as cursor:
  155. cursor.execute('''
  156. INSERT INTO collections
  157. (project, reference, name, description, position, autoselect)
  158. VALUES (?, ?, ?, ?, ?, ?)
  159. ON CONFLICT (project, reference) DO
  160. UPDATE SET name = ?, description = ?, position = ?, autoselect = ?
  161. ''', (self.identifier, reference, name, description, position, autoselect,
  162. name, description, position, autoselect))
  163. # lastrowid is 0 if on conflict clause applies.
  164. # If this is the case we do an extra query to receive the row id.
  165. if cursor.lastrowid > 0:
  166. row_id = cursor.lastrowid
  167. insert = True
  168. else:
  169. cursor.execute('SELECT id FROM collections WHERE project = ? AND reference = ?',
  170. (self.identifier, reference))
  171. row_id = cursor.fetchone()[0]
  172. insert = False
  173. return self.collection(row_id), insert
  174. def remove(self) -> None:
  175. """
  176. remove this project from the database
  177. :return:
  178. """
  179. with closing(self.database.con.cursor()) as cursor:
  180. cursor.execute('DELETE FROM projects WHERE id = ?', [self.identifier])
  181. def set_name(self, name: str) -> None:
  182. """
  183. set this projects name
  184. :param name: new name
  185. :return:
  186. """
  187. with closing(self.database.con.cursor()) as cursor:
  188. cursor.execute('UPDATE projects SET name = ? WHERE id = ?', (name, self.identifier))
  189. self.name = name
  190. def set_description(self, description: str) -> None:
  191. """
  192. set this projects description
  193. :param description: new description
  194. :return:
  195. """
  196. with closing(self.database.con.cursor()) as cursor:
  197. cursor.execute('UPDATE projects SET description = ? WHERE id = ?',
  198. (description, self.identifier))
  199. self.description = description
  200. def count_files(self) -> int:
  201. """
  202. count files associated with this project
  203. :return: count
  204. """
  205. with closing(self.database.con.cursor()) as cursor:
  206. cursor.execute('SELECT COUNT(*) FROM files WHERE project = ?', [self.identifier])
  207. return cursor.fetchone()[0]
  208. def files(self, offset: int = 0, limit: int = -1) -> Iterator[File]:
  209. """
  210. get an iterator of files associated with this project
  211. :param offset: file offset
  212. :param limit: file limit
  213. :return: iterator of files
  214. """
  215. with closing(self.database.con.cursor()) as cursor:
  216. cursor.execute('SELECT * FROM files WHERE project = ? ORDER BY id ASC LIMIT ? OFFSET ?',
  217. (self.identifier, limit, offset))
  218. return map(
  219. lambda row: File(self.database, row),
  220. cursor.fetchall()
  221. )
  222. def count_files_without_results(self) -> int:
  223. """
  224. count files without associated results
  225. :return: count
  226. """
  227. with closing(self.database.con.cursor()) as cursor:
  228. cursor.execute('''
  229. SELECT COUNT(*)
  230. FROM files
  231. LEFT JOIN results ON files.id = results.file
  232. WHERE files.project = ? AND results.id IS NULL
  233. ''', [self.identifier])
  234. return cursor.fetchone()[0]
  235. def files_without_results(self) -> Iterator[File]:
  236. """
  237. get an iterator of files without associated results
  238. :return: list of files
  239. """
  240. with closing(self.database.con.cursor()) as cursor:
  241. cursor.execute('''
  242. SELECT files.*
  243. FROM files
  244. LEFT JOIN results ON files.id = results.file
  245. WHERE files.project = ? AND results.id IS NULL
  246. ORDER BY id ASC
  247. ''', [self.identifier])
  248. for row in cursor:
  249. yield File(self.database, row)
  250. def count_files_without_collection(self) -> int:
  251. """
  252. count files associated with this project but with no collection
  253. :return: count
  254. """
  255. with closing(self.database.con.cursor()) as cursor:
  256. cursor.execute('SELECT COUNT(*) FROM files WHERE project = ? AND collection IS NULL',
  257. [self.identifier])
  258. return cursor.fetchone()[0]
  259. def files_without_collection(self, offset=0, limit=-1) -> Iterator[File]:
  260. """
  261. get an iterator of files without not associated with any collection
  262. :return: list of files
  263. """
  264. with closing(self.database.con.cursor()) as cursor:
  265. cursor.execute('''
  266. SELECT * FROM files
  267. WHERE files.project = ? AND files.collection IS NULL
  268. ORDER BY id ASC
  269. LIMIT ? OFFSET ?
  270. ''', (self.identifier, limit, offset))
  271. for row in cursor:
  272. yield File(self.database, row)
  273. def file(self, identifier) -> Optional[File]:
  274. """
  275. get a file using its unique identifier
  276. :param identifier: unique identifier
  277. :return: file
  278. """
  279. with closing(self.database.con.cursor()) as cursor:
  280. cursor.execute('SELECT * FROM files WHERE id = ? AND project = ?',
  281. (identifier, self.identifier))
  282. row = cursor.fetchone()
  283. if row is not None:
  284. return File(self.database, row)
  285. return None
  286. def add_file(self, uuid: str, file_type: str, name: str, extension: str, size: int,
  287. filename: str, frames: int = None, fps: float = None) -> Tuple[File, bool]:
  288. """
  289. add a file to this project
  290. :param uuid: unique identifier which is used for temporary files
  291. :param file_type: file type (either image or video)
  292. :param name: file name
  293. :param extension: file extension
  294. :param size: file size
  295. :param filename: actual name in filesystem
  296. :param frames: frame count
  297. :param fps: frames per second
  298. :return: file
  299. """
  300. created = int(time())
  301. path = join(self.data_folder, filename + extension)
  302. with closing(self.database.con.cursor()) as cursor:
  303. cursor.execute('''
  304. INSERT INTO files (
  305. uuid, project, type, name, extension, size, created, path, frames, fps
  306. )
  307. VALUES (
  308. ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
  309. )
  310. ON CONFLICT (project, path) DO
  311. UPDATE SET type = ?, name = ?, extension = ?, size = ?, frames = ?, fps = ?
  312. ''', (uuid, self.identifier, file_type, name, extension, size, created, path, frames,
  313. fps, file_type, name, extension, size, frames, fps))
  314. # lastrowid is 0 if on conflict clause applies.
  315. # If this is the case we do an extra query to receive the row id.
  316. if cursor.lastrowid > 0:
  317. row_id = cursor.lastrowid
  318. insert = True
  319. else:
  320. cursor.execute('SELECT id FROM files WHERE project = ? AND path = ?',
  321. (self.identifier, path))
  322. row_id = cursor.fetchone()[0]
  323. insert = False
  324. return self.file(row_id), insert