Database.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320
  1. import sqlite3
  2. from contextlib import closing
  3. from time import time
  4. from typing import Optional, Iterator
  5. from pycs.database.Collection import Collection
  6. from pycs.database.File import File
  7. from pycs.database.LabelProvider import LabelProvider
  8. from pycs.database.Model import Model
  9. from pycs.database.Project import Project
  10. from pycs.database.Result import Result
  11. from pycs.database.discovery.LabelProviderDiscovery import discover as discover_label_providers
  12. from pycs.database.discovery.ModelDiscovery import discover as discover_models
  13. class Database:
  14. """
  15. opens an sqlite database and allows to access several objects
  16. """
  17. def __init__(self, path: str = ':memory:', initialization=True, discovery=True):
  18. """
  19. opens or creates a given sqlite database and creates all required tables
  20. :param path: path to sqlite database
  21. """
  22. # save properties
  23. self.path = path
  24. # initialize database connection
  25. self.con = sqlite3.connect(path)
  26. self.con.execute("PRAGMA foreign_keys = ON")
  27. if initialization:
  28. # create tables
  29. with self, closing(self.con.cursor()) as cursor:
  30. cursor.execute('''
  31. CREATE TABLE IF NOT EXISTS models (
  32. id INTEGER PRIMARY KEY,
  33. name TEXT NOT NULL,
  34. description TEXT,
  35. root_folder TEXT NOT NULL UNIQUE,
  36. supports TEXT NOT NULL
  37. )
  38. ''')
  39. cursor.execute('''
  40. CREATE TABLE IF NOT EXISTS label_providers (
  41. id INTEGER PRIMARY KEY,
  42. name TEXT NOT NULL,
  43. description TEXT,
  44. root_folder TEXT NOT NULL,
  45. configuration_file TEXT NOT NULL,
  46. UNIQUE(root_folder, configuration_file)
  47. )
  48. ''')
  49. cursor.execute('''
  50. CREATE TABLE IF NOT EXISTS projects (
  51. id INTEGER PRIMARY KEY,
  52. name TEXT NOT NULL,
  53. description TEXT,
  54. created INTEGER NOT NULL,
  55. model INTEGER,
  56. label_provider INTEGER,
  57. root_folder TEXT NOT NULL UNIQUE,
  58. external_data BOOL NOT NULL,
  59. data_folder TEXT NOT NULL,
  60. FOREIGN KEY (model) REFERENCES models(id)
  61. ON UPDATE CASCADE ON DELETE SET NULL,
  62. FOREIGN KEY (label_provider) REFERENCES label_providers(id)
  63. ON UPDATE CASCADE ON DELETE SET NULL
  64. )
  65. ''')
  66. cursor.execute('''
  67. CREATE TABLE IF NOT EXISTS labels (
  68. id INTEGER PRIMARY KEY,
  69. project INTEGER NOT NULL,
  70. parent INTEGER,
  71. created INTEGER NOT NULL,
  72. reference TEXT,
  73. name TEXT NOT NULL,
  74. hierarchy_level TEXT,
  75. FOREIGN KEY (project) REFERENCES projects(id)
  76. ON UPDATE CASCADE ON DELETE CASCADE,
  77. FOREIGN KEY (parent) REFERENCES labels(id)
  78. ON UPDATE CASCADE ON DELETE SET NULL,
  79. UNIQUE(project, reference)
  80. )
  81. ''')
  82. cursor.execute('''
  83. CREATE TABLE IF NOT EXISTS collections (
  84. id INTEGER PRIMARY KEY,
  85. project INTEGER NOT NULL,
  86. reference TEXT NOT NULL,
  87. name TEXT NOT NULL,
  88. description TEXT,
  89. position INTEGER NOT NULL,
  90. autoselect INTEGER NOT NULL,
  91. FOREIGN KEY (project) REFERENCES projects(id)
  92. ON UPDATE CASCADE ON DELETE CASCADE,
  93. UNIQUE(project, reference)
  94. )
  95. ''')
  96. cursor.execute('''
  97. CREATE TABLE IF NOT EXISTS files (
  98. id INTEGER PRIMARY KEY,
  99. uuid TEXT NOT NULL,
  100. project INTEGER NOT NULL,
  101. collection INTEGER,
  102. type TEXT NOT NULL,
  103. name TEXT NOT NULL,
  104. extension TEXT NOT NULL,
  105. size INTEGER NOT NULL,
  106. created INTEGER NOT NULL,
  107. path TEXT NOT NULL,
  108. frames INTEGER,
  109. fps FLOAT,
  110. FOREIGN KEY (project) REFERENCES projects(id)
  111. ON UPDATE CASCADE ON DELETE CASCADE,
  112. FOREIGN KEY (collection) REFERENCES collections(id)
  113. ON UPDATE CASCADE ON DELETE SET NULL,
  114. UNIQUE(project, path)
  115. )
  116. ''')
  117. cursor.execute('''
  118. CREATE TABLE IF NOT EXISTS results (
  119. id INTEGER PRIMARY KEY,
  120. file INTEGER NOT NULL,
  121. origin TEXT NOT NULL,
  122. type TEXT NOT NULL,
  123. label INTEGER,
  124. data TEXT,
  125. FOREIGN KEY (file) REFERENCES files(id)
  126. ON UPDATE CASCADE ON DELETE CASCADE
  127. )
  128. ''')
  129. if discovery:
  130. # run discovery modules
  131. with self:
  132. discover_models(self.con)
  133. discover_label_providers(self.con)
  134. def close(self):
  135. """
  136. close database file
  137. """
  138. self.con.close()
  139. def copy(self):
  140. """
  141. Create a copy of this database object. This can be used to access the database
  142. from another thread. Table initialization and model and label provider discovery is
  143. disabled to speedup this function.
  144. :return: Database
  145. """
  146. return Database(self.path, initialization=False, discovery=False)
  147. def commit(self):
  148. """
  149. commit changes
  150. """
  151. self.con.commit()
  152. def __enter__(self):
  153. self.con.__enter__()
  154. return self
  155. def __exit__(self, exc_type, exc_val, exc_tb):
  156. self.con.__exit__(exc_type, exc_val, exc_tb)
  157. def get_object_by_id(self, table_name: str, identifier: int, cls):
  158. """
  159. create an object from cls and a row fetched from table_name and
  160. identified by the identifier
  161. :param table_name: table name
  162. :param identifier: unique identifier
  163. :param cls: class that is used to create the object
  164. :return: object of type cls
  165. """
  166. with closing(self.con.cursor()) as cursor:
  167. cursor.execute(f'SELECT * FROM {table_name} WHERE id = ?', [identifier])
  168. row = cursor.fetchone()
  169. if row is not None:
  170. return cls(self, row)
  171. return None
  172. def get_objects(self, table_name: str, cls):
  173. """
  174. get a list of all available objects in the table
  175. :param table_name: table name
  176. :param cls: class that is used to create the objects
  177. :return: list of object of type cls
  178. """
  179. with closing(self.con.cursor()) as cursor:
  180. cursor.execute(f'SELECT * FROM {table_name}')
  181. for row in cursor:
  182. yield cls(self, row)
  183. def models(self) -> Iterator[Model]:
  184. """
  185. get a list of all available models
  186. :return: iterator of models
  187. """
  188. return self.get_objects("models", Model)
  189. def model(self, identifier: int) -> Optional[Model]:
  190. """
  191. get a model using its unique identifier
  192. :param identifier: unique identifier
  193. :return: model
  194. """
  195. return self.get_object_by_id("models", identifier, Model)
  196. def label_providers(self) -> Iterator[LabelProvider]:
  197. """
  198. get a list of all available label providers
  199. :return: iterator over label providers
  200. """
  201. return self.get_objects("label_providers", LabelProvider)
  202. def label_provider(self, identifier: int) -> Optional[LabelProvider]:
  203. """
  204. get a label provider using its unique identifier
  205. :param identifier: unique identifier
  206. :return: label provider
  207. """
  208. return self.get_object_by_id("label_providers", identifier, LabelProvider)
  209. def projects(self) -> Iterator[Project]:
  210. """
  211. get a list of all available projects
  212. :return: iterator over projects
  213. """
  214. return self.get_objects("projects", Project)
  215. def project(self, identifier: int) -> Optional[Project]:
  216. """
  217. get a project using its unique identifier
  218. :param identifier: unique identifier
  219. :return: project
  220. """
  221. return self.get_object_by_id("projects", identifier, Project)
  222. def create_project(self,
  223. name: str,
  224. description: str,
  225. model: Model,
  226. label_provider: Optional[LabelProvider],
  227. root_folder: str,
  228. external_data: bool,
  229. data_folder: str):
  230. """
  231. insert a project into the database
  232. :param name: project name
  233. :param description: project description
  234. :param model: used model
  235. :param label_provider: used label provider (optional)
  236. :param root_folder: path to project folder
  237. :param external_data: whether an external data directory is used
  238. :param data_folder: path to data folder
  239. :return: created project
  240. """
  241. # prepare some values
  242. created = int(time())
  243. label_provider_id = label_provider.identifier if label_provider is not None else None
  244. # insert statement
  245. with closing(self.con.cursor()) as cursor:
  246. cursor.execute('''
  247. INSERT INTO projects (
  248. name, description, created, model, label_provider, root_folder, external_data,
  249. data_folder
  250. )
  251. VALUES (?, ?, ?, ?, ?, ?, ?, ?)
  252. ''', (name, description, created, model.identifier, label_provider_id, root_folder,
  253. external_data, data_folder))
  254. return self.project(cursor.lastrowid)
  255. def collection(self, identifier: int) -> Optional[Collection]:
  256. """
  257. get a collection using its unique identifier
  258. :param identifier: unique identifier
  259. :return: collection
  260. """
  261. return self.get_object_by_id("collections", identifier, Collection)
  262. def file(self, identifier) -> Optional[File]:
  263. """
  264. get a file using its unique identifier
  265. :param identifier: unique identifier
  266. :return: file
  267. """
  268. return self.get_object_by_id("files", identifier, File)
  269. def result(self, identifier) -> Optional[Result]:
  270. """
  271. get a result using its unique identifier
  272. :param identifier: unique identifier
  273. :return: result
  274. """
  275. return self.get_object_by_id("results", identifier, Result)