forked from Sage-Bionetworks/synapsePythonClient
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathannotations.py
More file actions
281 lines (225 loc) · 12.3 KB
/
Copy pathannotations.py
File metadata and controls
281 lines (225 loc) · 12.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
"""
***********
Annotations
***********
Annotations are arbitrary metadata attached to Synapse entities. They can be
accessed like ordinary object properties or like dictionary keys::
entity.my_annotation = 'This is one way to do it'
entity['other_annotation'] = 'This is another'
Annotations can be given in the constructor for Synapse Entities::
entity = File('data.xyz', parent=my_project, rating=9.1234)
Annotate the entity with location data::
entity.lat_long = [47.627477, -122.332154]
Record when we collected the data::
from datetime import datetime as Datetime
entity.collection_date = Datetime.now()
See:
- :py:meth:`synapseclient.Synapse.getAnnotations`
- :py:meth:`synapseclient.Synapse.setAnnotations`
~~~~~~~~~~~~~~~~~~~~~~~
Annotating data sources
~~~~~~~~~~~~~~~~~~~~~~~
Data sources are best recorded using Synapse's `provenance <Activity.html>`_ tools.
~~~~~~~~~~~~~~~~~~~~~~
Implementation details
~~~~~~~~~~~~~~~~~~~~~~
In Synapse, entities have both properties and annotations. Properties are used by
the system, whereas annotations are completely user defined. In the Python client,
we try to present this situation as a normal object, with one set of properties.
For more on the implementation and a few gotchas, see the documentation on
:py:mod:`synapseclient.entity`.
See also:
- :py:class:`synapseclient.entity.Entity`
- :py:mod:`synapseclient.entity`
"""
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
from builtins import str
from builtins import int
import six
import collections
import warnings
from .utils import to_unix_epoch_time, from_unix_epoch_time, _is_date, _to_list
from .exceptions import SynapseError
def is_synapse_annotations(annotations):
"""Tests if the given object is a Synapse-style Annotations object."""
keys=['id', 'etag', 'creationDate', 'uri', 'stringAnnotations','longAnnotations','doubleAnnotations','dateAnnotations', 'blobAnnotations']
if not isinstance(annotations, collections.Mapping): return False
return all([key in keys for key in annotations.keys()])
def to_synapse_annotations(annotations):
"""Transforms a simple flat dictionary to a Synapse-style Annotation object."""
if is_synapse_annotations(annotations):
return annotations
synapseAnnos = {}
for key in Annotations.system_properties:
if hasattr(annotations, key):
synapseAnnos[key] = getattr(annotations, key)
for key, value in six.iteritems(annotations):
if key in ['id', 'etag', 'blobAnnotations', 'creationDate', 'uri']:
synapseAnnos[key] = value
elif key in ['stringAnnotations','longAnnotations','doubleAnnotations','dateAnnotations'] and isinstance(value, collections.Mapping):
synapseAnnos.setdefault(key, {}).update({k:_to_list(v) for k,v in six.iteritems(value)})
else:
elements = _to_list(value)
if all((isinstance(elem, six.string_types) for elem in elements)):
synapseAnnos.setdefault('stringAnnotations', {})[key] = elements
elif all((isinstance(elem, bool) for elem in elements)):
synapseAnnos.setdefault('stringAnnotations', {})[key] = [str(element).lower() for element in elements]
elif all((isinstance(elem, int) for elem in elements)):
synapseAnnos.setdefault('longAnnotations', {})[key] = elements
elif all((isinstance(elem, float) for elem in elements)):
synapseAnnos.setdefault('doubleAnnotations', {})[key] = elements
elif all((_is_date(elem) for elem in elements)):
synapseAnnos.setdefault('dateAnnotations', {})[key] = [to_unix_epoch_time(elem) for elem in elements]
## TODO: support blob annotations
# elif all((isinstance(elem, ???) for elem in elements)):
# synapseAnnos.setdefault('blobAnnotations', {})[key] = [???(elem) for elem in elements]
else:
synapseAnnos.setdefault('stringAnnotations', {})[key] = [str(elem) for elem in elements]
return synapseAnnos
def from_synapse_annotations(annotations):
"""Transforms a Synapse-style Annotation object to a simple flat dictionary."""
def process_user_defined_annotations(kvps, annos, func):
"""
for each annotation of a given class (date, string, double, ...), process the
annotation with the given function and add it to the dict 'annos'.
"""
for k,v in six.iteritems(kvps):
## don't overwrite system keys which won't be lists
if k in Annotations.system_properties:
warnings.warn('A user defined annotation, "%s", has the same name as a system defined annotation and will be dropped. Try syn._getRawAnnotations to get annotations in native Synapse format.' % k)
else:
annos.setdefault(k,[]).extend([func(elem) for elem in v])
# Flatten the raw annotations to consolidate doubleAnnotations, longAnnotations,
# stringAnnotations and dateAnnotations into one dictionary
annos = Annotations()
for key, value in annotations.items():
if key in Annotations.system_properties:
setattr(annos, key, value)
elif key=='dateAnnotations':
process_user_defined_annotations(value, annos, lambda x: from_unix_epoch_time(float(x)))
elif key in ['stringAnnotations','longAnnotations']:
process_user_defined_annotations(value, annos, lambda x: x)
elif key == 'doubleAnnotations':
process_user_defined_annotations(value, annos, lambda x: float(x))
elif key=='blobAnnotations':
process_user_defined_annotations(value, annos, lambda x: x)
else:
warnings.warn('Unknown key in annotations response: %s' % key)
return annos
def is_submission_status_annotations(annotations):
"""Tests if the given dictionary is in the form of annotations to submission status"""
keys = ['objectId', 'scopeId', 'stringAnnos','longAnnos','doubleAnnos']
if not isinstance(annotations, collections.Mapping): return False
return all([key in keys for key in annotations.keys()])
def to_submission_status_annotations(annotations, is_private=True):
"""
Converts a normal dictionary to the format used to annotate submission
statuses, which is different from the format used to annotate entities.
:param annotations: A normal Python dictionary whose values are strings, floats, ints or doubles
:param is_private: Set privacy on all annotations at once. These can be set individually using :py:func:`set_privacy`.
Example::
from synapseclient.annotations import to_submission_status_annotations, from_submission_status_annotations
from datetime import datetime as Datetime
## create a submission and get its status
submission = syn.submit(evaluation, 'syn11111111')
submission_status = syn.getSubmissionStatus(submission)
## add annotations
submission_status.annotations = {'foo':'bar', 'shoe_size':12, 'IQ':12, 'timestamp':Datetime.now()}
## convert annotations
submission_status.annotations = to_submission_status_annotations(submission_status.annotations)
submission_status = syn.store(submission_status)
Synapse categorizes these annotations by: stringAnnos, doubleAnnos,
longAnnos. If date or blob annotations are supported, they are not
`documented <http://rest.synapse.org/org/sagebionetworks/repo/model/annotation/Annotations.html>`_
"""
if is_submission_status_annotations(annotations):
return annotations
synapseAnnos = {}
for key, value in six.iteritems(annotations):
if key in ['objectId', 'scopeId', 'stringAnnos','longAnnos','doubleAnnos']:
synapseAnnos[key] = value
elif isinstance(value, bool):
synapseAnnos.setdefault('stringAnnos', []).append({ 'key':key, 'value':str(value).lower(), 'isPrivate':is_private })
elif isinstance(value, int):
synapseAnnos.setdefault('longAnnos', []).append({ 'key':key, 'value':value, 'isPrivate':is_private })
elif isinstance(value, float):
synapseAnnos.setdefault('doubleAnnos', []).append({ 'key':key, 'value':value, 'isPrivate':is_private })
elif isinstance(value, six.string_types):
synapseAnnos.setdefault('stringAnnos', []).append({ 'key':key, 'value':value, 'isPrivate':is_private })
elif _is_date(value):
synapseAnnos.setdefault('longAnnos', []).append({ 'key':key, 'value':to_unix_epoch_time(value), 'isPrivate':is_private })
else:
synapseAnnos.setdefault('stringAnnos', []).append({ 'key':key, 'value':str(value), 'isPrivate':is_private })
return synapseAnnos
## TODO: this should accept a status object and return its annotations or an empty dict if there are none
def from_submission_status_annotations(annotations):
"""
Convert back from submission status annotation format to a normal dictionary.
Example::
submission_status.annotations = from_submission_status_annotations(submission_status.annotations)
"""
dictionary = {}
for key, value in six.iteritems(annotations):
if key in ['stringAnnos','longAnnos']:
dictionary.update( { kvp['key']:kvp['value'] for kvp in value } )
elif key == 'doubleAnnos':
dictionary.update( { kvp['key']:float(kvp['value']) for kvp in value } )
else:
dictionary[key] = value
return dictionary
def set_privacy(annotations, key, is_private=True, value_types=['longAnnos', 'doubleAnnos', 'stringAnnos']):
"""
Set privacy of individual annotations, where annotations are in the format used by Synapse
SubmissionStatus objects. See the `Annotations documentation <http://rest.synapse.org/org/sagebionetworks/repo/model/annotation/Annotations.html>`_
and the docs regarding `querying annotations <http://rest.synapse.org/GET/evaluation/submission/query.html>`_.
:param annotations: Annotations that have already been converted to Synapse format using
:py:func:`to_submission_status_annotations`.
:param key: The key of the annotation whose privacy we're setting.
:param is_private: If False, the annotation will be visible to users with READ permission on the evaluation.
If True, the it will be visible only to users with READ_PRIVATE_SUBMISSION on the evaluation.
Note: Is this really correct???
:param value_types: A list of the value types in which to search for the key. Defaults to all types
['longAnnos', 'doubleAnnos', 'stringAnnos'].
"""
for value_type in value_types:
kvps = annotations.get(value_type, None)
if kvps:
for kvp in kvps:
if kvp['key'] == key:
kvp['isPrivate'] = is_private
return kvp
raise KeyError('The key "%s" couldn\'t be found in the annotations.' % key)
class Annotations(dict):
"""
Represent Synapse Entity annotations as a flat dictionary with the system
assigned properties id, etag, creationDate and uri as object attributes.
"""
system_properties = ['id', 'etag', 'creationDate', 'uri']
def __init__(self, *args, **kwargs):
"""
Create an Annotations object taking key value pairs from a dictionary or
from keyword arguments. System properties id, etag, creationDate and uri
become attributes of the object.
"""
## make sure all system properties exist
for key in Annotations.system_properties:
self.__dict__[key] = None
for arg in args + (kwargs,):
if isinstance(arg, collections.Mapping):
for key in arg:
if key in Annotations.system_properties:
self.__dict__[key] = arg[key]
else:
self.__setitem__(key, arg[key])
else:
raise ValueError("Unrecognized argument to constructor of Annotations: %s" + str(arg))
def __getattr__(self, key):
return self[key]
def __setattr__(self, key, value):
if hasattr(self,key):
return super(Annotations, self).__setattr__(key, value)
else:
return self.__setitem__(key, value)