scipion-pyworkflow 3.7.0__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 (140) hide show
  1. pyworkflow/__init__.py +33 -0
  2. pyworkflow/apps/__init__.py +29 -0
  3. pyworkflow/apps/pw_manager.py +37 -0
  4. pyworkflow/apps/pw_plot.py +51 -0
  5. pyworkflow/apps/pw_project.py +113 -0
  6. pyworkflow/apps/pw_protocol_list.py +143 -0
  7. pyworkflow/apps/pw_protocol_run.py +51 -0
  8. pyworkflow/apps/pw_run_tests.py +267 -0
  9. pyworkflow/apps/pw_schedule_run.py +322 -0
  10. pyworkflow/apps/pw_sleep.py +37 -0
  11. pyworkflow/apps/pw_sync_data.py +439 -0
  12. pyworkflow/apps/pw_viewer.py +78 -0
  13. pyworkflow/config.py +536 -0
  14. pyworkflow/constants.py +212 -0
  15. pyworkflow/exceptions.py +18 -0
  16. pyworkflow/gui/__init__.py +36 -0
  17. pyworkflow/gui/browser.py +726 -0
  18. pyworkflow/gui/canvas.py +1190 -0
  19. pyworkflow/gui/dialog.py +976 -0
  20. pyworkflow/gui/form.py +2627 -0
  21. pyworkflow/gui/graph.py +247 -0
  22. pyworkflow/gui/graph_layout.py +271 -0
  23. pyworkflow/gui/gui.py +566 -0
  24. pyworkflow/gui/matplotlib_image.py +233 -0
  25. pyworkflow/gui/plotter.py +247 -0
  26. pyworkflow/gui/project/__init__.py +25 -0
  27. pyworkflow/gui/project/base.py +192 -0
  28. pyworkflow/gui/project/constants.py +139 -0
  29. pyworkflow/gui/project/labels.py +205 -0
  30. pyworkflow/gui/project/project.py +484 -0
  31. pyworkflow/gui/project/searchprotocol.py +154 -0
  32. pyworkflow/gui/project/searchrun.py +181 -0
  33. pyworkflow/gui/project/steps.py +166 -0
  34. pyworkflow/gui/project/utils.py +332 -0
  35. pyworkflow/gui/project/variables.py +179 -0
  36. pyworkflow/gui/project/viewdata.py +472 -0
  37. pyworkflow/gui/project/viewprojects.py +510 -0
  38. pyworkflow/gui/project/viewprotocols.py +2093 -0
  39. pyworkflow/gui/project/viewprotocols_extra.py +560 -0
  40. pyworkflow/gui/text.py +771 -0
  41. pyworkflow/gui/tooltip.py +185 -0
  42. pyworkflow/gui/tree.py +684 -0
  43. pyworkflow/gui/widgets.py +307 -0
  44. pyworkflow/mapper/__init__.py +26 -0
  45. pyworkflow/mapper/mapper.py +222 -0
  46. pyworkflow/mapper/sqlite.py +1578 -0
  47. pyworkflow/mapper/sqlite_db.py +145 -0
  48. pyworkflow/object.py +1512 -0
  49. pyworkflow/plugin.py +712 -0
  50. pyworkflow/project/__init__.py +31 -0
  51. pyworkflow/project/config.py +451 -0
  52. pyworkflow/project/manager.py +179 -0
  53. pyworkflow/project/project.py +1990 -0
  54. pyworkflow/project/scripts/clean_projects.py +77 -0
  55. pyworkflow/project/scripts/config.py +92 -0
  56. pyworkflow/project/scripts/create.py +77 -0
  57. pyworkflow/project/scripts/edit_workflow.py +90 -0
  58. pyworkflow/project/scripts/fix_links.py +39 -0
  59. pyworkflow/project/scripts/load.py +87 -0
  60. pyworkflow/project/scripts/refresh.py +83 -0
  61. pyworkflow/project/scripts/schedule.py +111 -0
  62. pyworkflow/project/scripts/stack2volume.py +41 -0
  63. pyworkflow/project/scripts/stop.py +81 -0
  64. pyworkflow/protocol/__init__.py +38 -0
  65. pyworkflow/protocol/bibtex.py +48 -0
  66. pyworkflow/protocol/constants.py +86 -0
  67. pyworkflow/protocol/executor.py +334 -0
  68. pyworkflow/protocol/hosts.py +313 -0
  69. pyworkflow/protocol/launch.py +270 -0
  70. pyworkflow/protocol/package.py +42 -0
  71. pyworkflow/protocol/params.py +744 -0
  72. pyworkflow/protocol/protocol.py +2554 -0
  73. pyworkflow/resources/Imagej.png +0 -0
  74. pyworkflow/resources/chimera.png +0 -0
  75. pyworkflow/resources/fa-exclamation-triangle_alert.png +0 -0
  76. pyworkflow/resources/fa-info-circle_alert.png +0 -0
  77. pyworkflow/resources/fa-search.png +0 -0
  78. pyworkflow/resources/fa-times-circle_alert.png +0 -0
  79. pyworkflow/resources/file_vol.png +0 -0
  80. pyworkflow/resources/loading.gif +0 -0
  81. pyworkflow/resources/no-image128.png +0 -0
  82. pyworkflow/resources/scipion_bn.png +0 -0
  83. pyworkflow/resources/scipion_icon.png +0 -0
  84. pyworkflow/resources/scipion_icon.svg +397 -0
  85. pyworkflow/resources/scipion_icon_proj.png +0 -0
  86. pyworkflow/resources/scipion_icon_projs.png +0 -0
  87. pyworkflow/resources/scipion_icon_prot.png +0 -0
  88. pyworkflow/resources/scipion_logo.png +0 -0
  89. pyworkflow/resources/scipion_logo_normal.png +0 -0
  90. pyworkflow/resources/scipion_logo_small.png +0 -0
  91. pyworkflow/resources/sprites.png +0 -0
  92. pyworkflow/resources/sprites.xcf +0 -0
  93. pyworkflow/resources/wait.gif +0 -0
  94. pyworkflow/template.py +322 -0
  95. pyworkflow/tests/__init__.py +29 -0
  96. pyworkflow/tests/test_utils.py +25 -0
  97. pyworkflow/tests/tests.py +341 -0
  98. pyworkflow/utils/__init__.py +38 -0
  99. pyworkflow/utils/dataset.py +414 -0
  100. pyworkflow/utils/echo.py +104 -0
  101. pyworkflow/utils/graph.py +196 -0
  102. pyworkflow/utils/log.py +284 -0
  103. pyworkflow/utils/path.py +527 -0
  104. pyworkflow/utils/process.py +132 -0
  105. pyworkflow/utils/profiler.py +92 -0
  106. pyworkflow/utils/progressbar.py +154 -0
  107. pyworkflow/utils/properties.py +627 -0
  108. pyworkflow/utils/reflection.py +129 -0
  109. pyworkflow/utils/utils.py +877 -0
  110. pyworkflow/utils/which.py +229 -0
  111. pyworkflow/viewer.py +328 -0
  112. pyworkflow/webservices/__init__.py +8 -0
  113. pyworkflow/webservices/config.py +11 -0
  114. pyworkflow/webservices/notifier.py +162 -0
  115. pyworkflow/webservices/repository.py +59 -0
  116. pyworkflow/webservices/workflowhub.py +74 -0
  117. pyworkflow/wizard.py +64 -0
  118. pyworkflowtests/__init__.py +51 -0
  119. pyworkflowtests/bibtex.py +51 -0
  120. pyworkflowtests/objects.py +830 -0
  121. pyworkflowtests/protocols.py +154 -0
  122. pyworkflowtests/tests/__init__.py +0 -0
  123. pyworkflowtests/tests/test_canvas.py +72 -0
  124. pyworkflowtests/tests/test_domain.py +45 -0
  125. pyworkflowtests/tests/test_logs.py +74 -0
  126. pyworkflowtests/tests/test_mappers.py +392 -0
  127. pyworkflowtests/tests/test_object.py +507 -0
  128. pyworkflowtests/tests/test_project.py +42 -0
  129. pyworkflowtests/tests/test_protocol_execution.py +72 -0
  130. pyworkflowtests/tests/test_protocol_export.py +78 -0
  131. pyworkflowtests/tests/test_protocol_output.py +158 -0
  132. pyworkflowtests/tests/test_streaming.py +47 -0
  133. pyworkflowtests/tests/test_utils.py +210 -0
  134. scipion_pyworkflow-3.7.0.dist-info/LICENSE.txt +674 -0
  135. scipion_pyworkflow-3.7.0.dist-info/METADATA +107 -0
  136. scipion_pyworkflow-3.7.0.dist-info/RECORD +140 -0
  137. scipion_pyworkflow-3.7.0.dist-info/WHEEL +5 -0
  138. scipion_pyworkflow-3.7.0.dist-info/dependency_links.txt +1 -0
  139. scipion_pyworkflow-3.7.0.dist-info/entry_points.txt +5 -0
  140. scipion_pyworkflow-3.7.0.dist-info/top_level.txt +2 -0
pyworkflow/object.py ADDED
@@ -0,0 +1,1512 @@
1
+ # **************************************************************************
2
+ # *
3
+ # * Authors: J.M. De la Rosa Trevin (jmdelarosa@cnb.csic.es)
4
+ # *
5
+ # * Unidad de Bioinformatica of Centro Nacional de Biotecnologia , CSIC
6
+ # *
7
+ # * This program is free software; you can redistribute it and/or modify
8
+ # * it under the terms of the GNU General Public License as published by
9
+ # * the Free Software Foundation; either version 3 of the License, or
10
+ # * (at your option) any later version.
11
+ # *
12
+ # * This program is distributed in the hope that it will be useful,
13
+ # * but WITHOUT ANY WARRANTY; without even the implied warranty of
14
+ # * MERCHANTABIlITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
+ # * GNU General Public License for more details.
16
+ # *
17
+ # * You should have received a copy of the GNU General Public License
18
+ # * along with this program; if not, write to the Free Software
19
+ # * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
20
+ # * 02111-1307 USA
21
+ # *
22
+ # * All comments concerning this program package may be sent to the
23
+ # * e-mail address 'scipion@cnb.csic.es'
24
+ # *
25
+ # **************************************************************************
26
+ """
27
+ This modules holds the base classes for the ORM implementation.
28
+ The Object class is the root in the hierarchy and some other
29
+ basic classes.
30
+ """
31
+ import logging
32
+ logger = logging.getLogger(__name__)
33
+
34
+ from collections import OrderedDict
35
+ import datetime as dt
36
+ from os.path import getmtime
37
+
38
+ from pyworkflow.utils import getListFromValues, getListFromRangeString, strToDuration
39
+ from pyworkflow.utils.reflection import getSubclasses
40
+
41
+
42
+ # Binary relations always involve two objects, we
43
+ # call them parent-child objects, the following
44
+ # constants reflect which direction of the relation we refer
45
+ RELATION_CHILDS = 0
46
+ RELATION_PARENTS = 1
47
+
48
+ # There are other relations related to data provenance,
49
+ # which object were used to produce a result (source of it)
50
+ RELATION_SOURCE = 'relation_datasource'
51
+ RELATION_TRANSFORM = 'relation_transform'
52
+
53
+
54
+ # Then column name of the parent id relation
55
+ OBJECT_PARENT_ID = 'object_parent_id'
56
+
57
+
58
+ class Object(object):
59
+ """ All objects in our Domain should inherit from this class
60
+ that will contain all base properties"""
61
+
62
+ def __init__(self, value=None, **kwargs):
63
+ object.__init__(self)
64
+ if len(kwargs) == 0:
65
+ self._objIsPointer = False # True if will be treated as a reference for storage
66
+ self._objId = None # Unique identifier of this object in some context
67
+ self._objParentId = None # identifier of the parent object
68
+ self._objName = '' # The name of the object will contains the whole path of ancestors
69
+ self._objLabel = '' # This will serve to label the objects
70
+ self._objComment = ''
71
+ # Performance: self._objTag = None # This attribute serve to make some annotation on the object.
72
+ self._objDoStore = True
73
+ else:
74
+
75
+ self._objIsPointer = kwargs.get('objIsPointer', False) # True if will be treated as a reference for storage
76
+ self._objId = kwargs.get('objId', None) # Unique identifier of this object in some context
77
+ self._objParentId = kwargs.get('objParentId', None) # identifier of the parent object
78
+ self._objName = kwargs.get('objName', '') # The name of the object will contains the whole path of ancestors
79
+ self._objLabel = kwargs.get('objLabel', '') # This will serve to label the objects
80
+ self._objComment = kwargs.get('objComment', '')
81
+ #Performance: self._objTag = kwargs.get('objTag', None) # This attribute serve to make some annotation on the object.
82
+ self._objDoStore = kwargs.get('objDoStore', True) # True if this object will be stored from his parent
83
+
84
+ self._objCreation = None
85
+ self._objParent = None # Reference to parent object
86
+ self._objEnabled = True
87
+ self.set(value)
88
+
89
+ @classmethod
90
+ def getClassName(cls):
91
+ return cls.__name__
92
+
93
+ def getClass(self):
94
+ return type(self)
95
+
96
+ @classmethod
97
+ def getDoc(cls):
98
+ return cls.__doc__ or ''
99
+
100
+ # FIXME: This function should be renamed to hasAttribute when we address that issue
101
+ def hasAttributeExt(self, attrName):
102
+ attrList = attrName.split('.')
103
+ obj = self
104
+ for partName in attrList:
105
+ obj = getattr(obj, partName, None)
106
+ if obj is None:
107
+ return False
108
+ return True
109
+
110
+ # FIXME: This function is not symmetric with setAttributeValue
111
+ def hasAttribute(self, attrName):
112
+ return hasattr(self, attrName)
113
+
114
+ # FIXME: This function is not symmetric with setAttributeValue
115
+ def getAttributeValue(self, attrName, defaultValue=None):
116
+ """ Get the attribute value given its name.
117
+ Equivalent to getattr(self, name).get()
118
+ """
119
+ attr = getattr(self, attrName, None)
120
+ if attr is None:
121
+ value = defaultValue
122
+ elif callable(attr):
123
+ value = attr()
124
+ elif isinstance(attr, Object):
125
+ value = attr.get()
126
+ else:
127
+ value = attr # behave well for non-Object attributes
128
+ return value
129
+
130
+ def setAttributeValue(self, attrName, value, ignoreMissing=True):
131
+ """ Set the attribute value given its name.
132
+ Equivalent to setattr(self, name).set(value)
133
+ If the attrName contains dot: x.y
134
+ it will be equivalent to getattr(getattr(self, 'x'), 'y').set(value)
135
+ If ignoreMissing is True, non-existing attrName will not raise an
136
+ exception.
137
+ """
138
+ attrList = attrName.split('.')
139
+ obj = self
140
+ for partName in attrList:
141
+ obj = getattr(obj, partName, None)
142
+ if obj is None:
143
+ if ignoreMissing:
144
+ return
145
+ raise Exception("Object.setAttributeValue: %s has no attribute %s."
146
+ % (self.__class__.__name__, attrName))
147
+ obj.set(value)
148
+
149
+ def getAttributes(self):
150
+ """Return the list of attributes that are
151
+ subclasses of Object"""
152
+ for name in vars(self):
153
+ value = getattr(self, name)
154
+ if isinstance(value, Object):
155
+ yield name, value
156
+
157
+ def getAttributesToStore(self):
158
+ """Return the list of attributes than are
159
+ subclasses of Object and will be stored"""
160
+ for key, attr in self.getAttributes():
161
+ try:
162
+ if attr is not None and attr._objDoStore:
163
+ yield key, attr
164
+ except Exception as e:
165
+ logger.info("Object.getAttributesToStore: attribute '%s' (%s) seems to "
166
+ "be overwritten, since '_objDoStore' was not found. Ignoring attribute." % (key, type(attr)))
167
+ logger.debug("Exception: %s" % e)
168
+
169
+ def isPointer(self):
170
+ """If this is true, the value field is a pointer
171
+ to another object"""
172
+ return self._objIsPointer
173
+
174
+ def _convertValue(self, value):
175
+ """Convert a value to desired scalar type"""
176
+ return value
177
+
178
+ def set(self, value):
179
+ """Set the internal value, if it is different from None
180
+ call the convert function in subclasses"""
181
+ if value is not None:
182
+ value = self._convertValue(value)
183
+ self._objValue = value
184
+
185
+ def get(self):
186
+ """Return internal value"""
187
+ return self._objValue
188
+
189
+ def trace(self, callback):
190
+ """ Add an observer when the set method is called. """
191
+ if self.set == self.__setTrace:
192
+ pass
193
+ else:
194
+ self.__set = self.set
195
+ self.set = self.__setTrace
196
+ self.__setCallback = callback
197
+
198
+ def __setTrace(self, value):
199
+ self.__set(value)
200
+ self.__setCallback()
201
+
202
+ def getObjValue(self):
203
+ """Return the internal value for storage.
204
+ This is a good place to do some update of the
205
+ internal value before been stored"""
206
+ return self._objValue
207
+
208
+ def getObjId(self):
209
+ """Return object id"""
210
+ return self._objId
211
+
212
+ def setObjId(self, newId):
213
+ """Set the object id"""
214
+ self._objId = newId
215
+
216
+ def copyObjId(self, other):
217
+ """ Copy the object id form other to self. """
218
+ self.setObjId(other.getObjId())
219
+
220
+ def hasObjId(self):
221
+ return self._objId is not None
222
+
223
+ def cleanObjId(self):
224
+ """ This function will set to None this object id
225
+ and the id of all its children attributes.
226
+ This function should be used when retrieving
227
+ an object from a mapper and storing in a different one.
228
+ """
229
+ self.setObjId(None)
230
+ for _, attr in self.getAttributesToStore():
231
+ attr.cleanObjId()
232
+
233
+ def getObjParentId(self):
234
+ return self._objParentId
235
+
236
+ def hasObjParentId(self):
237
+ return self._objParentId is not None
238
+
239
+ def getObjLabel(self):
240
+ """ Return the label associated with this object"""
241
+ return self._objLabel
242
+
243
+ def setObjLabel(self, label):
244
+ """ Set the label to better identify this object"""
245
+ self._objLabel = label
246
+
247
+ def getObjComment(self):
248
+ """ Return the comment associated with this object"""
249
+ return self._objComment
250
+
251
+ def setObjComment(self, comment):
252
+ """ Set the comment to better identify this object"""
253
+ self._objComment = comment
254
+
255
+ def setObjCreation(self, creation):
256
+ """ Set the creation time of the object. """
257
+ self._objCreation = creation
258
+
259
+ def getObjCreation(self):
260
+ """ Return the stored creation time of the object. """
261
+ return self._objCreation
262
+
263
+ def getObjCreationAsDate(self):
264
+ """ Return the stored creation time of the object as date """
265
+ return String.getDatetime(self._objCreation)
266
+
267
+ def strId(self):
268
+ """String representation of id"""
269
+ return str(self._objId)
270
+
271
+ def getName(self):
272
+ # TODO: REMOVE THIS FUNCTION, SINCE IT DOES NOT COMPLAIN WITH _objX naming
273
+ return self._objName
274
+
275
+ def getObjName(self):
276
+ return self._objName
277
+
278
+ def setEnabled(self, enabled):
279
+ self._objEnabled = bool(enabled)
280
+
281
+ def isEnabled(self):
282
+ """Return if object is enabled"""
283
+ return self._objEnabled
284
+
285
+ def getNameId(self):
286
+ """ Return an unique and readable id that identifies this object. """
287
+ label = self.getObjLabel()
288
+ if len(label) > 0:
289
+ return label
290
+ elif self.hasObjId():
291
+ return '%s.%s' % (self.getName(), self.strId())
292
+ return ''
293
+
294
+ def getLastName(self):
295
+ """ If the name contains parent path, remove it
296
+ and only return the attribute name in its parent.
297
+ """
298
+ if '.' in self._objName:
299
+ return self._objName.split('.')[-1]
300
+ return self._objName
301
+
302
+ def setName(self, name):
303
+ self._objName = name
304
+
305
+ def hasValue(self):
306
+ return True
307
+
308
+ def getStore(self):
309
+ """Return True if the object will be stored by the mapper"""
310
+ return self._objDoStore
311
+
312
+ def setStore(self, value):
313
+ """set the store flag"""
314
+ self._objDoStore = value
315
+
316
+ def __hash__(self):
317
+ # Issue found in scipion-em-xmipp/xmipp3/protocols/protocol_extract_particles_pairs.py (_setupBasicProperties)
318
+ # TypeError: unhashable type: 'Micrograph'
319
+ # Solution: Hashability makes an object usable as a dictionary key and a set member, because these data
320
+ # structures use the hash value internally. All of Python’s immutable built_in objects are hashable, while no
321
+ # mutable containers (such as lists or dictionaries) are. Objects which are instances of user-defined classes
322
+ # are hashable by default; they all compare unequal, and their hash value is their id().
323
+ # https://docs.python.org/3.1/glossary.html
324
+ return id(self)
325
+
326
+ def __eq__(self, other):
327
+ """Comparison for scalars should be by value
328
+ and for other objects by reference"""
329
+ if self._objValue is None:
330
+ return object.__eq__(self, other)
331
+ return self._objValue == other._objValue
332
+
333
+ def equalAttributes(self, other, ignore=[], verbose=False):
334
+ """Compare that all attributes are equal"""
335
+ for k, v1 in self.getAttributes():
336
+ # v1 = getattr(self, k) # This is necessary because of FakedObject simulation of getattr
337
+ # Skip comparison of attribute names in 'ignore' list
338
+ if k in ignore:
339
+ continue
340
+ v2 = getattr(other, k)
341
+ if issubclass(type(v1), Object):
342
+ comp = v1.equalAttributes(v2, ignore=ignore, verbose=verbose)
343
+ else:
344
+ comp = v1 == v2
345
+ if not comp:
346
+ if verbose:
347
+ logger.info("Different attributes: self.%s = %s, other.%s = %s" % (k, v1, k, v2))
348
+ return False
349
+ return True
350
+
351
+ def copyAttributes(self, other, *attrNames):
352
+ """ Copy attributes in attrNames from other to self.
353
+ If the name X is in attrNames, it would be equivalent to:
354
+ self.X.set(other.X.get())
355
+ This method is more useful for Scalar attributes.
356
+ There are two paths for Pointer and PointerList.
357
+ """
358
+ for name in attrNames:
359
+ attr = getattr(self, name, None)
360
+ otherAttr = getattr(other, name)
361
+
362
+ if attr is None:
363
+ setattr(self, name, otherAttr.clone())
364
+ elif isinstance(attr, Pointer):
365
+ attr.copy(otherAttr)
366
+ elif isinstance(attr, PointerList):
367
+ for pointer in otherAttr:
368
+ attr.append(pointer)
369
+ elif isinstance(attr, Scalar) and otherAttr.hasPointer():
370
+ attr.copy(otherAttr)
371
+ else:
372
+ attr.set(otherAttr.get())
373
+
374
+ def __getObjDict(self, prefix, objDict, includeClass, includePointers=True):
375
+ if prefix:
376
+ prefix += '.'
377
+ for k, v in self.getAttributesToStore():
378
+ self.fillObjDict(prefix, objDict, includeClass, k, v, includePointers=includePointers)
379
+
380
+ @staticmethod
381
+ def fillObjDict(prefix, objDict, includeClass, k, v, includePointers=False):
382
+
383
+ if not v.isPointer():
384
+ kPrefix = prefix + k
385
+ if includeClass:
386
+ objDict[kPrefix] = (v.getClassName(), v.getObjValue())
387
+ else:
388
+ objDict[kPrefix] = v.getObjValue()
389
+ if not isinstance(v, Scalar):
390
+ v.__getObjDict(kPrefix, objDict, includeClass)
391
+ # Is a pointer ...
392
+ elif includePointers and not v.pointsNone():
393
+ # Should we take into account the prefix??
394
+ objDict[k]=v.getUniqueId()
395
+
396
+
397
+ def getObjDict(self, includeClass=False, includeBasic=False, includePointers=False):
398
+ """
399
+ Return all attributes and values in a dictionary.
400
+ Nested attributes will be separated with a dot in the dict key.
401
+
402
+ :param includeClass: if True, the values will be a tuple (ClassName, value)
403
+ otherwise only the values of the attributes
404
+ :param includeBasic: if True include the id, label and comment.
405
+ :param includePointers: If true pointer are also added using Pointer.getUniqueId --> "2.outputTomograms"
406
+
407
+ :return a dictionary
408
+
409
+ includeBasic example::
410
+
411
+ object.id: objId
412
+ object.label: objLabel
413
+ object.comment: objComment
414
+
415
+ """
416
+ d = OrderedDict()
417
+
418
+ if includeClass:
419
+ d['self'] = (self.getClassName(),)
420
+
421
+ if includeBasic:
422
+ d['object.id'] = self.getObjId()
423
+ d['object.label'] = self.getObjLabel()
424
+ d['object.comment'] = self.getObjComment()
425
+
426
+ self.__getObjDict('', d, includeClass, includePointers=includePointers )
427
+
428
+ return d
429
+
430
+ def setAttributesFromDict(self, attrDict, setBasic=True,
431
+ ignoreMissing=False):
432
+ """ Set object attributes from the dict obtained from getObjDict.
433
+ WARNING: this function is yet experimental and not fully tested.
434
+ """
435
+ if setBasic:
436
+ self.setObjId(attrDict.get('object.id', None))
437
+ self.setObjLabel(attrDict.get('object.label', ''))
438
+ self.setObjComment(attrDict.get('object.comment', ''))
439
+
440
+ for attrName, value in attrDict.items():
441
+ if not attrName.startswith('object.'):
442
+ self.setAttributeValue(attrName, value, ignoreMissing)
443
+
444
+ def __getMappedDict(self, prefix, objDict):
445
+ if prefix:
446
+ prefix += '.'
447
+ for k, v in self.getAttributesToStore():
448
+ if not v.isPointer():
449
+ kPrefix = prefix + k
450
+ objDict[kPrefix] = v
451
+ if not isinstance(v, Scalar):
452
+ v.__getMappedDict(kPrefix, objDict)
453
+
454
+ def getMappedDict(self):
455
+ d = OrderedDict()
456
+ self.__getMappedDict('', d)
457
+ return d
458
+
459
+ def getNestedValue(self, key):
460
+ """ Retrieve the value of nested attributes like: _ctfModel.defocusU. """
461
+ attr = self
462
+ for p in key.split('.'):
463
+ attr = getattr(attr, p)
464
+ return attr.get()
465
+
466
+ def getValuesFromDict(self, objDict):
467
+ """ Retrieve the values of the attributes
468
+ for each of the keys that comes in the objDict.
469
+ """
470
+ return [self.getNestedValue(k) for k in objDict if k != 'self']
471
+
472
+ def getValuesFromMappedDict(self, mappedDict):
473
+ return [v.getObjValue() for v in mappedDict.values()]
474
+
475
+ def copy(self, other, copyId=True, ignoreAttrs=[], copyEnable=False):
476
+ """
477
+ Copy all attributes values from one object to the other.
478
+ The attributes will be created if needed with the corresponding type.
479
+
480
+ :param other: the other object from which to make the copy.
481
+ :param copyId: if true, the _objId will be also copied.
482
+ :param ignoreAttrs: pass a list with attributes names to ignore.
483
+ :param copyEnable: Pass true if you want enabled flag to be copied
484
+
485
+ """
486
+ copyDict = {'internalPointers': []}
487
+ self._copy(other, copyDict, copyId, ignoreAttrs=ignoreAttrs, copyEnable=copyEnable)
488
+ self._updatePointers(copyDict)
489
+ return copyDict
490
+
491
+ def _updatePointers(self, copyDict):
492
+ """ Update the internal pointers after a copy.
493
+ If there are pointers to other object in the copy
494
+ the references should be updated.
495
+ """
496
+ for ptr in copyDict['internalPointers']:
497
+ pointedId = ptr.getObjValue().getObjId()
498
+ if pointedId in copyDict:
499
+ ptr.set(copyDict[pointedId])
500
+
501
+ def _copy(self, other, copyDict, copyId, level=1, ignoreAttrs=[], copyEnable=False):
502
+ """ Recursively clone all attributes from one object to the other.
503
+ (Currently, we are not deleting attributes missing in the 'other' object.)
504
+ Params:
505
+
506
+ :param copyDict: this dict is used to store the ids map between 'other' and
507
+ 'self' attributes. It is used for update pointers and relations
508
+ later on. This will only work if the ids of 'other' attributes
509
+ has been properly set.
510
+ :param copyId: Pass true to copy the id
511
+ :param level: (1), depth to the copy
512
+ :param ignoreAttrs: List with attributes to ignore
513
+ :param copyEnable: (False) pass true to copy the enable flag
514
+ """
515
+ # Copy basic object data
516
+ # self._objName = other._objName
517
+ if copyId:
518
+ self._objId = other._objId
519
+ self._objValue = other._objValue
520
+ self._objLabel = other._objLabel
521
+ self._objComment = other._objComment
522
+
523
+ if copyEnable:
524
+ self._objEnabled = other._objEnabled
525
+
526
+ # Copy attributes recursively
527
+ for name, attr in other.getAttributes():
528
+ if name not in ignoreAttrs:
529
+ myAttr = getattr(self, name, None)
530
+
531
+ if myAttr is None:
532
+ myAttr = attr.getClass()()
533
+ setattr(self, name, myAttr)
534
+
535
+ myAttr._copy(attr, copyDict, copyId, level+2)
536
+ # Store the attr in the copyDict
537
+ if attr.hasObjId():
538
+ # storing in copyDict with id=", attr.getObjId()
539
+ copyDict[attr.getObjId()] = myAttr
540
+ # Use the copyDict to fix the reference in the copying object
541
+ # if the pointed one is inside the same object
542
+ if myAttr.isPointer() and myAttr.hasValue():
543
+ copyDict['internalPointers'].append(myAttr)
544
+
545
+ def clone(self, copyEnable=False):
546
+ """ Clones the object
547
+
548
+ :param copyEnable (False): clone also the enable flag"""
549
+
550
+ #NOTE: Maybe be, as default, we should be cloning the enable, but we found it is not doing so.
551
+ # So for now it is not cloned, probably to void carying out the enable which may be a decision by one method that should not be
552
+ # tranferred to the next method. Only Subsetting protocols should respect this.
553
+ clone = self.getClass()()
554
+ clone.copy(self, copyEnable=copyEnable)
555
+ return clone
556
+
557
+ def evalCondition(self, condition):
558
+ """
559
+ Check if condition is meet.
560
+
561
+ Examples of condition::
562
+
563
+ "hasCTF"
564
+ "hasCTF and not hasAlignment"
565
+
566
+ :param condition: the condition string, it can contain variables
567
+ or methods without arguments to be evaluated.
568
+
569
+ :return The value of the condition evaluated with values
570
+
571
+ """
572
+ # Split in possible tokens
573
+ import re
574
+ tokens = re.split(r'\W+', condition)
575
+ condStr = condition
576
+
577
+ for t in tokens:
578
+ if self.hasAttribute(t):
579
+ condStr = condStr.replace(t, str(self.getAttributeValue(t)))
580
+ return eval(condStr)
581
+
582
+ def printAll(self, name=None, level=0):
583
+ """Print object and all its attributes.
584
+ Mainly for debugging"""
585
+ tab = ' ' * (level*3)
586
+ idStr = '' # ' (id = %s, pid = %s)' % (self.getObjId(), self._objParentId)
587
+ if name is None:
588
+ logger.info(f"{tab} {self.getClassName()} {idStr}")
589
+ else:
590
+ if name == 'submitTemplate': # Skip this because very large value
591
+ value = '...'
592
+ else:
593
+ value = self.getObjValue()
594
+
595
+ logger.info(f"{tab} {name} = {value} {idStr}")
596
+ for k, v in self.getAttributes():
597
+ v.printAll(k, level + 1)
598
+
599
+ def printObjDict(self, includeClasses=False):
600
+ """Print object dictionary. Mainly for debugging"""
601
+ import pprint
602
+ pp = pprint.PrettyPrinter(indent=4)
603
+ pp.pprint(dict(self.getObjDict(includeClasses)))
604
+
605
+ class OrderedObject(Object):
606
+ """Legacy class, to be removed. Object should give same functionality and is faster"""
607
+ pass
608
+
609
+ class Scalar(Object):
610
+ """Base class for basic types"""
611
+ def hasValue(self):
612
+ return self._objValue is not None
613
+
614
+ def equalAttributes(self, other, ignore=[], verbose=False):
615
+ """Compare that all attributes are equal"""
616
+ return self._objValue == other._objValue
617
+
618
+ def __str__(self):
619
+ """String representation of the scalar value"""
620
+ return str(self.get())
621
+
622
+ def __cmp(self, other):
623
+ """ Comparison implementation for scalars. """
624
+ a = self.get()
625
+ b = other.get() if isinstance(other, Object) else other
626
+ # cmp does not longer exist in Python3, so we will follow
627
+ # the recommend replacement: (a > b) - (a < b)
628
+ # https://docs.python.org/3.0/whatsnew/3.0.html#ordering-comparisons
629
+
630
+ # Consider None values
631
+ if a is None and b is None:
632
+ return True
633
+ elif a is None or b is None:
634
+ return False
635
+ else:
636
+ return (a > b) - (a < b)
637
+
638
+ def __eq__(self, other):
639
+ """ Comparison for scalars should be by value while
640
+ for other objects by reference. """
641
+ return self.__cmp(other) == 0
642
+
643
+ def __ne__(self, other):
644
+ return self.__cmp(other) != 0
645
+
646
+ def __lt__(self, other):
647
+ return self.__cmp(other) < 0
648
+
649
+ def __le__(self, other):
650
+ return self.__cmp(other) <= 0
651
+
652
+ def __gt__(self, other):
653
+ return self.__cmp(other) > 0
654
+
655
+ def __ge__(self, other):
656
+ return self.__cmp(other) >= 0
657
+
658
+ def get(self, default=None):
659
+ """Get the value, if internal value is None
660
+ the default argument passed is returned. """
661
+ if self.hasPointer():
662
+ # Get pointed value
663
+ pointedValue = self._pointer.get()
664
+
665
+ return default if pointedValue is None else pointedValue.get(default)
666
+
667
+ if self.hasValue():
668
+ return self._objValue
669
+ return default
670
+
671
+ def _copy(self, other, *args, **kwargs):
672
+ if other.hasPointer():
673
+ self.setPointer(other.getPointer())
674
+ else:
675
+ self.set(other.get())
676
+
677
+ def swap(self, other):
678
+ """ Swap the contained value between
679
+ self and other objects.
680
+ """
681
+ tmp = self._objValue
682
+ self._objValue = other._objValue
683
+ other._objValue = tmp
684
+
685
+ def sum(self, value):
686
+ self._objValue += self._convertValue(value)
687
+
688
+ def multiply(self, value):
689
+ self._objValue *= value
690
+
691
+ def setPointer(self, pointer):
692
+ """ Set an internal pointer from this Scalar.
693
+ Then, the value (retrieved with get) will be obtained
694
+ from the pointed object.
695
+ """
696
+ if pointer is None:
697
+ if self.hasPointer():
698
+ delattr(self, "_pointer")
699
+ else:
700
+ self._pointer = pointer
701
+
702
+ def hasPointer(self):
703
+ """ Return True if this Scalar has an internal pointer
704
+ from where the value will be retrieved with the get() method.
705
+ """
706
+ return hasattr(self, '_pointer')
707
+
708
+ def getPointer(self):
709
+ """ Return the internal pointer of this Scalar or None. """
710
+ return getattr(self, '_pointer', None)
711
+
712
+
713
+ class Integer(Scalar):
714
+ """Integer object"""
715
+ def _convertValue(self, value):
716
+ return int(value)
717
+
718
+ def increment(self):
719
+ """ Add 1 to the current value. """
720
+ self._objValue += 1
721
+
722
+ def __float__(self):
723
+ return float(self.get())
724
+
725
+ def __int__(self):
726
+ return self.get()
727
+
728
+ def __long__(self):
729
+ return int(self.get())
730
+
731
+
732
+ class String(Scalar):
733
+ """String object. """
734
+ DATETIME_FORMAT = "%Y-%m-%d %H:%M:%S"
735
+ FS = ".%f" # Femto seconds
736
+
737
+ @classmethod
738
+ def getDatetime(cls, strValue, formatStr=None, fs=True):
739
+ """
740
+ Converts the given string value to a datetime object.
741
+
742
+ :param strValue: string representation of the date
743
+ :param formatStr: if None, uses the default cls:String.DATETIME_FORMAT.
744
+ :param fs: Use femto seconds or not, only when format=None
745
+
746
+ """
747
+ if formatStr is None:
748
+ try:
749
+ formatStr = cls.DATETIME_FORMAT
750
+ if fs:
751
+ formatStr += cls.FS
752
+ datetime = dt.datetime.strptime(strValue, formatStr)
753
+ except Exception as ex:
754
+ # Maybe the %f (femtoseconds) is not working
755
+ # let's try to format without it
756
+ datetime = dt.datetime.strptime(strValue, cls.DATETIME_FORMAT)
757
+ else:
758
+ datetime = dt.datetime.strptime(strValue, formatStr)
759
+
760
+ return datetime
761
+
762
+ def _convertValue(self, value):
763
+ return str(value)
764
+
765
+ def empty(self):
766
+ """ Return true if None or len == 0 """
767
+ if not self.hasValue():
768
+ return True
769
+ return len(self.get().strip()) == 0
770
+
771
+ def datetime(self, formatStr=None, fs=True):
772
+ """ Get the datetime from this object string value. """
773
+ return String.getDatetime(self._objValue, formatStr, fs)
774
+
775
+ def getListFromValues(self, length=None, caster=int):
776
+ """ Returns a list from a string with values as described at getListFromValues. Useful for """
777
+ return getListFromValues(self._objValue, length, caster)
778
+
779
+ def getListFromRange(self):
780
+ """ Returns a list from a string with values as described at getListFromRangeString.
781
+ Useful for NumericRangeParam params"""
782
+ return getListFromRangeString(self._objValue)
783
+
784
+ def toSeconds(self):
785
+
786
+ return strToDuration(self.get())
787
+
788
+
789
+ class Float(Scalar):
790
+ """Float object"""
791
+ EQUAL_PRECISION = 0.001
792
+
793
+ @classmethod
794
+ def setPrecision(cls, newPrecision):
795
+ """ Set the precision to compare float values.
796
+ Mainly used for testing purposes.
797
+ """
798
+ cls.EQUAL_PRECISION = newPrecision
799
+
800
+ def _convertValue(self, value):
801
+ return float(value)
802
+
803
+ def equalAttributes(self, other, ignore=[], verbose=False):
804
+ """Compare that all attributes are equal"""
805
+ # If both float has some value distinct of None
806
+ # then we should compare the absolute value of difference
807
+ # against the EQUAL_PRECISION to say equal or not
808
+ if self.hasValue() and other.hasValue():
809
+ return abs(self._objValue - other._objValue) < self.EQUAL_PRECISION
810
+ # If one has None as value, then both should have None
811
+ # to have equal attributes
812
+ if not self.hasValue() and not other.hasValue():
813
+ return True
814
+
815
+ return False
816
+
817
+ def __float__(self):
818
+ return self.get()
819
+
820
+
821
+ class Boolean(Scalar):
822
+ """Boolean object"""
823
+
824
+ def _convertValue(self, value):
825
+ t = type(value)
826
+ if t is bool:
827
+ return value
828
+ if t is str:
829
+ v = value.strip().lower()
830
+ return v == 'true' or v == '1'
831
+ return bool(value)
832
+
833
+ def __nonzero__(self):
834
+ if not self.hasValue():
835
+ return False
836
+ return self.get()
837
+
838
+ def __bool__(self):
839
+ return self.get()
840
+
841
+
842
+ class Pointer(Object):
843
+ """Reference object to other one"""
844
+ EXTENDED_ATTR = '__attribute__'
845
+ EXTENDED_ITEMID = '__itemid__'
846
+ _ERRORS = {}
847
+
848
+ def __init__(self, value=None, **kwargs):
849
+ Object.__init__(self, value, objIsPointer=True, **kwargs)
850
+ # The _extended attribute will be used to point to attributes of a
851
+ # pointed object or the id of an item inside a set
852
+ self._extended = String()
853
+
854
+ if 'extended' in kwargs:
855
+ self.setExtended(kwargs.get('extended'))
856
+
857
+ def __str__(self):
858
+ """String representation of a pointer"""
859
+ if self.hasValue():
860
+ className = self.getObjValue().getClassName()
861
+ strId = self.getObjValue().strId()
862
+ return '-> %s (%s)' % (className, strId)
863
+ return '-> None'
864
+
865
+ def __clean(self, extended):
866
+ """ Remove old attributes conventions. """
867
+ # TODO: This replacements are needed now by backward compatibility
868
+ # reasons, when we used __attribute__ and __item__ to mark both cases
869
+ # in a future the following two lines can be removed.
870
+ ext = extended.replace(self.EXTENDED_ATTR, '')
871
+ ext = ext.replace(self.EXTENDED_ITEMID, '')
872
+ return ext
873
+
874
+ def hasValue(self):
875
+ return self._objValue is not None
876
+
877
+ def get(self, default=None):
878
+ """ Get the pointed object.
879
+ By default, all pointers store a "pointed object" value.
880
+ The _extended attribute allows to also point to internal
881
+ attributes or items (in case of sets) of the pointed object.
882
+ """
883
+ extended = self._extended.get()
884
+ if extended:
885
+ ext = self.__clean(extended)
886
+ parts = ext.split('.')
887
+ value = self._objValue
888
+ for p in parts:
889
+ if hasattr(value, "__getitem__") and p.isdigit():
890
+ value = value[int(p)] # item case
891
+ else:
892
+ value = getattr(value, p, None)
893
+ if value is None:
894
+ break
895
+ else:
896
+ value = self._objValue
897
+
898
+ return value
899
+
900
+ def set(self, other, cleanExtended=True):
901
+ """ Set the pointer value but cleaning the extended property. """
902
+ Object.set(self, other)
903
+ # This check is needed because set is call from the Object constructor
904
+ # when this attribute is not setup yet (a dirty patch, I know)
905
+ if cleanExtended and hasattr(self, '_extended'):
906
+ self._extended.set(None)
907
+
908
+ def hasExtended(self):
909
+ return bool(self._extended.get()) # consider empty string as false
910
+
911
+ def getExtended(self):
912
+ return self.__clean(self._extended.get(''))
913
+
914
+ def setExtended(self, attribute):
915
+ """ Set the attribute name of the "pointed object"
916
+ that will be the result of the get() action.
917
+ """
918
+ self._extended.set(attribute)
919
+
920
+ def getExtendedParts(self):
921
+ """ Return the extended components as a list. """
922
+ if self.hasExtended():
923
+ return self.getExtended().split('.')
924
+ else:
925
+ return []
926
+
927
+ def setExtendedParts(self, parts):
928
+ """ Set the extended attribute but using
929
+ a list as input.
930
+ """
931
+ self.setExtended('.'.join(parts))
932
+
933
+ def addExtended(self, attribute):
934
+ """ Similar to setExtended, but concatenating more extensions
935
+ instead of replacing the previous value.
936
+ """
937
+ if self.hasExtended():
938
+ self._extended.set('%s.%s' % (self._extended.get(), attribute))
939
+ else:
940
+ self.setExtended(attribute)
941
+
942
+ def removeExtended(self):
943
+ """ Remove the last part of the extended attribute. """
944
+ if self.hasExtended():
945
+ parts = self.getExtendedParts()
946
+ self.setExtendedParts(parts[:-1])
947
+
948
+ def getAttributes(self):
949
+ yield '_extended', getattr(self, '_extended')
950
+
951
+ def pointsNone(self):
952
+ return self.get() is None
953
+
954
+ def getUniqueId(self):
955
+ """ Return the unique id concatenating the id
956
+ of the direct pointed object plus the extended
957
+ attribute.
958
+ """
959
+ uniqueId = self.getObjValue().strId()
960
+ if self.hasExtended():
961
+ uniqueId += '.%s' % self.getExtended()
962
+
963
+ return uniqueId
964
+
965
+
966
+ class List(Object, list):
967
+ ITEM_PREFIX = '__item__'
968
+
969
+ """Class to store a list of objects"""
970
+ def __init__(self, value=None, **kwargs):
971
+ list.__init__(self)
972
+ Object.__init__(self, value, **kwargs)
973
+
974
+ def __getattr__(self, name):
975
+ if name.startswith(self.ITEM_PREFIX):
976
+ i = self._stringToIndex(name)
977
+ if i < len(self):
978
+ return self[i]
979
+ raise AttributeError("List object has not attribute: " + name)
980
+
981
+ def __setattr__(self, name, value):
982
+ if name.startswith('__item__') or len(name) == 0:
983
+ self.append(value)
984
+ else:
985
+ object.__setattr__(self, name, value)
986
+
987
+ def getAttributes(self):
988
+ # First yield all attributes not contained in the list
989
+ for name, attr in Object.getAttributes(self):
990
+ yield name, attr
991
+ # Now yield elements contained in the list
992
+ for i, item in enumerate(self):
993
+ yield self._indexToString(i), item
994
+
995
+ def _indexToString(self, i):
996
+ """Return the way the string index is generated.
997
+ String indexes will start in 1, that's why i+1
998
+ """
999
+ return "%s%06d" % (self.ITEM_PREFIX, i+1)
1000
+
1001
+ def _stringToIndex(self, strIndex):
1002
+ """ From the string index representation obtain the index.
1003
+ For symmetry the number in the index string will be
1004
+ decreased in 1.
1005
+ """
1006
+ return int(strIndex.split(self.ITEM_PREFIX)[1]) - 1
1007
+
1008
+ def __len__(self):
1009
+ return list.__len__(self)
1010
+
1011
+ def getSize(self): # Just to have similar API than Set
1012
+ return len(self)
1013
+
1014
+ def isEmpty(self):
1015
+ return len(self) == 0
1016
+
1017
+ def clear(self):
1018
+ del self[:]
1019
+
1020
+ def _convertValue(self, value):
1021
+ """Value should be a list."""
1022
+ if not (isinstance(value, list)):
1023
+ raise Exception("List.set: value should be a list.")
1024
+ self.clear()
1025
+ for item in value:
1026
+ self.append(item)
1027
+ return None
1028
+
1029
+
1030
+ class PointerList(List):
1031
+ def __init__(self, value=None, **kwargs):
1032
+ List.__init__(self, value, **kwargs)
1033
+
1034
+ def append(self, value):
1035
+ """ Append Pointer of objects to the list.
1036
+ If value is a Pointer, just add it to the list.
1037
+ If is another subclass of Object, create
1038
+ a Pointer first and append the pointer.
1039
+ """
1040
+ if isinstance(value, Pointer):
1041
+ pointer = value
1042
+ elif isinstance(value, Object):
1043
+ pointer = Pointer()
1044
+ pointer.set(value)
1045
+ else:
1046
+ raise Exception("Only subclasses of Object can be added to PointerList\n"
1047
+ " Passing value: %s, type: %s" % (value, type(value)))
1048
+
1049
+ List.append(self, pointer)
1050
+
1051
+
1052
+ class CsvList(Scalar, list):
1053
+ """This class will store a list of objects
1054
+ in a single DB row separated by comma.
1055
+ pType: the type of the list elements, int, bool, str"""
1056
+ def __init__(self, pType=str, **kwargs):
1057
+ Scalar.__init__(self, **kwargs)
1058
+ list.__init__(self)
1059
+ self._pType = pType
1060
+
1061
+ def _convertValue(self, value):
1062
+ """ Value should be a str with comma separated values or a list.
1063
+ """
1064
+ self.clear()
1065
+ if value:
1066
+ if isinstance(value, str):
1067
+ for s in value.split(','):
1068
+ self.append(self._pType(s))
1069
+ elif isinstance(value, list) or isinstance(value, tuple):
1070
+ for s in value:
1071
+ self.append(self._pType(s))
1072
+ else:
1073
+ raise Exception("CsvList.set: Invalid value type: ",
1074
+ type(value))
1075
+
1076
+ def equalAttributes(self, other, ignore=[], verbose=False):
1077
+ """Compare that all attributes are equal"""
1078
+
1079
+ return self == other
1080
+
1081
+ def getObjValue(self):
1082
+ self._objValue = ','.join(map(str, self))
1083
+ return self._objValue
1084
+
1085
+ def get(self):
1086
+ return self.getObjValue()
1087
+
1088
+ def __str__(self):
1089
+ return list.__str__(self)
1090
+
1091
+ def isEmpty(self):
1092
+ return len(self) == 0
1093
+
1094
+ def clear(self):
1095
+ del self[:]
1096
+
1097
+ def __eq__(self, other):
1098
+ """ Comparison for scalars should be by value
1099
+ and for other objects by reference.
1100
+ """
1101
+ return all(a == b for a, b in zip(self, other))
1102
+
1103
+
1104
+ class Set(Object):
1105
+ """ This class will be a container implementation for elements.
1106
+ It will use an extra sqlite file to store the elements.
1107
+ All items will have a unique id that identifies each element in the set.
1108
+ """
1109
+ ITEM_TYPE = None # This property should be defined to know the item type
1110
+
1111
+ # This will be used for stream Set where data is populated on the fly
1112
+ STREAM_OPEN = 1
1113
+ STREAM_CLOSED = 2
1114
+
1115
+ indexes = ['_index']
1116
+ # Dict that contains the attributes to be checked for compatibility in operations involving multiple sets,
1117
+ # such us the union of sets. Example: {'sampling rates': 'getSamplingRate', 'dimensions': 'getDimensions'}
1118
+ _compatibilityDict = {}
1119
+
1120
+ def __init__(self, filename=None, prefix='',
1121
+ mapperClass=None, classesDict=None, **kwargs):
1122
+ # Use the object value to store the filename
1123
+ super().__init__(**kwargs)
1124
+ self._mapper = None
1125
+ self._idCount = 0
1126
+ self._size = Integer(0) # cached value of the number of images
1127
+ # It is a bit contradictory that initially a set is Closed
1128
+ # but this is the default behaviour of the Set before Streaming extension
1129
+ self._streamState = Integer(self.STREAM_CLOSED)
1130
+ self.setMapperClass(mapperClass)
1131
+ self._mapperPath = CsvList() # sqlite filename
1132
+ self._representative = None
1133
+ self._classesDict = classesDict
1134
+ self._indexes = kwargs.get('indexes', [])
1135
+ # If filename is passed in the constructor, it means that
1136
+ # we want to create a new object, so we need to delete it if
1137
+ # the file exists
1138
+ if filename:
1139
+ self._mapperPath.set('%s, %s' % (filename, prefix))
1140
+ self.load()
1141
+
1142
+ @classmethod
1143
+ def getCompatibilityDict(cls):
1144
+ return cls._compatibilityDict
1145
+
1146
+ def copy(self, other, copyId=True, ignoreAttrs=['_mapperPath', '_size', '_streamState']):
1147
+ """ Copies the attributes of the set
1148
+
1149
+ :param other: Set to copy attributes from
1150
+ :param copyId: True. Copies the objId.
1151
+ :param ignoreAttrs: Attributes list to ignore while copying. _mapperPath, _size and _streamState
1152
+ are ignored by default.
1153
+
1154
+ """
1155
+
1156
+ # Note: By default this behaves properly for cloning non identical objects.
1157
+ # This is to clone a set from another new set that is based on "other".
1158
+ # For exact clone, we need to pass empty ignoreAttrs. This case happens in Protocol.__tryUpdateOutputSet
1159
+ super().copy(other, copyId, ignoreAttrs)
1160
+
1161
+ def _getMapper(self):
1162
+ """ This method will open the connection to the items
1163
+ database on demand. That's why this method should
1164
+ be used instead of accessing directly _mapper.
1165
+ """
1166
+ if self._mapper is None:
1167
+ self.load()
1168
+ return self._mapper
1169
+
1170
+ def aggregate(self, operations, operationLabel, groupByLabels=None):
1171
+ """
1172
+ This method operate on sets of values. They are used with a
1173
+ GROUP BY clause to group values into subsets
1174
+ :param operations: list of aggregate function such as COUNT, MAX, MIN,...
1175
+ :param operationLabel: label to use by the aggregate function
1176
+ :param groupByLabels: list of labels to group by
1177
+ :return: the aggregated value of each group
1178
+ """
1179
+ return self._getMapper().aggregate(operations, operationLabel, groupByLabels)
1180
+
1181
+ def setMapperClass(self, MapperClass):
1182
+ """ Set the mapper to be used for storage. """
1183
+ if MapperClass is None:
1184
+ from pyworkflow.mapper.sqlite import SqliteFlatMapper
1185
+ MapperClass = SqliteFlatMapper
1186
+ Object.__setattr__(self, '_MapperClass', MapperClass)
1187
+
1188
+ def getItem(self, field, value):
1189
+ """ Alternative to [] to get a single item form the set.
1190
+
1191
+ :param field: attribute of the item to look up for. Should be a unique identifier
1192
+ :param value: value to look for"""
1193
+
1194
+ return self.__getitem__({field:value})
1195
+
1196
+ def __getitem__(self, itemId):
1197
+ """ Get the image with the given id.
1198
+
1199
+ NOTE: Performance warning: this method is expensive in terms of performance use of
1200
+ iterItems is preferred. Use this one only for a single item retrieval.
1201
+
1202
+ :param itemId: the objId (integer) of the item. Alternatively itemId could be a dictionary like
1203
+
1204
+ {_alternative_identifier: 'RG-1234'}
1205
+
1206
+ where '_alternative_identifier' is an attribute of the item and 'RG-1234' the value to look for
1207
+ only the first matching item is returned in case there are more
1208
+
1209
+ """
1210
+ closedMapper = self._mapper is None
1211
+
1212
+ if isinstance(itemId, dict):
1213
+ for obj in self._getMapper().selectBy(**itemId):
1214
+ item = obj
1215
+ break
1216
+ else:
1217
+ item = self._getMapper().selectById(itemId)
1218
+
1219
+ if closedMapper:
1220
+ self.close()
1221
+ return item
1222
+
1223
+ def __contains__(self, itemId):
1224
+ """ element in Set """
1225
+ return self._getMapper().exists(itemId)
1226
+
1227
+ def iterItems(self, orderBy='id', direction='ASC', where=None,
1228
+ limit=None, iterate=True):
1229
+ return self._getMapper().selectAll(orderBy=orderBy,
1230
+ direction=direction,
1231
+ where=where,
1232
+ limit=limit,
1233
+ iterate=iterate) # has flat mapper, iterate is true
1234
+
1235
+ def getFirstItem(self):
1236
+ """ Return the first item in the Set. """
1237
+ # This function is used in many contexts where the mapper can be
1238
+ # left open and could be problematic locking the db for other processes
1239
+ # So, we look if the mapper was closed before, in which case
1240
+ # we will close it after that
1241
+ closedMapper = self._mapper is None
1242
+ firstItem = self._getMapper().selectFirst()
1243
+ if closedMapper:
1244
+ self.close()
1245
+ return firstItem
1246
+
1247
+ def __iter__(self):
1248
+ """ Iterate over the set of images. """
1249
+ return self.iterItems()
1250
+
1251
+ def __len__(self):
1252
+ return self._size.get()
1253
+
1254
+ def getSize(self):
1255
+ """Return the number of images"""
1256
+ return self._size.get()
1257
+
1258
+ def isEmpty(self):
1259
+ return self.getSize() == 0
1260
+
1261
+ def getFileName(self):
1262
+ if len(self._mapperPath):
1263
+ return self._mapperPath[0]
1264
+ return None
1265
+
1266
+ def getPrefix(self):
1267
+ if len(self._mapperPath) > 1:
1268
+ return self._mapperPath[1]
1269
+ return None
1270
+
1271
+ def write(self, properties=True):
1272
+ """
1273
+ Commit the changes made to the Set underlying database.
1274
+
1275
+ :param properties: this flag controls when to write Set attributes to
1276
+ special table 'Properties' in the database. False value is
1277
+ use for example in SetOfClasses for not writing each Class2D
1278
+ properties.
1279
+
1280
+ """
1281
+ if properties:
1282
+ self._getMapper().setProperty('self', self.getClassName())
1283
+ objDict = self.getObjDict()
1284
+ for key, value in objDict.items():
1285
+ self._getMapper().setProperty(key, value)
1286
+ self._getMapper().commit()
1287
+
1288
+ def _loadClassesDict(self):
1289
+ return self._classesDict or globals()
1290
+
1291
+ def setClassesDict(self, classesDict):
1292
+ """ Set the dictionary with classes where to look for classes names. """
1293
+ self._classesDict = classesDict
1294
+
1295
+ def load(self):
1296
+ """ Load extra data from files. """
1297
+ if self._mapperPath.isEmpty():
1298
+ raise Exception("Set.load: mapper path and prefix not set.")
1299
+ fn, prefix = self._mapperPath
1300
+ self._mapper = self._MapperClass(fn, self._loadClassesDict(), prefix, self._indexes)
1301
+ self._size.set(self._mapper.count())
1302
+ self._idCount = self._mapper.maxId()
1303
+
1304
+ def __del__(self):
1305
+ # Close connections to db when destroy this object
1306
+ if self._mapper is not None:
1307
+ self.close()
1308
+
1309
+ def close(self):
1310
+ if self._mapper is not None:
1311
+ self._mapper.close()
1312
+ self._mapper = None
1313
+
1314
+ def clear(self):
1315
+ self._mapper.clear()
1316
+ self._idCount = 0
1317
+ self._size.set(0)
1318
+
1319
+ def append(self, item):
1320
+ """ Add an item to the set.
1321
+ If the item has already an id, use it.
1322
+ If not, keep a counter with the max id
1323
+ and assign the next one.
1324
+ """
1325
+ # The _idCount and _size properties work fine
1326
+ # under the assumption that once a Set is stored,
1327
+ # then it is read-only (no more appends).
1328
+ #
1329
+ # Anyway, this can be easily changed by updating
1330
+ # both from the underlying sqlite when reading a set.
1331
+
1332
+ if not item.hasObjId():
1333
+ self._idCount += 1
1334
+ item.setObjId(self._idCount)
1335
+ else:
1336
+ self._idCount = max(self._idCount, item.getObjId())
1337
+ self._insertItem(item)
1338
+ self._size.increment()
1339
+
1340
+ def _insertItem(self, item):
1341
+ self._getMapper().insert(item)
1342
+
1343
+ def update(self, item):
1344
+ """ Update an existing item. """
1345
+ self._getMapper().update(item)
1346
+
1347
+ def __str__(self):
1348
+ return "%-20s (%d items%s)" % (self.getClassName(), self.getSize(),
1349
+ self._appendStreamState())
1350
+
1351
+ def _appendStreamState(self):
1352
+ return "" if self.isStreamClosed() else ", open set"
1353
+
1354
+ def getSubset(self, n):
1355
+ """ Return a subset of n element, making a clone of each. """
1356
+ subset = []
1357
+ for i, item in enumerate(self):
1358
+ subset.append(item.clone())
1359
+ if i == n:
1360
+ break
1361
+ return subset
1362
+
1363
+ def setRepresentative(self, representative):
1364
+ self._representative = representative
1365
+
1366
+ def getRepresentative(self):
1367
+ return self._representative
1368
+
1369
+ def hasRepresentative(self):
1370
+ """ Return true if have a representative image. """
1371
+ return self._representative is not None
1372
+
1373
+ def equalItemAttributes(self, other, ignore=[], verbose=False):
1374
+ """Compare that all items in self and other
1375
+ return True for equalAttributes.
1376
+ """
1377
+ return all(x.getObjId() == y.getObjId() and
1378
+ x.equalAttributes(y, ignore=ignore, verbose=verbose)
1379
+ for x, y in zip(self, other))
1380
+
1381
+ def hasProperty(self, key):
1382
+ return self._getMapper().hasProperty(key)
1383
+
1384
+ def getProperty(self, key, defaultValue=None):
1385
+ return self._getMapper().getProperty(key, defaultValue)
1386
+
1387
+ def loadProperty(self, propertyName, defaultValue=None):
1388
+ """ Get the value of a property and
1389
+ set its value as an object attribute.
1390
+ """
1391
+ try:
1392
+ self.setAttributeValue(propertyName, self.getProperty(propertyName, defaultValue))
1393
+ except Exception as e:
1394
+ # There could be an exception with "extra" set properties. We tolerate it but warn the developers
1395
+ # This happens in streaming scenarios where properties are only coming from the set sqlite.
1396
+ logger.warning("DEVELOPERS: %s property could not be loaded from the disk. "
1397
+ "Is the property an extended property?. It is lost from now on." % propertyName)
1398
+ def loadAllProperties(self):
1399
+ """ Retrieve all properties stored by the mapper. """
1400
+ for key in self._getMapper().getPropertyKeys():
1401
+ if key != 'self':
1402
+ self.loadProperty(key)
1403
+
1404
+ def getIdSet(self):
1405
+ """ Return a Python set object containing all ids. """
1406
+ return set(self.getUniqueValues('id'))
1407
+
1408
+ def getFiles(self):
1409
+ files = set()
1410
+ if self.getFileName():
1411
+ files.add(self.getFileName())
1412
+ return files
1413
+
1414
+ def getStreamState(self):
1415
+ return self._streamState.get()
1416
+
1417
+ def setStreamState(self, newState):
1418
+ self._streamState.set(newState)
1419
+
1420
+ def isStreamOpen(self):
1421
+ return self.getStreamState() == self.STREAM_OPEN
1422
+
1423
+ def isStreamClosed(self):
1424
+ return self.getStreamState() == self.STREAM_CLOSED
1425
+
1426
+ def enableAppend(self):
1427
+ """ By default, when a Set is loaded, it is opened
1428
+ in read-only mode, so no new insertions are allowed.
1429
+ This function will allow to append more items
1430
+ to an existing set.
1431
+ """
1432
+ self._getMapper().enableAppend()
1433
+
1434
+
1435
+
1436
+
1437
+ # ******* Streaming helpers to deal with sets **********
1438
+ def hasChangedSince(self, time):
1439
+ """ Returns if the set has changed since the timestamp passed as parameter. It will check
1440
+ the last modified time of the file this set uses to persists.
1441
+
1442
+ :parameter time: timestamp to compare to the last modification time """
1443
+
1444
+ if time is None:
1445
+ return True
1446
+
1447
+ # Get the file name
1448
+ localFile = self.getFileName()
1449
+
1450
+ #self.lastCheck = getattr(self, 'lastCheck', now)
1451
+ # Get the last time it was modified
1452
+ modTime = dt.datetime.fromtimestamp(getmtime(localFile))
1453
+ return time < modTime
1454
+
1455
+ def getUniqueValues(self, attributes, where=None):
1456
+ """ Returns a list (for a single label) or a dictionary with unique values for the passed labels.
1457
+ If more than one label is passed it will be unique rows similar ti SQL unique clause.
1458
+
1459
+ :param attributes (string or list) item attribute/s to retrieve unique row values
1460
+ :param where (string) condition to filter the results"""
1461
+
1462
+ return self._getMapper().unique(attributes, where)
1463
+
1464
+ def fmtDate(self, date):
1465
+ """ Formats a python date to a valid string for the mapper"""
1466
+ return self._getMapper().fmtDate(date)
1467
+
1468
+ @staticmethod
1469
+ def isItemEnabled(item):
1470
+ """ Returns if the item is enabled...to be used as a callback. In some other cases (new user subsets)
1471
+ this method will be replaced"""
1472
+
1473
+ return item.isEnabled()
1474
+
1475
+ def ObjectWrap(value):
1476
+ """This function will act as a simple Factory
1477
+ to create objects from Python basic types"""
1478
+ t = type(value)
1479
+ if issubclass(t, Object):
1480
+ return value
1481
+ if t is int:
1482
+ return Integer(value)
1483
+ if t is bool:
1484
+ return Boolean(value)
1485
+ if t is float:
1486
+ return Float(value)
1487
+ if t is list:
1488
+ o = CsvList()
1489
+ o.set(value)
1490
+ return o
1491
+ if t is None:
1492
+ return None
1493
+ # If it is str, unicode or unknown type, convert to string
1494
+ return String(value)
1495
+
1496
+
1497
+ class Dict(dict):
1498
+ """ Simple wrapper around dict class to have a default value. """
1499
+ def __init__(self, default=None):
1500
+ self._default = default
1501
+ dict.__init__(self)
1502
+
1503
+ def __getitem__(self, key):
1504
+ """ Get the image with the given id. """
1505
+ return self.get(key, self._default)
1506
+
1507
+ def __contains__(self, item):
1508
+ return True
1509
+
1510
+
1511
+ # Define a global dict with all basic defined objects
1512
+ OBJECTS_DICT = getSubclasses(Object, globals())