scipion-pyworkflow 3.11.0__py3-none-any.whl → 3.11.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pyworkflow/apps/__init__.py +29 -0
- pyworkflow/apps/pw_manager.py +37 -0
- pyworkflow/apps/pw_plot.py +51 -0
- pyworkflow/apps/pw_project.py +130 -0
- pyworkflow/apps/pw_protocol_list.py +143 -0
- pyworkflow/apps/pw_protocol_run.py +51 -0
- pyworkflow/apps/pw_run_tests.py +268 -0
- pyworkflow/apps/pw_schedule_run.py +322 -0
- pyworkflow/apps/pw_sleep.py +37 -0
- pyworkflow/apps/pw_sync_data.py +440 -0
- pyworkflow/apps/pw_viewer.py +78 -0
- pyworkflow/constants.py +1 -1
- pyworkflow/gui/__init__.py +36 -0
- pyworkflow/gui/browser.py +768 -0
- pyworkflow/gui/canvas.py +1190 -0
- pyworkflow/gui/dialog.py +981 -0
- pyworkflow/gui/form.py +2727 -0
- pyworkflow/gui/graph.py +247 -0
- pyworkflow/gui/graph_layout.py +271 -0
- pyworkflow/gui/gui.py +571 -0
- pyworkflow/gui/matplotlib_image.py +233 -0
- pyworkflow/gui/plotter.py +247 -0
- pyworkflow/gui/project/__init__.py +25 -0
- pyworkflow/gui/project/base.py +193 -0
- pyworkflow/gui/project/constants.py +139 -0
- pyworkflow/gui/project/labels.py +205 -0
- pyworkflow/gui/project/project.py +491 -0
- pyworkflow/gui/project/searchprotocol.py +240 -0
- pyworkflow/gui/project/searchrun.py +181 -0
- pyworkflow/gui/project/steps.py +171 -0
- pyworkflow/gui/project/utils.py +332 -0
- pyworkflow/gui/project/variables.py +179 -0
- pyworkflow/gui/project/viewdata.py +472 -0
- pyworkflow/gui/project/viewprojects.py +519 -0
- pyworkflow/gui/project/viewprotocols.py +2141 -0
- pyworkflow/gui/project/viewprotocols_extra.py +562 -0
- pyworkflow/gui/text.py +774 -0
- pyworkflow/gui/tooltip.py +185 -0
- pyworkflow/gui/tree.py +684 -0
- pyworkflow/gui/widgets.py +307 -0
- pyworkflow/mapper/__init__.py +26 -0
- pyworkflow/mapper/mapper.py +226 -0
- pyworkflow/mapper/sqlite.py +1583 -0
- pyworkflow/mapper/sqlite_db.py +145 -0
- pyworkflow/object.py +1 -0
- pyworkflow/plugin.py +4 -4
- pyworkflow/project/__init__.py +31 -0
- pyworkflow/project/config.py +454 -0
- pyworkflow/project/manager.py +180 -0
- pyworkflow/project/project.py +2095 -0
- pyworkflow/project/usage.py +165 -0
- pyworkflow/protocol/__init__.py +38 -0
- pyworkflow/protocol/bibtex.py +48 -0
- pyworkflow/protocol/constants.py +87 -0
- pyworkflow/protocol/executor.py +515 -0
- pyworkflow/protocol/hosts.py +318 -0
- pyworkflow/protocol/launch.py +277 -0
- pyworkflow/protocol/package.py +42 -0
- pyworkflow/protocol/params.py +781 -0
- pyworkflow/protocol/protocol.py +2712 -0
- pyworkflow/resources/protlabels.xcf +0 -0
- pyworkflow/resources/sprites.png +0 -0
- pyworkflow/resources/sprites.xcf +0 -0
- pyworkflow/template.py +1 -1
- pyworkflow/tests/__init__.py +29 -0
- pyworkflow/tests/test_utils.py +25 -0
- pyworkflow/tests/tests.py +342 -0
- pyworkflow/utils/__init__.py +38 -0
- pyworkflow/utils/dataset.py +414 -0
- pyworkflow/utils/echo.py +104 -0
- pyworkflow/utils/graph.py +169 -0
- pyworkflow/utils/log.py +293 -0
- pyworkflow/utils/path.py +528 -0
- pyworkflow/utils/process.py +154 -0
- pyworkflow/utils/profiler.py +92 -0
- pyworkflow/utils/progressbar.py +154 -0
- pyworkflow/utils/properties.py +618 -0
- pyworkflow/utils/reflection.py +129 -0
- pyworkflow/utils/utils.py +880 -0
- pyworkflow/utils/which.py +229 -0
- pyworkflow/webservices/__init__.py +8 -0
- pyworkflow/webservices/config.py +8 -0
- pyworkflow/webservices/notifier.py +152 -0
- pyworkflow/webservices/repository.py +59 -0
- pyworkflow/webservices/workflowhub.py +86 -0
- pyworkflowtests/tests/__init__.py +0 -0
- pyworkflowtests/tests/test_canvas.py +72 -0
- pyworkflowtests/tests/test_domain.py +45 -0
- pyworkflowtests/tests/test_logs.py +74 -0
- pyworkflowtests/tests/test_mappers.py +392 -0
- pyworkflowtests/tests/test_object.py +507 -0
- pyworkflowtests/tests/test_project.py +42 -0
- pyworkflowtests/tests/test_protocol_execution.py +146 -0
- pyworkflowtests/tests/test_protocol_export.py +78 -0
- pyworkflowtests/tests/test_protocol_output.py +158 -0
- pyworkflowtests/tests/test_streaming.py +47 -0
- pyworkflowtests/tests/test_utils.py +210 -0
- {scipion_pyworkflow-3.11.0.dist-info → scipion_pyworkflow-3.11.2.dist-info}/METADATA +2 -2
- scipion_pyworkflow-3.11.2.dist-info/RECORD +162 -0
- scipion_pyworkflow-3.11.0.dist-info/RECORD +0 -71
- {scipion_pyworkflow-3.11.0.dist-info → scipion_pyworkflow-3.11.2.dist-info}/WHEEL +0 -0
- {scipion_pyworkflow-3.11.0.dist-info → scipion_pyworkflow-3.11.2.dist-info}/entry_points.txt +0 -0
- {scipion_pyworkflow-3.11.0.dist-info → scipion_pyworkflow-3.11.2.dist-info}/licenses/LICENSE.txt +0 -0
- {scipion_pyworkflow-3.11.0.dist-info → scipion_pyworkflow-3.11.2.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,1583 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
# **************************************************************************
|
3
|
+
# *
|
4
|
+
# * Authors: J.M. De la Rosa Trevin (delarosatrevin@scilifelab.se) [1]
|
5
|
+
# *
|
6
|
+
# * [1] SciLifeLab, Stockholm University
|
7
|
+
# *
|
8
|
+
# * This program is free software: you can redistribute it and/or modify
|
9
|
+
# * it under the terms of the GNU General Public License as published by
|
10
|
+
# * the Free Software Foundation, either version 3 of the License, or
|
11
|
+
# * (at your option) any later version.
|
12
|
+
# *
|
13
|
+
# * This program is distributed in the hope that it will be useful,
|
14
|
+
# * but WITHOUT ANY WARRANTY; without even the implied warranty of
|
15
|
+
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
16
|
+
# * GNU General Public License for more details.
|
17
|
+
# *
|
18
|
+
# * You should have received a copy of the GNU General Public License
|
19
|
+
# * along with this program. If not, see <https://www.gnu.org/licenses/>.
|
20
|
+
# *
|
21
|
+
# * All comments concerning this program package may be sent to the
|
22
|
+
# * e-mail address 'scipion@cnb.csic.es'
|
23
|
+
# *
|
24
|
+
# **************************************************************************
|
25
|
+
import logging
|
26
|
+
logger = logging.getLogger(__name__)
|
27
|
+
import re
|
28
|
+
from collections import OrderedDict
|
29
|
+
|
30
|
+
from pyworkflow import Config, ID_ATTRIBUTE, ID_COLUMN
|
31
|
+
from pyworkflow.utils import replaceExt, joinExt, valueToList
|
32
|
+
from .sqlite_db import SqliteDb, OperationalError
|
33
|
+
from .mapper import Mapper
|
34
|
+
|
35
|
+
ID = ID_COLUMN
|
36
|
+
CREATION = 'creation'
|
37
|
+
PARENT_ID = 'parent_id'
|
38
|
+
CLASSNAME = 'classname'
|
39
|
+
NAME = 'name'
|
40
|
+
|
41
|
+
|
42
|
+
class SqliteMapper(Mapper):
|
43
|
+
"""Specific Mapper implementation using Sqlite database"""
|
44
|
+
def __init__(self, dbName, dictClasses=None):
|
45
|
+
Mapper.__init__(self, dictClasses)
|
46
|
+
self.__initObjDict()
|
47
|
+
self.__initUpdateDict()
|
48
|
+
try:
|
49
|
+
self.db = SqliteObjectsDb(dbName)
|
50
|
+
except Exception as ex:
|
51
|
+
raise Exception('Error creating SqliteMapper, dbName: %s'
|
52
|
+
'\n error: %s' % (dbName, ex))
|
53
|
+
|
54
|
+
def close(self):
|
55
|
+
self.db.close()
|
56
|
+
|
57
|
+
def commit(self):
|
58
|
+
self.db.commit()
|
59
|
+
|
60
|
+
def __getObjectValue(self, obj):
|
61
|
+
""" Get the value of the object to be stored.
|
62
|
+
We need to handle the special case of pointer, where we should
|
63
|
+
store as value the object id of the pointed object.
|
64
|
+
"""
|
65
|
+
value = obj.getObjValue()
|
66
|
+
|
67
|
+
if obj.isPointer() and obj.hasValue():
|
68
|
+
if value.hasObjId(): # Check the object has been stored previously
|
69
|
+
value = value.strId() # For pointers store the id of referenced object
|
70
|
+
else:
|
71
|
+
self.updatePendingPointers.append(obj)
|
72
|
+
value = "Pending update"
|
73
|
+
|
74
|
+
return value
|
75
|
+
|
76
|
+
def __insert(self, obj, namePrefix=None):
|
77
|
+
if not hasattr(obj, '_objDoStore'):
|
78
|
+
logger.info("MAPPER: object '%s' doesn't seem to be an Object subclass,"
|
79
|
+
" it does not have attribute '_objDoStore'. "
|
80
|
+
"Insert skipped." % obj)
|
81
|
+
return
|
82
|
+
obj._objId = self.db.insertObject(obj._objName, obj.getClassName(),
|
83
|
+
self.__getObjectValue(obj),
|
84
|
+
obj._objParentId,
|
85
|
+
obj._objLabel, obj._objComment)
|
86
|
+
self.updateDict[obj._objId] = obj
|
87
|
+
sid = obj.strId()
|
88
|
+
if namePrefix is None:
|
89
|
+
namePrefix = sid
|
90
|
+
else:
|
91
|
+
namePrefix = joinExt(namePrefix, sid)
|
92
|
+
self.insertChilds(obj, namePrefix)
|
93
|
+
|
94
|
+
def insert(self, obj):
|
95
|
+
"""Insert a new object into the system, the id will be set"""
|
96
|
+
self.__insert(obj)
|
97
|
+
|
98
|
+
def insertChild(self, obj, key, attr, namePrefix=None):
|
99
|
+
if not hasattr(attr, '_objDoStore'):
|
100
|
+
logger.info("MAPPER: object '%s' doesn't seem to be an Object subclass,"
|
101
|
+
" it does not have attribute '_objDoStore'. \n"
|
102
|
+
"Insert skipped." % attr)
|
103
|
+
return
|
104
|
+
|
105
|
+
if namePrefix is None:
|
106
|
+
namePrefix = self.__getNamePrefix(obj)
|
107
|
+
attr._objName = joinExt(namePrefix, key)
|
108
|
+
attr._objParentId = obj._objId
|
109
|
+
self.__insert(attr, namePrefix)
|
110
|
+
|
111
|
+
def insertChilds(self, obj, namePrefix=None):
|
112
|
+
""" Insert childs of an object, if namePrefix is None,
|
113
|
+
the it will be deduced from obj. """
|
114
|
+
# This is also done in insertChild, but avoid
|
115
|
+
# doing the same for every child element
|
116
|
+
if namePrefix is None:
|
117
|
+
namePrefix = self.__getNamePrefix(obj)
|
118
|
+
for key, attr in obj.getAttributesToStore():
|
119
|
+
self.insertChild(obj, key, attr, namePrefix)
|
120
|
+
|
121
|
+
def deleteChilds(self, obj):
|
122
|
+
namePrefix = self.__getNamePrefix(obj)
|
123
|
+
self.db.deleteChildObjects(namePrefix)
|
124
|
+
|
125
|
+
def deleteAll(self):
|
126
|
+
""" Delete all objects stored """
|
127
|
+
self.db.deleteAll()
|
128
|
+
|
129
|
+
def delete(self, obj):
|
130
|
+
"""Delete an object and all its childs"""
|
131
|
+
self.deleteChilds(obj)
|
132
|
+
self.db.deleteObject(obj.getObjId())
|
133
|
+
|
134
|
+
def __getNamePrefix(self, obj):
|
135
|
+
if len(obj._objName) > 0 and '.' in obj._objName:
|
136
|
+
return replaceExt(obj._objName, obj.strId())
|
137
|
+
return obj.strId()
|
138
|
+
|
139
|
+
def __printObj(self, obj):
|
140
|
+
logger.info("obj._objId: %s" % obj._objId)
|
141
|
+
logger.info("obj._objParentId: %s" % obj._objParentId)
|
142
|
+
logger.info("obj._objName: %s"% obj._objName)
|
143
|
+
logger.info("obj.getObjValue(): %s" % obj.getObjValue())
|
144
|
+
|
145
|
+
def updateTo(self, obj, level=1):
|
146
|
+
self.__initUpdateDict()
|
147
|
+
self.__updateTo(obj, level)
|
148
|
+
# Update pending pointers to objects
|
149
|
+
for ptr in self.updatePendingPointers:
|
150
|
+
self.db.updateObject(ptr._objId, ptr._objName,
|
151
|
+
Mapper.getObjectPersistingClassName(ptr),
|
152
|
+
self.__getObjectValue(obj), ptr._objParentId,
|
153
|
+
ptr._objLabel, ptr._objComment)
|
154
|
+
|
155
|
+
# Delete any child objects that have not been found.
|
156
|
+
# This could be the case if some elements (such as inside List)
|
157
|
+
# were stored in the database and were removed from the object
|
158
|
+
self.db.deleteMissingObjectsByAncestor(self.__getNamePrefix(obj),
|
159
|
+
self.updateDict.keys())
|
160
|
+
|
161
|
+
def __updateTo(self, obj, level):
|
162
|
+
self.db.updateObject(obj._objId, obj._objName,
|
163
|
+
Mapper.getObjectPersistingClassName(obj),
|
164
|
+
self.__getObjectValue(obj), obj._objParentId,
|
165
|
+
obj._objLabel, obj._objComment)
|
166
|
+
|
167
|
+
if obj.getObjId() in self.updateDict:
|
168
|
+
raise Exception('Circular reference, object: %s found twice'
|
169
|
+
% obj.getName())
|
170
|
+
|
171
|
+
self.updateDict[obj._objId] = obj
|
172
|
+
|
173
|
+
for key, attr in obj.getAttributesToStore():
|
174
|
+
if attr._objId is None: # Insert new items from the previous state
|
175
|
+
attr._objParentId = obj._objId
|
176
|
+
namePrefix = self.__getNamePrefix(obj)
|
177
|
+
attr._objName = joinExt(namePrefix, key)
|
178
|
+
self.__insert(attr, namePrefix)
|
179
|
+
else:
|
180
|
+
self.__updateTo(attr, level + 2)
|
181
|
+
|
182
|
+
def updateFrom(self, obj):
|
183
|
+
objRow = self.db.selectObjectById(obj._objId)
|
184
|
+
self.fillObject(obj, objRow)
|
185
|
+
|
186
|
+
def selectById(self, objId):
|
187
|
+
"""Build the object which id is objId"""
|
188
|
+
if objId in self.objDict:
|
189
|
+
obj = self.objDict[objId]
|
190
|
+
|
191
|
+
if __debug__:
|
192
|
+
# If not optimized code
|
193
|
+
logger.debug("Object with id %s already loaded: %s", objId, obj)
|
194
|
+
else:
|
195
|
+
objRow = self.db.selectObjectById(objId)
|
196
|
+
if objRow is None:
|
197
|
+
obj = None
|
198
|
+
else:
|
199
|
+
obj = self._buildObjectFromClass(objRow['classname'])
|
200
|
+
if obj is not None:
|
201
|
+
self.fillObject(obj, objRow)
|
202
|
+
return obj
|
203
|
+
|
204
|
+
def exists(self, objId):
|
205
|
+
return self.db.doesRowExist(objId)
|
206
|
+
|
207
|
+
def getParent(self, obj):
|
208
|
+
""" Retrieve the parent object of another. """
|
209
|
+
return self.selectById(obj._objParentId)
|
210
|
+
|
211
|
+
def fillObjectWithRow(self, obj, objRow):
|
212
|
+
""" Fill the object with row data. """
|
213
|
+
|
214
|
+
rowId = objRow[ID]
|
215
|
+
rowName = self._getStrValue(objRow['name'])
|
216
|
+
|
217
|
+
if not hasattr(obj, ID_ATTRIBUTE):
|
218
|
+
raise Exception(f"Entry '{rowName}' (id={rowId}) in the database, stored as '{objRow['classname']}'"
|
219
|
+
f", is being mapped to {type(obj)} object.")
|
220
|
+
|
221
|
+
obj._objId = rowId
|
222
|
+
|
223
|
+
self.objDict[rowId] = obj
|
224
|
+
obj._objName = rowName
|
225
|
+
obj._objLabel = self._getStrValue(objRow['label'])
|
226
|
+
obj._objComment = self._getStrValue(objRow['comment'])
|
227
|
+
obj._objCreation = self._getStrValue(objRow[CREATION])
|
228
|
+
objValue = objRow['value']
|
229
|
+
obj._objParentId = objRow[PARENT_ID]
|
230
|
+
|
231
|
+
if obj.isPointer():
|
232
|
+
if objValue is not None:
|
233
|
+
objValue = self.selectById(int(objValue))
|
234
|
+
# This is necessary in some specific cases. E.g.:
|
235
|
+
# CTF consensus creating:
|
236
|
+
# A.- Micrographs
|
237
|
+
# B.- CTFs --> pointing to micrographs in this same protocol(#1)
|
238
|
+
# When loading this kind of protocol the sequence is as follows:
|
239
|
+
# 1 Loading of protocol consuming consensus CTF output ..
|
240
|
+
# ...
|
241
|
+
# finds inputCtf (DIRECT pointer to B)
|
242
|
+
# 2 loads set properties
|
243
|
+
# ...
|
244
|
+
# 2.4 pointer to micrographs (Pointer to Consensus + extended)
|
245
|
+
# 2.4.1 pointee loads (Consensus protocol)
|
246
|
+
# ...
|
247
|
+
# ...
|
248
|
+
# 2.4.n _extended of 2.4 is loaded since is a child of consensus
|
249
|
+
# 2.4 obj.set() for 2.4 pointer --> will reset the extended to None.
|
250
|
+
obj.set(objValue, cleanExtended=False)
|
251
|
+
else:
|
252
|
+
try:
|
253
|
+
obj.set(objValue)
|
254
|
+
except Exception as e:
|
255
|
+
# Case for parameter type change. Loading the project tolerates type changes like Float to Int.
|
256
|
+
# But when running a protocol loads happens differently (maybe something to look at) and comes here.
|
257
|
+
logger.error("Can't set %s to %s. Maybe its type has changed!. Continues with default value %s." %
|
258
|
+
(objValue, rowName, obj.get()))
|
259
|
+
|
260
|
+
def fillObject(self, obj, objRow, includeChildren=True):
|
261
|
+
""" Fills an already instantiated object the data in a row, including children
|
262
|
+
|
263
|
+
NOTE: This, incase children are included, makes a query to the db with all its children 'like 2.*'.
|
264
|
+
At some point it calls selectById triggering the loading of other protocols and ancestors.
|
265
|
+
|
266
|
+
:param obj: Object to fill
|
267
|
+
:param objRow: row with the values
|
268
|
+
:param includeChildren: (True). If true children are also populated
|
269
|
+
|
270
|
+
"""
|
271
|
+
|
272
|
+
self.fillObjectWithRow(obj, objRow)
|
273
|
+
namePrefix = self.__getNamePrefix(obj)
|
274
|
+
|
275
|
+
if includeChildren:
|
276
|
+
childs = self.db.selectObjectsByAncestor(namePrefix)
|
277
|
+
|
278
|
+
for childRow in childs:
|
279
|
+
|
280
|
+
childParts = childRow[NAME].split('.')
|
281
|
+
childName = childParts[-1]
|
282
|
+
childId = childRow[ID]
|
283
|
+
parentId = int(childParts[-2])
|
284
|
+
# Here we are assuming that always the parent have
|
285
|
+
# been processed first, so it will be in the dictionary
|
286
|
+
parentObj = self.objDict.get(parentId, None)
|
287
|
+
if parentObj is None: # Something went wrong
|
288
|
+
continue
|
289
|
+
|
290
|
+
# If id already in the objDict skip all this
|
291
|
+
if childId in self.objDict.keys():
|
292
|
+
setattr(parentObj, childName, self.objDict[childId])
|
293
|
+
continue
|
294
|
+
|
295
|
+
# Try to get the instance from the parent
|
296
|
+
childObj = getattr(parentObj, childName, None)
|
297
|
+
|
298
|
+
# If parent does not have that attribute...
|
299
|
+
if childObj is None:
|
300
|
+
# Instantiate it based on the class Name
|
301
|
+
childObj = self._buildObjectFromClass(childRow[CLASSNAME])
|
302
|
+
|
303
|
+
# If we have any problem building the object, just ignore it
|
304
|
+
if childObj is None:
|
305
|
+
# This is the case for deprecated types.
|
306
|
+
continue
|
307
|
+
setattr(parentObj, childName, childObj)
|
308
|
+
|
309
|
+
self.fillObjectWithRow(childObj, childRow)
|
310
|
+
|
311
|
+
def __buildObject(self, row):
|
312
|
+
""" Builds and object, either based on the parent attribute, or a new
|
313
|
+
one based on the class. """
|
314
|
+
parentId = self._getParentIdFromRow(row)
|
315
|
+
# If there is no parent...
|
316
|
+
if parentId is None:
|
317
|
+
# It must be a Root object, use the class
|
318
|
+
return self._buildObjectFromClass(self._getClassFromRow(row))
|
319
|
+
else:
|
320
|
+
# Try to get the instance from the parent
|
321
|
+
name = self._getNameFromRow(row)
|
322
|
+
childParts = name.split('.')
|
323
|
+
childName = childParts[-1]
|
324
|
+
|
325
|
+
# Get the parent, we should have it cached
|
326
|
+
parentObj = self.objDict.get(parentId, None)
|
327
|
+
if parentObj is None: # Something went wrong
|
328
|
+
logger.warning("Parent object (id=%d) was not found, "
|
329
|
+
"object: %s. Ignored." % (parentId, name))
|
330
|
+
return None
|
331
|
+
|
332
|
+
childObj = getattr(parentObj, childName, None)
|
333
|
+
|
334
|
+
# If parent object does not have that attribute
|
335
|
+
if childObj is None:
|
336
|
+
childObj = self._buildObjectFromClass(row[CLASSNAME])
|
337
|
+
# If we have any problem building the object, just ignore it
|
338
|
+
if childObj is None:
|
339
|
+
return None
|
340
|
+
|
341
|
+
setattr(parentObj, childName, childObj)
|
342
|
+
|
343
|
+
return childObj
|
344
|
+
|
345
|
+
def __objFromRow(self, objRow, includeChildren=True):
|
346
|
+
objClassName = objRow['classname']
|
347
|
+
obj = self._buildObjectFromClass(objClassName)
|
348
|
+
|
349
|
+
if obj is not None:
|
350
|
+
self.fillObject(obj, objRow, includeChildren)
|
351
|
+
|
352
|
+
return obj
|
353
|
+
|
354
|
+
def __iterObjectsFromRows(self, objRows, objectFilter=None):
|
355
|
+
for objRow in objRows:
|
356
|
+
obj = self.objDict.get(objRow['id'], None) or self.__objFromRow(objRow)
|
357
|
+
|
358
|
+
if (obj is not None and
|
359
|
+
objectFilter is None or objectFilter(obj)):
|
360
|
+
yield obj
|
361
|
+
|
362
|
+
def __objectsFromRows(self, objRows, iterate=False, objectFilter=None):
|
363
|
+
"""Create a set of object from a set of rows
|
364
|
+
Params:
|
365
|
+
objRows: rows result from a db select.
|
366
|
+
iterate: if True, iterates over all elements, if False the whole
|
367
|
+
list is returned
|
368
|
+
objectFilter: function to filter some of the objects of the
|
369
|
+
results.
|
370
|
+
"""
|
371
|
+
if not iterate:
|
372
|
+
return [obj for obj in self.__iterObjectsFromRows(objRows,
|
373
|
+
objectFilter)]
|
374
|
+
else:
|
375
|
+
return self.__iterObjectsFromRows(objRows, objectFilter)
|
376
|
+
|
377
|
+
def __initObjDict(self):
|
378
|
+
""" Clear the objDict cache """
|
379
|
+
self.objDict = {}
|
380
|
+
|
381
|
+
def __initUpdateDict(self):
|
382
|
+
""" Clear the updateDict cache """
|
383
|
+
self.updateDict = {}
|
384
|
+
# This is used to store pointers that pointed object are not stored yet
|
385
|
+
self.updatePendingPointers = []
|
386
|
+
|
387
|
+
def selectBy(self, iterate=False, objectFilter=None, **args):
|
388
|
+
"""Select object meetings some criteria"""
|
389
|
+
self.__initObjDict()
|
390
|
+
objRows = self.db.selectObjectsBy(**args)
|
391
|
+
return self.__objectsFromRows(objRows, iterate, objectFilter)
|
392
|
+
|
393
|
+
def selectByClass(self, className, includeSubclasses=True, iterate=False,
|
394
|
+
objectFilter=None):
|
395
|
+
self.__initObjDict()
|
396
|
+
|
397
|
+
if includeSubclasses:
|
398
|
+
from pyworkflow.utils.reflection import getSubclasses
|
399
|
+
whereStr = "classname='%s'" % className
|
400
|
+
base = self.dictClasses.get(className)
|
401
|
+
subDict = getSubclasses(base, self.dictClasses)
|
402
|
+
for k, v in subDict.items():
|
403
|
+
if issubclass(v, base):
|
404
|
+
whereStr += " OR classname='%s'" % k
|
405
|
+
objRows = self.db.selectObjectsWhere(whereStr)
|
406
|
+
return self.__objectsFromRows(objRows, iterate, objectFilter)
|
407
|
+
else:
|
408
|
+
return self.selectBy(iterate=iterate, classname=className)
|
409
|
+
|
410
|
+
def selectAll(self, iterate=False, objectFilter=None):
|
411
|
+
self.__initObjDict()
|
412
|
+
objRows = self.db.selectObjectsByParent(parent_id=None)
|
413
|
+
return self.__objectsFromRows(objRows, iterate, objectFilter)
|
414
|
+
|
415
|
+
def selectAllBatch(self, objectFilter=None):
|
416
|
+
""" Select all the row at once for all the project
|
417
|
+
|
418
|
+
Returns:
|
419
|
+
all the protocols populated with the data from the DB
|
420
|
+
|
421
|
+
"""
|
422
|
+
self.__initObjDict()
|
423
|
+
|
424
|
+
# Get all the data from Objects table sorted by Name
|
425
|
+
objAll = self.db.selectAllObjects()
|
426
|
+
|
427
|
+
# We should have first the protocol lines
|
428
|
+
# then each of the protocol attributes
|
429
|
+
# at then there is the creation time
|
430
|
+
|
431
|
+
# Dictionary to store objects
|
432
|
+
objs = []
|
433
|
+
|
434
|
+
# For each row
|
435
|
+
for row in objAll:
|
436
|
+
obj = self._getObjectFromRow(row)
|
437
|
+
|
438
|
+
if obj is not None and objectFilter is None or objectFilter(obj):
|
439
|
+
objs.append(obj)
|
440
|
+
|
441
|
+
return objs
|
442
|
+
|
443
|
+
def _getObjectFromRow(self, row):
|
444
|
+
""" Creates and fills and object described iin row
|
445
|
+
|
446
|
+
:param row: A row with the Class to instantiate, and value to set."""
|
447
|
+
|
448
|
+
# Get the ID first
|
449
|
+
identifier = row[ID]
|
450
|
+
|
451
|
+
# If already in the dictionary we skipp al this
|
452
|
+
if identifier in self.objDict.keys():
|
453
|
+
return self.objDict[identifier]
|
454
|
+
|
455
|
+
# Build the object without children
|
456
|
+
obj = self.__buildObject(row)
|
457
|
+
|
458
|
+
# If an object was created
|
459
|
+
if obj is not None:
|
460
|
+
try:
|
461
|
+
# Fill object attributes with row values
|
462
|
+
self.fillObjectWithRow(obj, row)
|
463
|
+
|
464
|
+
# Add it to the obj cache, we might need it later to assign
|
465
|
+
# attributes
|
466
|
+
self.objDict[obj._objId] = obj
|
467
|
+
|
468
|
+
return obj
|
469
|
+
|
470
|
+
except Exception as e:
|
471
|
+
# Tolerate errors when loading attributes.
|
472
|
+
# This could happen then a protocol has change a param to a new type not compatible
|
473
|
+
# with previous values. Warn properly about it.
|
474
|
+
logger.warning("Can't load the row (%s, %s, %s) form the database to memory. This could cause future error." %
|
475
|
+
(identifier, row[NAME], row[PARENT_ID]))
|
476
|
+
|
477
|
+
def _getObjectFromDictionary(self, objId):
|
478
|
+
return self.objDict[objId]
|
479
|
+
|
480
|
+
@staticmethod
|
481
|
+
def _getIdFromRow(row):
|
482
|
+
return SqliteMapper._getFieldFromRow(ID, row)
|
483
|
+
|
484
|
+
@staticmethod
|
485
|
+
def _getParentIdFromRow(row):
|
486
|
+
return SqliteMapper._getFieldFromRow(PARENT_ID, row)
|
487
|
+
|
488
|
+
@staticmethod
|
489
|
+
def _getClassFromRow(row):
|
490
|
+
return SqliteMapper._getFieldFromRow(CLASSNAME, row)
|
491
|
+
|
492
|
+
@staticmethod
|
493
|
+
def _getNameFromRow(row):
|
494
|
+
return SqliteMapper._getFieldFromRow(NAME, row)
|
495
|
+
|
496
|
+
@staticmethod
|
497
|
+
def _getFieldFromRow(fieldName, row):
|
498
|
+
return row[fieldName]
|
499
|
+
|
500
|
+
def insertRelation(self, relName, creatorObj, parentObj, childObj,
|
501
|
+
parentExt=None, childExt=None):
|
502
|
+
""" This function will add a new relation between two objects.
|
503
|
+
Params:
|
504
|
+
relName: the name of the relation to be added.
|
505
|
+
creatorObj: this object will be the one who register the relation.
|
506
|
+
parentObj: this is "parent" in the relation
|
507
|
+
childObj: this is "child" in the relation
|
508
|
+
"""
|
509
|
+
for o in [creatorObj, parentObj, childObj]:
|
510
|
+
if not o.hasObjId():
|
511
|
+
raise Exception("Before adding a relation, the object should "
|
512
|
+
"be stored in mapper")
|
513
|
+
self.db.insertRelation(relName, creatorObj.getObjId(),
|
514
|
+
parentObj.getObjId(), childObj.getObjId(),
|
515
|
+
parentExt, childExt)
|
516
|
+
|
517
|
+
def __objectsFromIds(self, objIds):
|
518
|
+
"""Return a list of objects, given a list of id's
|
519
|
+
"""
|
520
|
+
return [self.selectById(rowId[ID]) for rowId in objIds]
|
521
|
+
|
522
|
+
def getRelationChilds(self, relName, parentObj):
|
523
|
+
""" Return all "child" objects for a given relation.
|
524
|
+
Params:
|
525
|
+
relName: the name of the relation.
|
526
|
+
parentObj: this is "parent" in the relation
|
527
|
+
Returns:
|
528
|
+
a list of "child" objects.
|
529
|
+
"""
|
530
|
+
childIds = self.db.selectRelationChilds(relName, parentObj.getObjId())
|
531
|
+
|
532
|
+
return self.__objectsFromIds(childIds)
|
533
|
+
|
534
|
+
def getRelationParents(self, relName, childObj):
|
535
|
+
""" Return all "parent" objects for a given relation.
|
536
|
+
Params:
|
537
|
+
relName: the name of the relation.
|
538
|
+
childObj: this is "child" in the relation
|
539
|
+
Returns:
|
540
|
+
a list of "parent" objects.
|
541
|
+
"""
|
542
|
+
parentIds = self.db.selectRelationParents(relName, childObj.getObjId())
|
543
|
+
|
544
|
+
return self.__objectsFromIds(parentIds)
|
545
|
+
|
546
|
+
def getRelationsByCreator(self, creatorObj):
|
547
|
+
""" Return all relations created by creatorObj. """
|
548
|
+
return self.db.selectRelationsByCreator(creatorObj.getObjId())
|
549
|
+
|
550
|
+
def getRelationsByName(self, relationName):
|
551
|
+
""" Return all relations stored of a given type. """
|
552
|
+
return self.db.selectRelationsByName(relationName)
|
553
|
+
|
554
|
+
def deleteRelations(self, creatorObj):
|
555
|
+
""" Delete all relations created by object creatorObj """
|
556
|
+
self.db.deleteRelationsByCreator(creatorObj.getObjId())
|
557
|
+
|
558
|
+
def insertRelationData(self, relName, creatorId, parentId, childId,
|
559
|
+
parentExtended=None, childExtended=None):
|
560
|
+
self.db.insertRelation(relName, creatorId, parentId, childId,
|
561
|
+
parentExtended, childExtended)
|
562
|
+
|
563
|
+
|
564
|
+
class SqliteObjectsDb(SqliteDb):
|
565
|
+
"""Class to handle a Sqlite database.
|
566
|
+
It will create connection, execute queries and commands"""
|
567
|
+
# Maintain the current version of the DB schema
|
568
|
+
# useful for future updates and backward compatibility
|
569
|
+
# version should be an integer number
|
570
|
+
VERSION = 1
|
571
|
+
|
572
|
+
SELECT = ("SELECT id, parent_id, name, classname, value, label, comment, "
|
573
|
+
"datetime(creation, 'localtime') as creation FROM Objects")
|
574
|
+
DELETE = "DELETE FROM Objects WHERE "
|
575
|
+
DELETE_SEQUENCE = "DELETE FROM SQLITE_SEQUENCE WHERE name='Objects'"
|
576
|
+
|
577
|
+
SELECT_RELATION = ("SELECT object_%s_id AS id FROM Relations "
|
578
|
+
"WHERE name=? AND object_%s_id=?")
|
579
|
+
SELECT_RELATIONS = "SELECT * FROM Relations WHERE "
|
580
|
+
EXISTS = "SELECT EXISTS(SELECT 1 FROM Objects WHERE %s=? LIMIT 1)"
|
581
|
+
|
582
|
+
def selectCmd(self, whereStr, orderByStr=' ORDER BY id'):
|
583
|
+
|
584
|
+
whereStr = " WHERE " + whereStr if whereStr is not None else ''
|
585
|
+
return self.SELECT + whereStr + orderByStr
|
586
|
+
|
587
|
+
def __init__(self, dbName, timeout=1000, pragmas=None):
|
588
|
+
SqliteDb.__init__(self)
|
589
|
+
self._pragmas = pragmas or {}
|
590
|
+
self._createConnection(dbName, timeout)
|
591
|
+
self._initialize()
|
592
|
+
|
593
|
+
def _initialize(self):
|
594
|
+
""" Create the required tables if needed. """
|
595
|
+
tables = self.getTables()
|
596
|
+
# Check if the tables have been created or not
|
597
|
+
if not tables:
|
598
|
+
self.__createTables()
|
599
|
+
else:
|
600
|
+
self.__updateTables()
|
601
|
+
|
602
|
+
def __createTables(self):
|
603
|
+
"""Create required tables if don't exists"""
|
604
|
+
# Enable foreign keys
|
605
|
+
self.setVersion(self.VERSION)
|
606
|
+
self._pragmas['foreing_keys'] = "ON"
|
607
|
+
for pragma in self._pragmas.items():
|
608
|
+
self.executeCommand("PRAGMA %s=%s" % pragma)
|
609
|
+
# Create the Objects table
|
610
|
+
self.executeCommand("""CREATE TABLE IF NOT EXISTS Objects
|
611
|
+
(id INTEGER PRIMARY KEY AUTOINCREMENT,
|
612
|
+
parent_id INTEGER REFERENCES Objects(id),
|
613
|
+
name TEXT, -- object name
|
614
|
+
classname TEXT, -- object's class name
|
615
|
+
value TEXT DEFAULT NULL, -- object value, used for Scalars
|
616
|
+
label TEXT DEFAULT NULL, -- object label, text used for display
|
617
|
+
comment TEXT DEFAULT NULL, -- object comment, text used for annotations
|
618
|
+
creation DATE -- creation date and time of the object
|
619
|
+
)""")
|
620
|
+
# Create the Relations table
|
621
|
+
self.executeCommand("""CREATE TABLE IF NOT EXISTS Relations
|
622
|
+
(id INTEGER PRIMARY KEY AUTOINCREMENT,
|
623
|
+
parent_id INTEGER REFERENCES Objects(id), -- object that created the relation
|
624
|
+
name TEXT, -- relation name
|
625
|
+
classname TEXT DEFAULT NULL, -- relation's class name
|
626
|
+
value TEXT DEFAULT NULL, -- relation value
|
627
|
+
label TEXT DEFAULT NULL, -- relation label, text used for display
|
628
|
+
comment TEXT DEFAULT NULL, -- relation comment, text used for annotations
|
629
|
+
object_parent_id INTEGER REFERENCES Objects(id) ON DELETE CASCADE,
|
630
|
+
object_child_id INTEGER REFERENCES Objects(id) ON DELETE CASCADE,
|
631
|
+
creation DATE, -- creation date and time of the object
|
632
|
+
object_parent_extended TEXT DEFAULT NULL, -- extended property to consider internal objects
|
633
|
+
object_child_extended TEXT DEFAULT NULL
|
634
|
+
)""")
|
635
|
+
self.commit()
|
636
|
+
|
637
|
+
def __updateTables(self):
|
638
|
+
""" This method is intended to update the table schema
|
639
|
+
in the case of dealing with old database version.
|
640
|
+
"""
|
641
|
+
if self.getVersion() < self.VERSION: # This applies for version 1
|
642
|
+
# Add the extra column for pointer extended attribute in Relations table
|
643
|
+
# from version 1 on, there is not needed since the table will
|
644
|
+
# already contains this column
|
645
|
+
columns = [c[1] for c in self.getTableColumns('Relations')]
|
646
|
+
if 'object_parent_extended' not in columns:
|
647
|
+
self.executeCommand("ALTER TABLE Relations "
|
648
|
+
"ADD COLUMN object_parent_extended TEXT DEFAULT NULL")
|
649
|
+
if 'object_child_extended' not in columns:
|
650
|
+
self.executeCommand("ALTER TABLE Relations "
|
651
|
+
"ADD COLUMN object_child_extended TEXT DEFAULT NULL")
|
652
|
+
self.setVersion(self.VERSION)
|
653
|
+
|
654
|
+
def insertObject(self, name, classname, value, parent_id, label, comment):
|
655
|
+
"""Execute command to insert a new object. Return the inserted object id"""
|
656
|
+
try:
|
657
|
+
self.executeCommand(
|
658
|
+
'INSERT INTO Objects (parent_id, name, classname, value, label, comment, creation)' +
|
659
|
+
' VALUES (?, ?, ?, ?, ?, ?, datetime(\'now\'))',
|
660
|
+
(parent_id, name, classname, value, label, comment))
|
661
|
+
return self.cursor.lastrowid
|
662
|
+
except Exception as ex:
|
663
|
+
logger.error("insertObject: ERROR")
|
664
|
+
logger.error('INSERT INTO Objects (parent_id, name, classname, value, label, comment, creation)' +
|
665
|
+
' VALUES (?, ?, ?, ?, ?, ?, datetime(\'now\'))')
|
666
|
+
logger.error((parent_id, name, classname, value, label, comment))
|
667
|
+
raise ex
|
668
|
+
|
669
|
+
def insertRelation(self, relName, parent_id, object_parent_id, object_child_id,
|
670
|
+
object_parent_extended=None, object_child_extended=None, **kwargs):
|
671
|
+
"""Execute command to insert a new object. Return the inserted object id"""
|
672
|
+
self.executeCommand("INSERT INTO Relations "
|
673
|
+
"(parent_id, name, object_parent_id, object_child_id, creation, "
|
674
|
+
"object_parent_extended, object_child_extended) "
|
675
|
+
" VALUES (?, ?, ?, ?, datetime('now'), ?, ?)",
|
676
|
+
(parent_id, relName, object_parent_id, object_child_id,
|
677
|
+
object_parent_extended, object_child_extended))
|
678
|
+
return self.cursor.lastrowid
|
679
|
+
|
680
|
+
def updateObject(self, objId, name, classname, value, parent_id, label, comment):
|
681
|
+
"""Update object data """
|
682
|
+
self.executeCommand("UPDATE Objects SET parent_id=?, name=?, "
|
683
|
+
"classname=?, value=?, label=?, comment=? WHERE id=?",
|
684
|
+
(parent_id, name, classname, value, label, comment, objId))
|
685
|
+
|
686
|
+
def selectObjectById(self, objId):
|
687
|
+
"""Select an object give its id"""
|
688
|
+
self.executeCommand(self.selectCmd(ID + "=?"), (objId,))
|
689
|
+
return self.cursor.fetchone()
|
690
|
+
|
691
|
+
def doesRowExist(self, objId):
|
692
|
+
"""Return True if a row with a given id exists"""
|
693
|
+
self.executeCommand(self.EXISTS % ID, (objId,))
|
694
|
+
one = self.cursor.fetchone()
|
695
|
+
return one[0] == 1
|
696
|
+
|
697
|
+
def selectAllObjects(self):
|
698
|
+
"""Select all data at once"""
|
699
|
+
self.executeCommand(self.selectCmd(ID + ">0", ' ORDER BY parent_id'))
|
700
|
+
return self.cursor.fetchall()
|
701
|
+
|
702
|
+
def selectObjectsByParent(self, parent_id=None, iterate=False):
|
703
|
+
"""Select object with a given parent
|
704
|
+
if the parent_id is None, all object with parent_id NULL
|
705
|
+
will be returned"""
|
706
|
+
if parent_id is None:
|
707
|
+
self.executeCommand(self.selectCmd(PARENT_ID + " is NULL"))
|
708
|
+
else:
|
709
|
+
self.executeCommand(self.selectCmd(PARENT_ID + "=?"), (parent_id,))
|
710
|
+
return self._results(iterate)
|
711
|
+
|
712
|
+
def selectObjectsByAncestor(self, ancestor_namePrefix, iterate=False):
|
713
|
+
"""Select all objects in the hierarchy of ancestor_id"""
|
714
|
+
self.executeCommand(self.selectCmd("name LIKE '%s.%%'"
|
715
|
+
% ancestor_namePrefix))
|
716
|
+
return self._results(iterate)
|
717
|
+
|
718
|
+
def selectObjectsBy(self, iterate=False, **args):
|
719
|
+
"""More flexible select where the constrains can be passed
|
720
|
+
as a dictionary, the concatenation is done by an AND"""
|
721
|
+
|
722
|
+
if len(args) == 0:
|
723
|
+
whereStr = '1=?'
|
724
|
+
whereTuple = (1,)
|
725
|
+
else:
|
726
|
+
whereList = ['%s=?' % k for k in args.keys()]
|
727
|
+
whereStr = ' AND '.join(whereList)
|
728
|
+
whereTuple = tuple(args.values())
|
729
|
+
|
730
|
+
self.executeCommand(self.selectCmd(whereStr), whereTuple)
|
731
|
+
return self._results(iterate)
|
732
|
+
|
733
|
+
def selectObjectsWhere(self, whereStr, iterate=False):
|
734
|
+
self.executeCommand(self.selectCmd(whereStr))
|
735
|
+
return self._results(iterate)
|
736
|
+
|
737
|
+
def deleteObject(self, objId):
|
738
|
+
"""Delete an existing object"""
|
739
|
+
self.executeCommand(self.DELETE + ID + "=?", (objId,))
|
740
|
+
|
741
|
+
def deleteChildObjects(self, ancestor_namePrefix):
|
742
|
+
""" Delete from db all objects that are childs
|
743
|
+
of an ancestor, now them will have the same starting prefix"""
|
744
|
+
self.executeCommand(self.DELETE + "name LIKE '%s.%%'"
|
745
|
+
% ancestor_namePrefix)
|
746
|
+
|
747
|
+
def selectMissingObjectsByAncestor(self, ancestor_namePrefix,
|
748
|
+
idList):
|
749
|
+
"""Select all objects in the hierarchy of ancestor_id"""
|
750
|
+
idStr = ','.join(str(i) for i in idList)
|
751
|
+
cmd = self.selectCmd("name LIKE '%s.%%' AND id NOT IN (%s) "
|
752
|
+
% (ancestor_namePrefix, idStr))
|
753
|
+
self.executeCommand(cmd)
|
754
|
+
return self._results(iterate=False)
|
755
|
+
|
756
|
+
def deleteMissingObjectsByAncestor(self, ancestor_namePrefix, idList):
|
757
|
+
"""Select all objects in the hierarchy of ancestor_id"""
|
758
|
+
idStr = ','.join(str(i) for i in idList)
|
759
|
+
cmd = "%s name LIKE '%s.%%' AND id NOT IN (%s) " % (self.DELETE, ancestor_namePrefix, idStr)
|
760
|
+
self.executeCommand(cmd)
|
761
|
+
|
762
|
+
def deleteAll(self):
|
763
|
+
""" Delete all objects from the db. """
|
764
|
+
self.executeCommand(self.DELETE + "1")
|
765
|
+
self.executeCommand(self.DELETE_SEQUENCE) # restart the count of ids
|
766
|
+
|
767
|
+
def selectRelationChilds(self, relName, object_parent_id):
|
768
|
+
self.executeCommand(self.SELECT_RELATION % ('child', 'parent'),
|
769
|
+
(relName, object_parent_id))
|
770
|
+
return self._results()
|
771
|
+
|
772
|
+
def selectRelationParents(self, relName, object_child_id):
|
773
|
+
self.executeCommand(self.SELECT_RELATION % ('parent', 'child'),
|
774
|
+
(relName, object_child_id))
|
775
|
+
return self._results()
|
776
|
+
|
777
|
+
def selectRelationsByCreator(self, parent_id):
|
778
|
+
self.executeCommand(self.SELECT_RELATIONS + PARENT_ID + "=?", (parent_id,))
|
779
|
+
return self._results()
|
780
|
+
|
781
|
+
def selectRelationsByName(self, relationName):
|
782
|
+
self.executeCommand(self.SELECT_RELATIONS + "name=?", (relationName,))
|
783
|
+
return self._results()
|
784
|
+
|
785
|
+
def deleteRelationsByCreator(self, parent_id):
|
786
|
+
self.executeCommand("DELETE FROM Relations where parent_id=?", (parent_id,))
|
787
|
+
|
788
|
+
|
789
|
+
class SqliteFlatMapper(Mapper):
|
790
|
+
"""Specific Flat Mapper implementation using Sqlite database"""
|
791
|
+
def __init__(self, dbName, dictClasses=None, tablePrefix='',
|
792
|
+
indexes=None):
|
793
|
+
Mapper.__init__(self, dictClasses)
|
794
|
+
self._objTemplate = None
|
795
|
+
self._attributesToStore = None
|
796
|
+
try:
|
797
|
+
# We (ROB and JMRT) are playing with different
|
798
|
+
# PRAGMAS (see https://www.sqlite.org/pragma.html)
|
799
|
+
# for the SqliteFlatMapper instances
|
800
|
+
# We have been playing with the following
|
801
|
+
# pragmas: {synchronous, journal_mode, temp_store and
|
802
|
+
# cache_size} and inside scipion no improvement
|
803
|
+
# has been observed. Outside scipion a careful
|
804
|
+
# choosing of pragmas may duplicate the speed but
|
805
|
+
# inside scipion, I think that the overhead due
|
806
|
+
# to the manipulation of python classes is more
|
807
|
+
# important that the data access.
|
808
|
+
|
809
|
+
# uncommenting these pragmas increase the speed
|
810
|
+
# by a factor of two if NO python object is manipulated.
|
811
|
+
# Unfortunately, any interesting operation involve
|
812
|
+
# creation and manipulation of python objects that take
|
813
|
+
# longer than the access time to the database
|
814
|
+
pragmas = {
|
815
|
+
# 0 | OFF | 1 | NORMAL | 2 | FULL | 3 | EXTRA;
|
816
|
+
# #'synchronous': 'OFF', # ON
|
817
|
+
# DELETE | TRUNCATE | PERSIST | MEMORY | WAL | OFF
|
818
|
+
# #'journal_mode': 'OFF', # DELETE
|
819
|
+
# FILE 0 | DEFAULT | 1 | FILE | 2 | MEMORY;
|
820
|
+
# #'temp_store': 'MEMORY',
|
821
|
+
# PRAGMA schema.cache_size = pages;
|
822
|
+
# #'cache_size': '5000' # versus -2000
|
823
|
+
# "temp_store_directory": "'.'",
|
824
|
+
}
|
825
|
+
self.db = SqliteFlatDb(dbName, tablePrefix,
|
826
|
+
pragmas=pragmas, indexes=indexes)
|
827
|
+
self.doCreateTables = self.db.missingTables()
|
828
|
+
|
829
|
+
if not self.doCreateTables:
|
830
|
+
self.__loadObjDict()
|
831
|
+
except Exception as ex:
|
832
|
+
raise SqliteFlatMapperException('Error creating SqliteFlatMapper, '
|
833
|
+
'dbName: %s, tablePrefix: %s\n error: %s' %
|
834
|
+
(dbName, tablePrefix, ex))
|
835
|
+
|
836
|
+
def commit(self):
|
837
|
+
self.db.commit()
|
838
|
+
|
839
|
+
def close(self):
|
840
|
+
self.db.close()
|
841
|
+
|
842
|
+
def insert(self, obj):
|
843
|
+
if self.doCreateTables:
|
844
|
+
self.db.createTables(obj.getObjDict(includeClass=True))
|
845
|
+
self.doCreateTables = False
|
846
|
+
"""Insert a new object into the system, the id will be set"""
|
847
|
+
self.db.insertObject(obj.getObjId(), obj.isEnabled(), obj.getObjLabel(), obj.getObjComment(),
|
848
|
+
*self._getValuesFromObject(obj).values())
|
849
|
+
|
850
|
+
def getAttributes2Store(self, item):
|
851
|
+
|
852
|
+
if self._attributesToStore is None:
|
853
|
+
self._attributesToStore = [key for key, value in item.getAttributesToStore()]
|
854
|
+
|
855
|
+
return self._attributesToStore
|
856
|
+
|
857
|
+
def _getValuesFromObject(self, item):
|
858
|
+
|
859
|
+
valuesDict = OrderedDict()
|
860
|
+
|
861
|
+
for attr in self.getAttributes2Store(item):
|
862
|
+
item.fillObjDict('', valuesDict, False, attr, getattr(item, attr))
|
863
|
+
|
864
|
+
return valuesDict
|
865
|
+
|
866
|
+
def enableAppend(self):
|
867
|
+
""" This will allow to append items to existing db.
|
868
|
+
This is by default not allow, since most sets are not
|
869
|
+
modified after creation.
|
870
|
+
"""
|
871
|
+
if not self.doCreateTables:
|
872
|
+
obj = self.selectFirst()
|
873
|
+
if obj is not None:
|
874
|
+
self.db.setupCommands(obj.getObjDict(includeClass=True))
|
875
|
+
|
876
|
+
def clear(self):
|
877
|
+
self.db.clear()
|
878
|
+
self.doCreateTables = True
|
879
|
+
|
880
|
+
def deleteAll(self):
|
881
|
+
""" Delete all objects stored """
|
882
|
+
self.db.deleteAll()
|
883
|
+
|
884
|
+
def delete(self, obj):
|
885
|
+
"""Delete an object and all its children"""
|
886
|
+
self.db.deleteObject(obj.getObjId())
|
887
|
+
|
888
|
+
def updateTo(self, obj, level=1):
|
889
|
+
""" Update database entry with new object values. """
|
890
|
+
if self.db.INSERT_OBJECT is None:
|
891
|
+
self.db.setupCommands(obj.getObjDict(includeClass=True))
|
892
|
+
args = list(obj.getObjDict().values())
|
893
|
+
args.append(obj.getObjId())
|
894
|
+
self.db.updateObject(obj.isEnabled(), obj.getObjLabel(), obj.getObjComment(), *args)
|
895
|
+
|
896
|
+
def exists(self, objId):
|
897
|
+
return self.db.doesRowExist(objId)
|
898
|
+
|
899
|
+
def selectById(self, objId):
|
900
|
+
"""Build the object which id is objId"""
|
901
|
+
objRow = self.db.selectObjectById(objId)
|
902
|
+
if objRow is None:
|
903
|
+
obj = None
|
904
|
+
else:
|
905
|
+
obj = self.__objFromRow(objRow)
|
906
|
+
return obj
|
907
|
+
|
908
|
+
def __loadObjDict(self):
|
909
|
+
""" Load object properties and classes from db.
|
910
|
+
Stores the _objTemplate for future reuse"""
|
911
|
+
# Create a template object for retrieving stored ones
|
912
|
+
columnList = []
|
913
|
+
rows = self.db.getClassRows()
|
914
|
+
|
915
|
+
# Adds common fields to the mapping
|
916
|
+
# Schema definition in classes table
|
917
|
+
self.db.addCommonFieldsToMap()
|
918
|
+
|
919
|
+
attrClasses = {}
|
920
|
+
self._objBuildList = []
|
921
|
+
|
922
|
+
# For each row lin classes table (_samplinRate --> c01, Integer)
|
923
|
+
for r in rows:
|
924
|
+
|
925
|
+
# Something like: _acquisition._doseInitial
|
926
|
+
label = r['label_property']
|
927
|
+
|
928
|
+
# If the actual item class: "Particle" in a SetOfParticles
|
929
|
+
# First loop.
|
930
|
+
if label == SELF:
|
931
|
+
objClassName = r['class_name']
|
932
|
+
|
933
|
+
# Store the template to reuse it during iterations and avoid instantiation
|
934
|
+
self._objTemplate = self._buildObjectFromClass(objClassName)
|
935
|
+
self._objClass = self._objTemplate.__class__
|
936
|
+
else:
|
937
|
+
# Update the database column mapping: c01 <-> _samplingRate
|
938
|
+
self.db._columnsMapping[label] = r['column_name']
|
939
|
+
|
940
|
+
# List for the latter _objColumns [(5,"_smplingRate"), ...]
|
941
|
+
# This latter will be used to take the value from the cursor's row using the index (row[5] => obj._samplingRate)
|
942
|
+
columnList.append(label)
|
943
|
+
|
944
|
+
# Annotate the class
|
945
|
+
attrClasses[label] = r['class_name']
|
946
|
+
|
947
|
+
# Split the label: _acquisition._doseInitial -> ["_acquisition", "_doseInitial"]
|
948
|
+
# For the loop
|
949
|
+
attrParts = label.split('.')
|
950
|
+
|
951
|
+
# Like a breadcrumb in websites... partial path to the attribute
|
952
|
+
attrJoin = ''
|
953
|
+
|
954
|
+
# Start from the template (root)
|
955
|
+
o = self._objTemplate
|
956
|
+
|
957
|
+
# for each part: ["_acquisition", "_doseInitial"]
|
958
|
+
for a in attrParts:
|
959
|
+
attrJoin += a
|
960
|
+
|
961
|
+
# Try to get the attribute. Case of attributes defined in the model (init)
|
962
|
+
attr = getattr(o, a, None)
|
963
|
+
|
964
|
+
# If the object does not have the attribute, then it might be an extra parameter or an optional like Transform.p+ç1
|
965
|
+
if attr is None:
|
966
|
+
className = attrClasses[attrJoin]
|
967
|
+
|
968
|
+
# Instantiate the class.
|
969
|
+
attr = self._buildObjectFromClass(className)
|
970
|
+
|
971
|
+
# Get the class form the attr: it could come as a LegacyClass in case "className" is not found.
|
972
|
+
self._objBuildList.append((attr.__class__, attrJoin.split('.')))
|
973
|
+
setattr(o, a, attr)
|
974
|
+
o = attr
|
975
|
+
attrJoin += '.'
|
976
|
+
basicRows = 5
|
977
|
+
n = len(rows) + basicRows - 1
|
978
|
+
self._objColumns = list(zip(range(basicRows, n), columnList))
|
979
|
+
|
980
|
+
def __buildAndFillObj(self):
|
981
|
+
""" Instantiates the set item base on the _objBuildList.
|
982
|
+
_objBuildList has been populated when loading the classDictionary"""
|
983
|
+
|
984
|
+
obj = self._objClass()
|
985
|
+
|
986
|
+
for clazz, attrParts in self._objBuildList:
|
987
|
+
o = obj
|
988
|
+
for a in attrParts:
|
989
|
+
attr = getattr(o, a, None)
|
990
|
+
if not attr:
|
991
|
+
setattr(o, a, clazz())
|
992
|
+
break
|
993
|
+
o = attr
|
994
|
+
return obj
|
995
|
+
|
996
|
+
def getInstance(self):
|
997
|
+
|
998
|
+
if self._objTemplate is None:
|
999
|
+
self.__loadObjDict()
|
1000
|
+
|
1001
|
+
# Difference in performance using scipion3 tests pwperformance.tests.test_set_performance.TestSetPerformanceSteps.testSuperExtendedCoordinatesSet
|
1002
|
+
# A test that creates a 10**6 set of extended coordinates (coordinates with 20 extra attributes)
|
1003
|
+
# With the template iteration takes 10 secs
|
1004
|
+
# Building the object each time take 25 secs (15 seconds more)
|
1005
|
+
if Config.SCIPION_MAPPER_USE_TEMPLATE:
|
1006
|
+
return self._objTemplate
|
1007
|
+
else:
|
1008
|
+
return self.__buildAndFillObj()
|
1009
|
+
|
1010
|
+
def __objFromRow(self, objRow):
|
1011
|
+
|
1012
|
+
|
1013
|
+
obj = self.getInstance()
|
1014
|
+
obj.setObjId(objRow[ID])
|
1015
|
+
obj.setObjLabel(self._getStrValue(objRow['label']))
|
1016
|
+
obj.setObjComment(self._getStrValue(objRow['comment']))
|
1017
|
+
|
1018
|
+
try:
|
1019
|
+
obj.setEnabled(objRow['enabled'])
|
1020
|
+
obj.setObjCreation(self._getStrValue(objRow[CREATION]))
|
1021
|
+
except Exception:
|
1022
|
+
# THIS SHOULD NOT HAPPEN
|
1023
|
+
logger.warning("'creation' column not found in object: %s" % obj.getObjId())
|
1024
|
+
logger.warning(" db: %s" % self.db.getDbName())
|
1025
|
+
logger.warning(" objRow: %s." % dict(objRow))
|
1026
|
+
|
1027
|
+
for c, attrName in self._objColumns:
|
1028
|
+
obj.setAttributeValue(attrName, objRow[c])
|
1029
|
+
|
1030
|
+
return obj
|
1031
|
+
|
1032
|
+
def __iterObjectsFromRows(self, objRows, objectFilter=None, rowFilter=None):
|
1033
|
+
for objRow in objRows:
|
1034
|
+
if rowFilter and not rowFilter(objRow):
|
1035
|
+
continue
|
1036
|
+
|
1037
|
+
obj = self.__objFromRow(objRow)
|
1038
|
+
if objectFilter is None or objectFilter(obj):
|
1039
|
+
yield obj
|
1040
|
+
|
1041
|
+
def __objectsFromRows(self, objRows, iterate=False, objectFilter=None, rowFilter=None):
|
1042
|
+
"""Create a set of object from a set of rows
|
1043
|
+
Params:
|
1044
|
+
objRows: rows result from a db select.
|
1045
|
+
iterate: if True, iterates over all elements, if False the whole
|
1046
|
+
list is returned
|
1047
|
+
objectFilter: function to filter some of the objects of the results.
|
1048
|
+
"""
|
1049
|
+
if not iterate:
|
1050
|
+
return [obj.clone()
|
1051
|
+
for obj in self.__iterObjectsFromRows(objRows, objectFilter, rowFilter)]
|
1052
|
+
else:
|
1053
|
+
return self.__iterObjectsFromRows(objRows, objectFilter, rowFilter)
|
1054
|
+
|
1055
|
+
def selectBy(self, iterate=False, objectFilter=None, **args):
|
1056
|
+
"""Select object meetings some criteria"""
|
1057
|
+
objRows = self.db.selectObjectsBy(**args)
|
1058
|
+
return self.__objectsFromRows(objRows, iterate, objectFilter)
|
1059
|
+
|
1060
|
+
def selectAll(self, iterate=True, objectFilter=None, orderBy=ID,
|
1061
|
+
direction='ASC', where='1', limit=None, rowFilter=None):
|
1062
|
+
# Just a sanity check for emtpy sets, that doesn't contains
|
1063
|
+
# 'Properties' table
|
1064
|
+
if not self.db.hasTable('Properties'):
|
1065
|
+
return iter([]) if iterate else []
|
1066
|
+
|
1067
|
+
# Initialize the instance
|
1068
|
+
self.getInstance()
|
1069
|
+
|
1070
|
+
try:
|
1071
|
+
objRows = self.db.selectAll(orderBy=orderBy,
|
1072
|
+
direction=direction,
|
1073
|
+
where=where,
|
1074
|
+
limit=limit)
|
1075
|
+
except OperationalError as e:
|
1076
|
+
msg="""Error executing selectAll command: %s.
|
1077
|
+
You may want to change the directory used by sqlite to create temporary files
|
1078
|
+
to one that has enough free space. By default this directory is /tmp
|
1079
|
+
You may achieve this goal by defining the SQLITE_TMPDIR environment variable
|
1080
|
+
and restarting scipion. Export command:
|
1081
|
+
export SQLITE_TMPDIR=. """ % str(e)
|
1082
|
+
raise OperationalError(msg)
|
1083
|
+
|
1084
|
+
return self.__objectsFromRows(objRows, iterate, objectFilter, rowFilter)
|
1085
|
+
|
1086
|
+
def unique(self, labels, where=None):
|
1087
|
+
""" Returns a list (for a single label) or a dictionary with unique values for the passed labels.
|
1088
|
+
If more than one label is passed it will be unique rows similar ti SQL unique clause.
|
1089
|
+
|
1090
|
+
:param labels (string or list) item attribute/s to retrieve unique row values
|
1091
|
+
:param where (string) condition to filter the results"""
|
1092
|
+
|
1093
|
+
if isinstance(labels, str):
|
1094
|
+
labels = [labels]
|
1095
|
+
|
1096
|
+
rows = self.db.unique(labels, where)
|
1097
|
+
result = {label: [] for label in labels} # Initialize the results dictionary
|
1098
|
+
for row in rows:
|
1099
|
+
for label in labels:
|
1100
|
+
result[label].append(row[label])
|
1101
|
+
|
1102
|
+
# If there is only one label,
|
1103
|
+
if len(labels) == 1:
|
1104
|
+
return result[labels[0]]
|
1105
|
+
else:
|
1106
|
+
return result
|
1107
|
+
|
1108
|
+
def aggregate(self, operations, operationLabel, groupByLabels=None):
|
1109
|
+
|
1110
|
+
operations = valueToList(operations)
|
1111
|
+
groupByLabels = valueToList(groupByLabels)
|
1112
|
+
|
1113
|
+
rows = self.db.aggregate(operations, operationLabel, groupByLabels)
|
1114
|
+
|
1115
|
+
# Transform the sql row into a disconnected list of dictionaries
|
1116
|
+
results = []
|
1117
|
+
for row in rows:
|
1118
|
+
values = {}
|
1119
|
+
for key in row.keys():
|
1120
|
+
values[key] = row[key]
|
1121
|
+
results.append(values)
|
1122
|
+
|
1123
|
+
return results
|
1124
|
+
|
1125
|
+
def count(self):
|
1126
|
+
return 0 if self.doCreateTables else self.db.count()
|
1127
|
+
|
1128
|
+
def maxId(self):
|
1129
|
+
return 0 if self.doCreateTables else self.db.maxId()
|
1130
|
+
|
1131
|
+
def __objectsFromIds(self, objIds):
|
1132
|
+
"""Return a list of objects, given a list of id's
|
1133
|
+
"""
|
1134
|
+
return [self.selectById(rowId[ID]) for rowId in objIds]
|
1135
|
+
|
1136
|
+
def hasProperty(self, key):
|
1137
|
+
return self.db.hasProperty(key)
|
1138
|
+
|
1139
|
+
def getProperty(self, key, defaultValue=None):
|
1140
|
+
return self.db.getProperty(key, defaultValue)
|
1141
|
+
|
1142
|
+
def setProperty(self, key, value):
|
1143
|
+
return self.db.setProperty(key, value)
|
1144
|
+
|
1145
|
+
def deleteProperty(self, key):
|
1146
|
+
return self.db.deleteProperty(key)
|
1147
|
+
|
1148
|
+
def getPropertyKeys(self):
|
1149
|
+
return self.db.getPropertyKeys()
|
1150
|
+
|
1151
|
+
@staticmethod
|
1152
|
+
def fmtDate(date):
|
1153
|
+
""" Formats a python date into a valid string to be used in a where term
|
1154
|
+
Currently creation files is stored in utc time and is has no microseconds.
|
1155
|
+
|
1156
|
+
:param date: python date un utc. use datetime.datetime.utcnow() instead of now()"""
|
1157
|
+
return "datetime('%s')" % date.replace(microsecond=0)
|
1158
|
+
|
1159
|
+
class SqliteFlatMapperException(Exception):
|
1160
|
+
pass
|
1161
|
+
|
1162
|
+
|
1163
|
+
SELF = 'self'
|
1164
|
+
|
1165
|
+
|
1166
|
+
class SqliteFlatDb(SqliteDb):
|
1167
|
+
"""Class to handle a Sqlite database.
|
1168
|
+
It will create connection, execute queries and commands"""
|
1169
|
+
# Maintain the current version of the DB schema
|
1170
|
+
# useful for future updates and backward compatibility
|
1171
|
+
# version should be an integer number
|
1172
|
+
VERSION = 1
|
1173
|
+
|
1174
|
+
CLASS_MAP = {'Integer': 'INTEGER',
|
1175
|
+
'Float': 'REAL',
|
1176
|
+
'Boolean': 'INTEGER'
|
1177
|
+
}
|
1178
|
+
|
1179
|
+
def __init__(self, dbName, tablePrefix='', timeout=1000,
|
1180
|
+
pragmas=None, indexes=None):
|
1181
|
+
SqliteDb.__init__(self)
|
1182
|
+
self._pragmas = pragmas or {}
|
1183
|
+
self._indexes = indexes or []
|
1184
|
+
tablePrefix = tablePrefix.strip()
|
1185
|
+
# Avoid having _ for empty prefix
|
1186
|
+
if tablePrefix and not tablePrefix.endswith('_'):
|
1187
|
+
tablePrefix += '_'
|
1188
|
+
# NOTE (Jose Miguel, 2014/01/02
|
1189
|
+
# Reusing connections is a bit dangerous, since it have lead to
|
1190
|
+
# unexpected and hard to trace errors due to using an out-of-date
|
1191
|
+
# reused connection. That's why we are changing now the default to False
|
1192
|
+
# and only setting to True when the tablePrefix is non-empty, which is
|
1193
|
+
# the case for classes that are different tables in the same db and it
|
1194
|
+
# logical to reuse the connection.
|
1195
|
+
self._reuseConnections = bool(tablePrefix)
|
1196
|
+
|
1197
|
+
self.CHECK_TABLES = ("SELECT name FROM sqlite_master WHERE type='table'"
|
1198
|
+
" AND name='%sObjects';" % tablePrefix)
|
1199
|
+
self.SELECT = "SELECT * FROM %sObjects" % tablePrefix
|
1200
|
+
self.FROM = "FROM %sObjects" % tablePrefix
|
1201
|
+
self.DELETE = "DELETE FROM %sObjects WHERE " % tablePrefix
|
1202
|
+
self.INSERT_CLASS = ("INSERT INTO %sClasses (label_property, "
|
1203
|
+
"column_name, class_name) VALUES (?, ?, ?)"
|
1204
|
+
% tablePrefix)
|
1205
|
+
self.SELECT_CLASS = "SELECT * FROM %sClasses;" % tablePrefix
|
1206
|
+
self.EXISTS = "SELECT EXISTS(SELECT 1 FROM Objects WHERE %s=? LIMIT 1)"
|
1207
|
+
self.tablePrefix = tablePrefix
|
1208
|
+
self._createConnection(dbName, timeout)
|
1209
|
+
self.INSERT_OBJECT = None
|
1210
|
+
self.UPDATE_OBJECT = None
|
1211
|
+
self._columnsMapping = {}
|
1212
|
+
|
1213
|
+
self.INSERT_PROPERTY = "INSERT INTO Properties (key, value) VALUES (?, ?)"
|
1214
|
+
self.DELETE_PROPERTY = "DELETE FROM Properties WHERE key=?"
|
1215
|
+
self.UPDATE_PROPERTY = "UPDATE Properties SET value=? WHERE key=?"
|
1216
|
+
self.SELECT_PROPERTY = "SELECT value FROM Properties WHERE key=?"
|
1217
|
+
self.SELECT_PROPERTY_KEYS = "SELECT key FROM Properties"
|
1218
|
+
|
1219
|
+
def hasProperty(self, key):
|
1220
|
+
""" Return true if a property with this value is registered. """
|
1221
|
+
# The database not will not have the 'Properties' table when
|
1222
|
+
# there is not item inserted (ie an empty set)
|
1223
|
+
if not self.hasTable('Properties'):
|
1224
|
+
return False
|
1225
|
+
self.executeCommand(self.SELECT_PROPERTY, (key,))
|
1226
|
+
result = self.cursor.fetchone()
|
1227
|
+
return result is not None
|
1228
|
+
|
1229
|
+
def getProperty(self, key, defaultValue=None):
|
1230
|
+
""" Return the value of a given property with this key.
|
1231
|
+
If not found, the defaultValue will be returned.
|
1232
|
+
"""
|
1233
|
+
# The database not will not have the 'Properties' table when
|
1234
|
+
# there is not item inserted (ie an empty set)
|
1235
|
+
if not self.hasTable('Properties'):
|
1236
|
+
return defaultValue
|
1237
|
+
|
1238
|
+
self.executeCommand(self.SELECT_PROPERTY, (key,))
|
1239
|
+
result = self.cursor.fetchone()
|
1240
|
+
|
1241
|
+
if result:
|
1242
|
+
return result['value']
|
1243
|
+
else:
|
1244
|
+
return defaultValue
|
1245
|
+
|
1246
|
+
def setProperty(self, key, value):
|
1247
|
+
""" Insert or update the property with a value. """
|
1248
|
+
# Just ignore the set property for empty sets
|
1249
|
+
if not self.hasTable('Properties'):
|
1250
|
+
return
|
1251
|
+
|
1252
|
+
# All properties are stored as string, except for None type
|
1253
|
+
value = str(value) if value is not None else None
|
1254
|
+
|
1255
|
+
if self.hasProperty(key):
|
1256
|
+
self.executeCommand(self.UPDATE_PROPERTY, (value, key))
|
1257
|
+
else:
|
1258
|
+
self.executeCommand(self.INSERT_PROPERTY, (key, value))
|
1259
|
+
|
1260
|
+
def getPropertyKeys(self):
|
1261
|
+
""" Return all properties stored of this object. """
|
1262
|
+
self.executeCommand(self.SELECT_PROPERTY_KEYS)
|
1263
|
+
keys = [r[0] for r in self.cursor.fetchall()]
|
1264
|
+
return keys
|
1265
|
+
|
1266
|
+
def deleteProperty(self, key):
|
1267
|
+
self.executeCommand(self.DELETE_PROPERTY, (key,))
|
1268
|
+
|
1269
|
+
def selectCmd(self, whereStr, orderByStr=' ORDER BY ' + ID):
|
1270
|
+
|
1271
|
+
whereStr = "" if whereStr is None else " WHERE " + whereStr
|
1272
|
+
return self.SELECT + whereStr + orderByStr
|
1273
|
+
|
1274
|
+
def missingTables(self):
|
1275
|
+
""" Return True is the needed Objects and Classes table are not
|
1276
|
+
created yet. """
|
1277
|
+
self.executeCommand(self.CHECK_TABLES)
|
1278
|
+
result = self.cursor.fetchone()
|
1279
|
+
|
1280
|
+
return result is None
|
1281
|
+
|
1282
|
+
def clear(self):
|
1283
|
+
self.executeCommand("DROP TABLE IF EXISTS Properties;")
|
1284
|
+
self.executeCommand("DROP TABLE IF EXISTS %sClasses;"
|
1285
|
+
% self.tablePrefix)
|
1286
|
+
self.executeCommand("DROP TABLE IF EXISTS %sObjects;"
|
1287
|
+
% self.tablePrefix)
|
1288
|
+
|
1289
|
+
def createTables(self, objDict):
|
1290
|
+
"""Create the Classes and Object table to store items of a Set.
|
1291
|
+
Each object will be stored in a single row.
|
1292
|
+
Each nested property of the object will be stored as a column value.
|
1293
|
+
"""
|
1294
|
+
self.setVersion(self.VERSION)
|
1295
|
+
for pragma in self._pragmas.items():
|
1296
|
+
logger.debug("Executing pragma: %s" % pragma)
|
1297
|
+
self.executeCommand("PRAGMA %s = %s;" % pragma)
|
1298
|
+
# Create a general Properties table to store some needed values
|
1299
|
+
self.executeCommand("""CREATE TABLE IF NOT EXISTS Properties
|
1300
|
+
(key TEXT UNIQUE, -- property key
|
1301
|
+
value TEXT DEFAULT NULL -- property value
|
1302
|
+
)""")
|
1303
|
+
# Create the Classes table to store each column name and type
|
1304
|
+
self.executeCommand("""CREATE TABLE IF NOT EXISTS %sClasses
|
1305
|
+
(id INTEGER PRIMARY KEY AUTOINCREMENT,
|
1306
|
+
label_property TEXT UNIQUE, --object label
|
1307
|
+
column_name TEXT UNIQUE,
|
1308
|
+
class_name TEXT DEFAULT NULL -- relation's class name
|
1309
|
+
)""" % self.tablePrefix)
|
1310
|
+
CREATE_OBJECT_TABLE = """CREATE TABLE IF NOT EXISTS %sObjects
|
1311
|
+
(id INTEGER PRIMARY KEY,
|
1312
|
+
enabled INTEGER DEFAULT 1, -- used to selected/deselect items from a set
|
1313
|
+
label TEXT DEFAULT NULL, -- object label, text used for display
|
1314
|
+
comment TEXT DEFAULT NULL, -- object comment, text used for annotations
|
1315
|
+
creation DATE -- creation date and time of the object
|
1316
|
+
""" % self.tablePrefix
|
1317
|
+
|
1318
|
+
c = 0
|
1319
|
+
colMap = {}
|
1320
|
+
for k, v in objDict.items():
|
1321
|
+
colName = 'c%02d' % c
|
1322
|
+
className = v[0]
|
1323
|
+
colMap[k] = colName
|
1324
|
+
c += 1
|
1325
|
+
self.executeCommand(self.INSERT_CLASS, (k, colName, className))
|
1326
|
+
if k != SELF:
|
1327
|
+
CREATE_OBJECT_TABLE += ',%s %s DEFAULT NULL' % (colName, self.CLASS_MAP.get(className, 'TEXT'))
|
1328
|
+
|
1329
|
+
CREATE_OBJECT_TABLE += ')'
|
1330
|
+
# Create the Objects table
|
1331
|
+
self.executeCommand(CREATE_OBJECT_TABLE)
|
1332
|
+
|
1333
|
+
for idx in self._indexes:
|
1334
|
+
# first check if the attribute to be indexed exists
|
1335
|
+
if idx in colMap:
|
1336
|
+
self.executeCommand("CREATE INDEX index_%s ON Objects (%s);"
|
1337
|
+
% (idx.replace('.', '_'), colMap[idx]))
|
1338
|
+
|
1339
|
+
self.commit()
|
1340
|
+
# Prepare the INSERT and UPDATE commands
|
1341
|
+
self.setupCommands(objDict)
|
1342
|
+
|
1343
|
+
def setupCommands(self, objDict):
|
1344
|
+
""" Setup the INSERT and UPDATE commands base on the object dictionary. """
|
1345
|
+
self.INSERT_OBJECT = "INSERT INTO %sObjects (id, enabled, label, comment, creation" % self.tablePrefix
|
1346
|
+
self.UPDATE_OBJECT = "UPDATE %sObjects SET enabled=?, label=?, comment=?" % self.tablePrefix
|
1347
|
+
c = 0
|
1348
|
+
for k in objDict:
|
1349
|
+
colName = 'c%02d' % c
|
1350
|
+
self._columnsMapping[k] = colName
|
1351
|
+
c += 1
|
1352
|
+
if k != SELF:
|
1353
|
+
self.INSERT_OBJECT += ',%s' % colName
|
1354
|
+
self.UPDATE_OBJECT += ', %s=?' % colName
|
1355
|
+
|
1356
|
+
self.addCommonFieldsToMap()
|
1357
|
+
|
1358
|
+
self.INSERT_OBJECT += ") VALUES (?,?,?,?, datetime('now')" + ',?' * (c-1) + ')'
|
1359
|
+
self.UPDATE_OBJECT += ' WHERE id=?'
|
1360
|
+
|
1361
|
+
def addCommonFieldsToMap(self):
|
1362
|
+
|
1363
|
+
# Add common fields to the mapping
|
1364
|
+
self._columnsMapping[ID_COLUMN] = ID_COLUMN
|
1365
|
+
self._columnsMapping[ID_ATTRIBUTE] = ID_COLUMN
|
1366
|
+
|
1367
|
+
def getClassRows(self):
|
1368
|
+
""" Create a dictionary with names of the attributes
|
1369
|
+
of the columns. """
|
1370
|
+
self.executeCommand(self.SELECT_CLASS)
|
1371
|
+
return self._results(iterate=False)
|
1372
|
+
|
1373
|
+
def getSelfClassName(self):
|
1374
|
+
""" Return the class name of the attribute named 'self'.
|
1375
|
+
This is the class of the items stored in a Set.
|
1376
|
+
"""
|
1377
|
+
self.executeCommand(self.SELECT_CLASS)
|
1378
|
+
|
1379
|
+
for classRow in self._iterResults():
|
1380
|
+
if classRow['label_property'] == SELF:
|
1381
|
+
return classRow['class_name']
|
1382
|
+
raise Exception("Row '%s' was not found in Classes table. " % SELF)
|
1383
|
+
|
1384
|
+
def insertObject(self, *args):
|
1385
|
+
"""Insert a new object as a row.
|
1386
|
+
*args: id, label, comment, ...
|
1387
|
+
where ... is the values of the objDict from which the tables
|
1388
|
+
where created."""
|
1389
|
+
self.executeCommand(self.INSERT_OBJECT, args)
|
1390
|
+
|
1391
|
+
def updateObject(self, *args):
|
1392
|
+
"""Update object data """
|
1393
|
+
self.executeCommand(self.UPDATE_OBJECT, args)
|
1394
|
+
|
1395
|
+
def selectObjectById(self, objId):
|
1396
|
+
"""Select an object give its id"""
|
1397
|
+
self.executeCommand(self.selectCmd(ID + "=?"), (objId,))
|
1398
|
+
return self.cursor.fetchone()
|
1399
|
+
|
1400
|
+
def doesRowExist(self, objId):
|
1401
|
+
"""Return True if a row with a given id exists"""
|
1402
|
+
self.executeCommand(self.EXISTS % ID, (objId,))
|
1403
|
+
one = self.cursor.fetchone()
|
1404
|
+
return one[0] == 1
|
1405
|
+
|
1406
|
+
def _getRealCol(self, colName):
|
1407
|
+
""" Transform the column name taking into account
|
1408
|
+
special columns such as: id or RANDOM(), and
|
1409
|
+
getting the mapping translation otherwise.
|
1410
|
+
"""
|
1411
|
+
if colName in [ID, 'RANDOM()', CREATION]:
|
1412
|
+
return colName
|
1413
|
+
elif colName in self._columnsMapping:
|
1414
|
+
return self._columnsMapping[colName]
|
1415
|
+
else:
|
1416
|
+
return None
|
1417
|
+
def selectAll(self, iterate=True, orderBy=ID, direction='ASC',
|
1418
|
+
where=None, limit=None):
|
1419
|
+
# Handle the specials orderBy values of 'id' and 'RANDOM()'
|
1420
|
+
# other columns names should be mapped to table column
|
1421
|
+
# such as: _micId -> c04
|
1422
|
+
|
1423
|
+
if isinstance(orderBy, str):
|
1424
|
+
orderByCol = self._getRealCol(orderBy)
|
1425
|
+
elif isinstance(orderBy, list):
|
1426
|
+
orderByCol = ','.join([self._getRealCol(c) for c in orderBy])
|
1427
|
+
else:
|
1428
|
+
raise Exception('Invalid type for orderBy: %s' % type(orderBy))
|
1429
|
+
|
1430
|
+
whereStr = self._whereToWhereStr(where)
|
1431
|
+
|
1432
|
+
cmd = self.selectCmd(whereStr,
|
1433
|
+
orderByStr=' ORDER BY %s %s'
|
1434
|
+
% (orderByCol, direction))
|
1435
|
+
# If there is a limit
|
1436
|
+
if limit:
|
1437
|
+
# if it is a tuple
|
1438
|
+
if isinstance(limit, tuple):
|
1439
|
+
limit, skipRows = limit # Extract values from tuple
|
1440
|
+
else:
|
1441
|
+
skipRows = None
|
1442
|
+
# If we need to skip rows
|
1443
|
+
skipPart = "%s," % skipRows if skipRows else ""
|
1444
|
+
cmd += " LIMIT %s %s" % (skipPart, limit)
|
1445
|
+
|
1446
|
+
self.executeCommand(cmd)
|
1447
|
+
return self._results(iterate)
|
1448
|
+
|
1449
|
+
def _whereToWhereStr(self, where):
|
1450
|
+
""" Parse the where string to replace the column name with
|
1451
|
+
the real table column name ( for example: _micId -> c01 )
|
1452
|
+
Right now we are assuming a simple where string in the form
|
1453
|
+
colName=VALUE
|
1454
|
+
|
1455
|
+
:param where string with pair of terms separated by "=" where left
|
1456
|
+
element is an attribute of the item
|
1457
|
+
|
1458
|
+
>>> Example:
|
1459
|
+
>>> _micId=3 OR _micId=4
|
1460
|
+
"""
|
1461
|
+
if where is None:
|
1462
|
+
return
|
1463
|
+
|
1464
|
+
whereStr = where
|
1465
|
+
# Split by valid where operators: =, <, >
|
1466
|
+
result = re.split('<=|>=|=|<|>|AND|OR', where)
|
1467
|
+
# For each item
|
1468
|
+
for term in result:
|
1469
|
+
# trim it
|
1470
|
+
term = term.strip()
|
1471
|
+
whereRealCol = self._getRealCol(term)
|
1472
|
+
if whereRealCol is not None:
|
1473
|
+
whereStr = whereStr.replace(term, whereRealCol)
|
1474
|
+
|
1475
|
+
return whereStr
|
1476
|
+
|
1477
|
+
def unique(self, labels, where=None):
|
1478
|
+
""" Returns the results of the execution of a UNIQUE query
|
1479
|
+
|
1480
|
+
:param labels: list of attributes which you want unique values from
|
1481
|
+
:param where: condition to match in the form: attrName=value
|
1482
|
+
:return:
|
1483
|
+
"""
|
1484
|
+
# let us count for testing
|
1485
|
+
selectStr = 'SELECT DISTINCT '
|
1486
|
+
separator = ' '
|
1487
|
+
# This cannot be like the following line should be expressed in terms
|
1488
|
+
# of c01, c02 etc (actual fields)....
|
1489
|
+
for label in labels:
|
1490
|
+
selectStr += "%s %s AS %s " % (separator, self._getRealCol(label), label)
|
1491
|
+
separator = ', '
|
1492
|
+
|
1493
|
+
sqlCommand = selectStr + self.FROM
|
1494
|
+
|
1495
|
+
whereStr = self._whereToWhereStr(where)
|
1496
|
+
if whereStr is not None:
|
1497
|
+
sqlCommand += " WHERE " + whereStr
|
1498
|
+
|
1499
|
+
self.executeCommand(sqlCommand)
|
1500
|
+
return self._results(iterate=False)
|
1501
|
+
|
1502
|
+
def aggregate(self, operations, operationLabel, groupByLabels=None):
|
1503
|
+
"""
|
1504
|
+
|
1505
|
+
:param operations: string or LIST of operations: MIN, MAX, AVG, COUNT, SUM, TOTAL, GROUP_CONCAT. Any single argument function
|
1506
|
+
defined for sqlite at https://www.sqlite.org/lang_aggfunc.html
|
1507
|
+
:param operationLabel: string or LIST of attributes to apply the functions on
|
1508
|
+
:param groupByLabels: (Optional) attribute or list of attributes to group by the data
|
1509
|
+
:return:
|
1510
|
+
"""
|
1511
|
+
# let us count for testing
|
1512
|
+
selectStr = 'SELECT '
|
1513
|
+
separator = ' '
|
1514
|
+
|
1515
|
+
operations = valueToList(operations)
|
1516
|
+
operationLabel = valueToList(operationLabel)
|
1517
|
+
groupByLabels = valueToList(groupByLabels)
|
1518
|
+
|
1519
|
+
# This cannot be like the following line should be expressed in terms
|
1520
|
+
# of C1, C2 etc....
|
1521
|
+
for index,label in enumerate(operationLabel):
|
1522
|
+
for operation in operations:
|
1523
|
+
|
1524
|
+
if index==0:
|
1525
|
+
alias = operation
|
1526
|
+
else:
|
1527
|
+
alias = operation + label
|
1528
|
+
|
1529
|
+
selectStr += "%s %s(%s) AS %s" % (separator, operation,
|
1530
|
+
self._columnsMapping[label],
|
1531
|
+
alias)
|
1532
|
+
separator = ', '
|
1533
|
+
if groupByLabels:
|
1534
|
+
groupByStr = 'GROUP BY '
|
1535
|
+
separator = ' '
|
1536
|
+
for groupByLabel in groupByLabels:
|
1537
|
+
groupByCol = self._columnsMapping[groupByLabel]
|
1538
|
+
selectStr += ', %(groupByCol)s as "%(groupByLabel)s"' % locals()
|
1539
|
+
groupByStr += "%s %s" % (separator, groupByCol)
|
1540
|
+
separator = ', '
|
1541
|
+
else:
|
1542
|
+
groupByStr = ' '
|
1543
|
+
sqlCommand = selectStr + "\n" + self.FROM + "\n" + groupByStr
|
1544
|
+
self.executeCommand(sqlCommand)
|
1545
|
+
return self._results(iterate=False)
|
1546
|
+
|
1547
|
+
def count(self):
|
1548
|
+
""" Return the number of elements in the table. """
|
1549
|
+
self.executeCommand(self.selectCmd(None, orderByStr="").replace('*', 'COUNT(*)'))
|
1550
|
+
return self.cursor.fetchone()[0]
|
1551
|
+
|
1552
|
+
def maxId(self):
|
1553
|
+
""" Return the maximum id from the Objects table. """
|
1554
|
+
self.executeCommand(self.selectCmd(None, orderByStr="").replace('*', 'MAX(id)'))
|
1555
|
+
return self.cursor.fetchone()[0]
|
1556
|
+
|
1557
|
+
# FIXME: Seems to be duplicated and a subset of selectAll
|
1558
|
+
def selectObjectsBy(self, iterate=False, **args):
|
1559
|
+
"""More flexible select where the constraints can be passed
|
1560
|
+
as a dictionary, the concatenation is done by an AND"""
|
1561
|
+
whereList = ['%s=?' % k for k in args.keys()]
|
1562
|
+
whereStr = ' AND '.join(whereList)
|
1563
|
+
whereTuple = tuple(args.values())
|
1564
|
+
whereStr = self._whereToWhereStr(whereStr)
|
1565
|
+
self.executeCommand(self.selectCmd(whereStr), whereTuple)
|
1566
|
+
return self._results(iterate)
|
1567
|
+
|
1568
|
+
# FIXME: Seems to be duplicated and a subset of selectAll
|
1569
|
+
# Moreover, it does not translate between "user columns" and
|
1570
|
+
# "internal" Objects table columns
|
1571
|
+
def selectObjectsWhere(self, whereStr, iterate=False):
|
1572
|
+
self.executeCommand(self.selectCmd(whereStr))
|
1573
|
+
return self._results(iterate)
|
1574
|
+
|
1575
|
+
def deleteObject(self, objId):
|
1576
|
+
"""Delete an existing object"""
|
1577
|
+
self.executeCommand(self.DELETE + "id=?", (objId,))
|
1578
|
+
|
1579
|
+
def deleteAll(self):
|
1580
|
+
""" Delete all objects from the db. """
|
1581
|
+
if not self.missingTables():
|
1582
|
+
self.executeCommand(self.DELETE + "1")
|
1583
|
+
|