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.
Files changed (104) hide show
  1. pyworkflow/apps/__init__.py +29 -0
  2. pyworkflow/apps/pw_manager.py +37 -0
  3. pyworkflow/apps/pw_plot.py +51 -0
  4. pyworkflow/apps/pw_project.py +130 -0
  5. pyworkflow/apps/pw_protocol_list.py +143 -0
  6. pyworkflow/apps/pw_protocol_run.py +51 -0
  7. pyworkflow/apps/pw_run_tests.py +268 -0
  8. pyworkflow/apps/pw_schedule_run.py +322 -0
  9. pyworkflow/apps/pw_sleep.py +37 -0
  10. pyworkflow/apps/pw_sync_data.py +440 -0
  11. pyworkflow/apps/pw_viewer.py +78 -0
  12. pyworkflow/constants.py +1 -1
  13. pyworkflow/gui/__init__.py +36 -0
  14. pyworkflow/gui/browser.py +768 -0
  15. pyworkflow/gui/canvas.py +1190 -0
  16. pyworkflow/gui/dialog.py +981 -0
  17. pyworkflow/gui/form.py +2727 -0
  18. pyworkflow/gui/graph.py +247 -0
  19. pyworkflow/gui/graph_layout.py +271 -0
  20. pyworkflow/gui/gui.py +571 -0
  21. pyworkflow/gui/matplotlib_image.py +233 -0
  22. pyworkflow/gui/plotter.py +247 -0
  23. pyworkflow/gui/project/__init__.py +25 -0
  24. pyworkflow/gui/project/base.py +193 -0
  25. pyworkflow/gui/project/constants.py +139 -0
  26. pyworkflow/gui/project/labels.py +205 -0
  27. pyworkflow/gui/project/project.py +491 -0
  28. pyworkflow/gui/project/searchprotocol.py +240 -0
  29. pyworkflow/gui/project/searchrun.py +181 -0
  30. pyworkflow/gui/project/steps.py +171 -0
  31. pyworkflow/gui/project/utils.py +332 -0
  32. pyworkflow/gui/project/variables.py +179 -0
  33. pyworkflow/gui/project/viewdata.py +472 -0
  34. pyworkflow/gui/project/viewprojects.py +519 -0
  35. pyworkflow/gui/project/viewprotocols.py +2141 -0
  36. pyworkflow/gui/project/viewprotocols_extra.py +562 -0
  37. pyworkflow/gui/text.py +774 -0
  38. pyworkflow/gui/tooltip.py +185 -0
  39. pyworkflow/gui/tree.py +684 -0
  40. pyworkflow/gui/widgets.py +307 -0
  41. pyworkflow/mapper/__init__.py +26 -0
  42. pyworkflow/mapper/mapper.py +226 -0
  43. pyworkflow/mapper/sqlite.py +1583 -0
  44. pyworkflow/mapper/sqlite_db.py +145 -0
  45. pyworkflow/object.py +1 -0
  46. pyworkflow/plugin.py +4 -4
  47. pyworkflow/project/__init__.py +31 -0
  48. pyworkflow/project/config.py +454 -0
  49. pyworkflow/project/manager.py +180 -0
  50. pyworkflow/project/project.py +2095 -0
  51. pyworkflow/project/usage.py +165 -0
  52. pyworkflow/protocol/__init__.py +38 -0
  53. pyworkflow/protocol/bibtex.py +48 -0
  54. pyworkflow/protocol/constants.py +87 -0
  55. pyworkflow/protocol/executor.py +515 -0
  56. pyworkflow/protocol/hosts.py +318 -0
  57. pyworkflow/protocol/launch.py +277 -0
  58. pyworkflow/protocol/package.py +42 -0
  59. pyworkflow/protocol/params.py +781 -0
  60. pyworkflow/protocol/protocol.py +2712 -0
  61. pyworkflow/resources/protlabels.xcf +0 -0
  62. pyworkflow/resources/sprites.png +0 -0
  63. pyworkflow/resources/sprites.xcf +0 -0
  64. pyworkflow/template.py +1 -1
  65. pyworkflow/tests/__init__.py +29 -0
  66. pyworkflow/tests/test_utils.py +25 -0
  67. pyworkflow/tests/tests.py +342 -0
  68. pyworkflow/utils/__init__.py +38 -0
  69. pyworkflow/utils/dataset.py +414 -0
  70. pyworkflow/utils/echo.py +104 -0
  71. pyworkflow/utils/graph.py +169 -0
  72. pyworkflow/utils/log.py +293 -0
  73. pyworkflow/utils/path.py +528 -0
  74. pyworkflow/utils/process.py +154 -0
  75. pyworkflow/utils/profiler.py +92 -0
  76. pyworkflow/utils/progressbar.py +154 -0
  77. pyworkflow/utils/properties.py +618 -0
  78. pyworkflow/utils/reflection.py +129 -0
  79. pyworkflow/utils/utils.py +880 -0
  80. pyworkflow/utils/which.py +229 -0
  81. pyworkflow/webservices/__init__.py +8 -0
  82. pyworkflow/webservices/config.py +8 -0
  83. pyworkflow/webservices/notifier.py +152 -0
  84. pyworkflow/webservices/repository.py +59 -0
  85. pyworkflow/webservices/workflowhub.py +86 -0
  86. pyworkflowtests/tests/__init__.py +0 -0
  87. pyworkflowtests/tests/test_canvas.py +72 -0
  88. pyworkflowtests/tests/test_domain.py +45 -0
  89. pyworkflowtests/tests/test_logs.py +74 -0
  90. pyworkflowtests/tests/test_mappers.py +392 -0
  91. pyworkflowtests/tests/test_object.py +507 -0
  92. pyworkflowtests/tests/test_project.py +42 -0
  93. pyworkflowtests/tests/test_protocol_execution.py +146 -0
  94. pyworkflowtests/tests/test_protocol_export.py +78 -0
  95. pyworkflowtests/tests/test_protocol_output.py +158 -0
  96. pyworkflowtests/tests/test_streaming.py +47 -0
  97. pyworkflowtests/tests/test_utils.py +210 -0
  98. {scipion_pyworkflow-3.11.0.dist-info → scipion_pyworkflow-3.11.2.dist-info}/METADATA +2 -2
  99. scipion_pyworkflow-3.11.2.dist-info/RECORD +162 -0
  100. scipion_pyworkflow-3.11.0.dist-info/RECORD +0 -71
  101. {scipion_pyworkflow-3.11.0.dist-info → scipion_pyworkflow-3.11.2.dist-info}/WHEEL +0 -0
  102. {scipion_pyworkflow-3.11.0.dist-info → scipion_pyworkflow-3.11.2.dist-info}/entry_points.txt +0 -0
  103. {scipion_pyworkflow-3.11.0.dist-info → scipion_pyworkflow-3.11.2.dist-info}/licenses/LICENSE.txt +0 -0
  104. {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
+