Project.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373
  1. from contextlib import closing
  2. from os.path import join
  3. from time import time
  4. from typing import List, Optional, Tuple, Iterator
  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 create_label(self, name: str, reference: str = None,
  64. parent_id: int = None) -> Tuple[Optional[Label], bool]:
  65. """
  66. create a label for this project. If there is already a label with the same reference
  67. in the database its name is updated.
  68. :param name: label name
  69. :param reference: label reference
  70. :param parent_id: parent's identifier
  71. :return: created or edited label, insert
  72. """
  73. created = int(time())
  74. with closing(self.database.con.cursor()) as cursor:
  75. cursor.execute('''
  76. INSERT INTO labels (project, parent, created, reference, name)
  77. VALUES (?, ?, ?, ?, ?)
  78. ON CONFLICT (project, reference) DO
  79. UPDATE SET parent = ?, name = ?
  80. ''', (self.identifier, parent_id, created, reference, name, parent_id, name))
  81. # lastrowid is 0 if on conflict clause applies.
  82. # If this is the case we do an extra query to receive the row id.
  83. if cursor.lastrowid > 0:
  84. row_id = cursor.lastrowid
  85. insert = True
  86. else:
  87. cursor.execute('SELECT id FROM labels WHERE project = ? AND reference = ?',
  88. (self.identifier, reference))
  89. row_id = cursor.fetchone()[0]
  90. insert = False
  91. return self.label(row_id), insert
  92. def collections(self) -> List[Collection]:
  93. """
  94. get a list of collections associated with this project
  95. :return: list of collections
  96. """
  97. with closing(self.database.con.cursor()) as cursor:
  98. cursor.execute('SELECT * FROM collections WHERE project = ? ORDER BY position ASC',
  99. [self.identifier])
  100. return list(map(
  101. lambda row: Collection(self.database, row),
  102. cursor.fetchall()
  103. ))
  104. def collection(self, identifier: int) -> Optional[Collection]:
  105. """
  106. get a collection using its unique identifier
  107. :param identifier: unique identifier
  108. :return: collection
  109. """
  110. with closing(self.database.con.cursor()) as cursor:
  111. cursor.execute('SELECT * FROM collections WHERE id = ? AND project = ?',
  112. (identifier, self.identifier))
  113. row = cursor.fetchone()
  114. if row is not None:
  115. return Collection(self.database, row)
  116. return None
  117. def collection_by_reference(self, reference: str):
  118. """
  119. get a collection using its reference string
  120. :param reference: reference string
  121. :return: collection
  122. """
  123. with closing(self.database.con.cursor()) as cursor:
  124. cursor.execute('SELECT * FROM collections WHERE reference = ? AND project = ?',
  125. (reference, self.identifier))
  126. row = cursor.fetchone()
  127. if row is not None:
  128. return Collection(self.database, row)
  129. return None
  130. def create_collection(self,
  131. reference: str,
  132. name: str,
  133. description: str,
  134. position: int,
  135. autoselect: bool):
  136. autoselect = 1 if autoselect else 0
  137. with closing(self.database.con.cursor()) as cursor:
  138. cursor.execute('''
  139. INSERT INTO collections
  140. (project, reference, name, description, position, autoselect)
  141. VALUES (?, ?, ?, ?, ?, ?)
  142. ON CONFLICT (project, reference) DO
  143. UPDATE SET name = ?, description = ?, position = ?, autoselect = ?
  144. ''', (self.identifier, reference, name, description, position, autoselect,
  145. name, description, position, autoselect))
  146. # lastrowid is 0 if on conflict clause applies.
  147. # If this is the case we do an extra query to receive the row id.
  148. if cursor.lastrowid > 0:
  149. row_id = cursor.lastrowid
  150. insert = True
  151. else:
  152. cursor.execute('SELECT id FROM collections WHERE project = ? AND reference = ?',
  153. (self.identifier, reference))
  154. row_id = cursor.fetchone()[0]
  155. insert = False
  156. return self.collection(row_id), insert
  157. def remove(self) -> None:
  158. """
  159. remove this project from the database
  160. :return:
  161. """
  162. with closing(self.database.con.cursor()) as cursor:
  163. cursor.execute('DELETE FROM projects WHERE id = ?', [self.identifier])
  164. def set_name(self, name: str) -> None:
  165. """
  166. set this projects name
  167. :param name: new name
  168. :return:
  169. """
  170. with closing(self.database.con.cursor()) as cursor:
  171. cursor.execute('UPDATE projects SET name = ? WHERE id = ?', (name, self.identifier))
  172. self.name = name
  173. def set_description(self, description: str) -> None:
  174. """
  175. set this projects description
  176. :param description: new description
  177. :return:
  178. """
  179. with closing(self.database.con.cursor()) as cursor:
  180. cursor.execute('UPDATE projects SET description = ? WHERE id = ?',
  181. (description, self.identifier))
  182. self.description = description
  183. def count_files(self) -> int:
  184. """
  185. count files associated with this project
  186. :return: count
  187. """
  188. with closing(self.database.con.cursor()) as cursor:
  189. cursor.execute('SELECT COUNT(*) FROM files WHERE project = ?', [self.identifier])
  190. return cursor.fetchone()[0]
  191. def files(self, offset: int = 0, limit: int = -1) -> Iterator[File]:
  192. """
  193. get an iterator of files associated with this project
  194. :param offset: file offset
  195. :param limit: file limit
  196. :return: iterator of files
  197. """
  198. with closing(self.database.con.cursor()) as cursor:
  199. cursor.execute('SELECT * FROM files WHERE project = ? ORDER BY id ASC LIMIT ? OFFSET ?',
  200. (self.identifier, limit, offset))
  201. return map(
  202. lambda row: File(self.database, row),
  203. cursor.fetchall()
  204. )
  205. def count_files_without_results(self) -> int:
  206. """
  207. count files without associated results
  208. :return: count
  209. """
  210. with closing(self.database.con.cursor()) as cursor:
  211. cursor.execute('''
  212. SELECT COUNT(*)
  213. FROM files
  214. LEFT JOIN results ON files.id = results.file
  215. WHERE files.project = ? AND results.id IS NULL
  216. ''', [self.identifier])
  217. return cursor.fetchone()[0]
  218. def files_without_results(self) -> Iterator[File]:
  219. """
  220. get an iterator of files without associated results
  221. :return: list of files
  222. """
  223. with closing(self.database.con.cursor()) as cursor:
  224. cursor.execute('''
  225. SELECT files.*
  226. FROM files
  227. LEFT JOIN results ON files.id = results.file
  228. WHERE files.project = ? AND results.id IS NULL
  229. ORDER BY id ASC
  230. ''', [self.identifier])
  231. for row in cursor:
  232. yield File(self.database, row)
  233. def count_files_without_collection(self) -> int:
  234. """
  235. count files associated with this project but with no collection
  236. :return: count
  237. """
  238. with closing(self.database.con.cursor()) as cursor:
  239. cursor.execute('SELECT COUNT(*) FROM files WHERE project = ? AND collection IS NULL',
  240. [self.identifier])
  241. return cursor.fetchone()[0]
  242. def files_without_collection(self, offset=0, limit=-1) -> Iterator[File]:
  243. """
  244. get an iterator of files without not associated with any collection
  245. :return: list of files
  246. """
  247. with closing(self.database.con.cursor()) as cursor:
  248. cursor.execute('''
  249. SELECT * FROM files
  250. WHERE files.project = ? AND files.collection IS NULL
  251. ORDER BY id ASC
  252. LIMIT ? OFFSET ?
  253. ''', (self.identifier, limit, offset))
  254. for row in cursor:
  255. yield File(self.database, row)
  256. def file(self, identifier) -> Optional[File]:
  257. """
  258. get a file using its unique identifier
  259. :param identifier: unique identifier
  260. :return: file
  261. """
  262. with closing(self.database.con.cursor()) as cursor:
  263. cursor.execute('SELECT * FROM files WHERE id = ? AND project = ?',
  264. (identifier, self.identifier))
  265. row = cursor.fetchone()
  266. if row is not None:
  267. return File(self.database, row)
  268. return None
  269. def add_file(self, uuid: str, file_type: str, name: str, extension: str, size: int,
  270. filename: str, frames: int = None, fps: float = None) -> Tuple[File, bool]:
  271. """
  272. add a file to this project
  273. :param uuid: unique identifier which is used for temporary files
  274. :param file_type: file type (either image or video)
  275. :param name: file name
  276. :param extension: file extension
  277. :param size: file size
  278. :param filename: actual name in filesystem
  279. :param frames: frame count
  280. :param fps: frames per second
  281. :return: file
  282. """
  283. created = int(time())
  284. path = join(self.data_folder, filename + extension)
  285. with closing(self.database.con.cursor()) as cursor:
  286. cursor.execute('''
  287. INSERT INTO files (
  288. uuid, project, type, name, extension, size, created, path, frames, fps
  289. )
  290. VALUES (
  291. ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
  292. )
  293. ON CONFLICT (project, path) DO
  294. UPDATE SET type = ?, name = ?, extension = ?, size = ?, frames = ?, fps = ?
  295. ''', (uuid, self.identifier, file_type, name, extension, size, created, path, frames,
  296. fps, file_type, name, extension, size, frames, fps))
  297. # lastrowid is 0 if on conflict clause applies.
  298. # If this is the case we do an extra query to receive the row id.
  299. if cursor.lastrowid > 0:
  300. row_id = cursor.lastrowid
  301. insert = True
  302. else:
  303. cursor.execute('SELECT id FROM files WHERE project = ? AND path = ?',
  304. (self.identifier, path))
  305. row_id = cursor.fetchone()[0]
  306. insert = False
  307. return self.file(row_id), insert