data_types.py 9.56 KB
Newer Older
1
2
3
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

Jacek Lebioda's avatar
Jacek Lebioda committed
4
"""
5
6
Functions, which create all the data types specified by Beacon's API v3.
The basic idea is to pass all the required values in **kwargs by their names.
Jacek Lebioda's avatar
Jacek Lebioda committed
7
8
"""

9
import copy
Jacek Lebioda's avatar
Jacek Lebioda committed
10
11
12
import re

from beacon import helpers
13

Jacek Lebioda's avatar
Jacek Lebioda committed
14

Jacek Lebioda's avatar
Jacek Lebioda committed
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
class APIArgumentError(ValueError):
    """
    Exception thrown if there's something wrong with the argument
    provided to API query
    """
    pass


def create_beacon_dataset(**kwargs):
    """
    Creates an object which describes a dataset
    Needs the following keys in `**kwargs`:
      'id', 'name', 'assemblyId', 'createDateTime', 'updateDateTime'
    Optional keys in `**kwargs`:
      'description', 'version', 'variantCount', 'callCount', 'sampleCount'
      'externalUrl', 'info'
    """

    fields_required = ['id', 'name', 'assemblyId', 'createDateTime',
                       'updateDateTime']
    fields_optional = ['description', 'version', 'variantCount', 'callCount',
                       'sampleCount', 'externalUrl', 'info']

    return populate_fields({}, fields_required, fields_optional, **kwargs)


def validate_input(**kwargs):
    """
    Validates query parameters. If there's a violation,
    an `APIArgumentError` exception is raised
    """
    def wrap(name, message, regexp):
        re_regexp = re.compile(regexp)
        if not helpers.check_input(kwargs, name, re_regexp):
            arg = kwargs.get(name) if kwargs.get(name) is not None else '<<None>>'
            raise APIArgumentError("Invalid {0} ({1}) - {2}".format(name, arg, message))

    wrap('referenceName',
         'accepted ones are: 1-22, X, Y, M, MT',
         r'^([1-9]|1\d|2[012]|[Xx]|[Yy]|[Mm][Tt]?)$')

    wrap('start',
         'should be a positive number',
         r'^\d+$')

    wrap('assemblyId',
         'accepted ones are: GRCh38, GRCh37 and NCBI36',
         r'^([Gg][Rr][Cc][Hh](38|37)|[Nn][Cc][Bb][Ii]36)$')

    wrap('referenceBases',
         'should contain only A, C, T, G, D',
         r'^[AaCcTtGgDd]*$')

    wrap('alternateBases',
         'should contain only A, C, T, G, D',
         r'^[AaCcTtGgDd]*$')

Jacek Lebioda's avatar
Jacek Lebioda committed
72
    whitelist = [True, False, 'true', 'false', 'True', 'False']
Jacek Lebioda's avatar
Jacek Lebioda committed
73
74
75
76
77
78
79
80
    if 'includeDatasetResponses' in kwargs and kwargs['includeDatasetResponses'] not in whitelist:
        raise APIArgumentError("Invalid {0} ({1}) - {2}".format("includeDatasetResponses",
                                                                kwargs['includeDatasetResponses'],
                                                                'a boolean was expected'))

    return True


Jacek Lebioda's avatar
Jacek Lebioda committed
81
def inject_required(what_to_inject, from_what, to_what):
Jacek Lebioda's avatar
Jacek Lebioda committed
82
    """
Jacek Lebioda's avatar
Jacek Lebioda committed
83
84
    Extends the object `to_what` with `what_to_inject`.
    `what_to_inject` must be present in `from_what`.
85
86
87
88
89
90
91
92
    Throws an exception when the key is missing.
    Warning when dealing with mutable data structures
    (`inject_required` does not use deep copies).

    :param what_to_inject: The key of value to copy into `to_what`
    :param from_what: The dict which should contain `what_to_inject` key
    :param to_what: The output dict - the result will be here
    :return: nothing (to avoid confusion with `to_what` parameter)
Jacek Lebioda's avatar
Jacek Lebioda committed
93
    """
94

Jacek Lebioda's avatar
Jacek Lebioda committed
95
96
    if what_to_inject in from_what:
        to_what[what_to_inject] = from_what[what_to_inject]
Jacek Lebioda's avatar
Jacek Lebioda committed
97
    else:
98
99
100
        message = "`inject_required failed`, key {0} is marked as required" \
                  " and is not present in {1}".format(what_to_inject,
                                                      from_what)
101
        raise TypeError(message)
Jacek Lebioda's avatar
Jacek Lebioda committed
102
103
104
105
106
    return to_what


def inject_optional(what_to_inject, from_what, to_what):
    """
Jacek Lebioda's avatar
Jacek Lebioda committed
107
108
    Extends the object `to_what` with `what_to_inject`
    if `what_to_inject` is present in `from_what`.
109
110
111
112
113
114
115
    Warning when dealing with mutable data structures
    (`inject_optional` does not use deep copies).

    :param what_to_inject: The key of value to copy into `to_what`
    :param from_what: The dict which should contain `what_to_inject` key
    :param to_what: The output dict - the result will be here
    :return: nothing (to avoid confusion with `to_what` parameter)
Jacek Lebioda's avatar
Jacek Lebioda committed
116
    """
117

Jacek Lebioda's avatar
Jacek Lebioda committed
118
119
120
121
    if what_to_inject in from_what and from_what[what_to_inject] is not None:
        inject_required(what_to_inject, from_what, to_what)


122
def populate_fields(initial_dict, fields_required, fields_optional,
123
124
125
126
127
128
                    **kwargs):
    """
    Copies dict values (whose keys are in `fields_optional` or
    `fields_required` lists) from **kwargs into a copy
    of `initial_response`

129
130
    :param initial_dict: dict which will be populated with values
                         and returned
131
132
133
134
    :param fields_required: list of keys (strings) which must be present
                            in `**kwargs` and will be copied
    :param fields_optional: list of keys (strings) which can be present
                            in `**kwargs` and will be copied
135
    :param kwargs: dict of values to be used as a source
136
137
138
139
    :return: copy of `initial_response` dict extended by values
             from `**kwargs`
    """

140
    the_response = copy.deepcopy(initial_dict)
141
142
143
144
145
146
147
148
149
150

    for field_required in fields_required:
        inject_required(field_required, kwargs, the_response)

    for field_optional in fields_optional:
        inject_optional(field_optional, kwargs, the_response)

    return the_response


Jacek Lebioda's avatar
Jacek Lebioda committed
151
152
153
def create_organization(**kwargs):
    """
    Creates an object with metadata about organization which
Jacek Lebioda's avatar
Jacek Lebioda committed
154
    operates the beacon. Left as a reference.
155
156
157
158
    Needs the following keys in `**kwargs`:
      'id', 'name'
    Optional keys in `**kwargs`:
      'description', 'address', 'welcomeUrl', 'contactUrl', 'logoUrl', 'info'
Jacek Lebioda's avatar
Jacek Lebioda committed
159
    """
Jacek Lebioda's avatar
Jacek Lebioda committed
160

161
162
163
    fields_required = ['id', 'name']
    fields_optional = ['description', 'address', 'welcomeUrl', 'contactUrl',
                       'logoUrl', 'info']
Jacek Lebioda's avatar
Jacek Lebioda committed
164

165
    return populate_fields({}, fields_required, fields_optional, **kwargs)
Jacek Lebioda's avatar
Jacek Lebioda committed
166
167
168


def create_beacon(**kwargs):
Jacek Lebioda's avatar
Jacek Lebioda committed
169
    """
170
171
172
173
174
175
    Creates an object with Beacon's metadata. Left as a reference.
    Needs the following keys in `**kwargs`:
      'id', 'name', 'apiVersion', 'organization'
    Optional keys in `**kwargs`:
      'description', 'version', 'welcomeUrl', 'info', 'alternativeUrl',
      'createDateTime', 'updateDateTime', 'datasets', 'sampleAlleleRequests'
Jacek Lebioda's avatar
Jacek Lebioda committed
176
    """
Jacek Lebioda's avatar
Jacek Lebioda committed
177

178
179
180
181
    fields_required = ['id', 'name', 'apiVersion', 'organization']
    fields_optional = ['description', 'version', 'welcomeUrl', 'info' 
                       'alternativeUrl', 'createDateTime', 'updateDateTime',
                       'datasets', 'sampleAlleleRequests']
Jacek Lebioda's avatar
Jacek Lebioda committed
182

183
    return populate_fields({}, fields_required, fields_optional, **kwargs)
Jacek Lebioda's avatar
Jacek Lebioda committed
184
185
186


def create_beacon_allele_request(**kwargs):
187
188
189
190
191
192
193
194
    """
    Creates an object to be used as a request
    Needs the following keys in `**kwargs`:
      'referenceName', 'start', 'referenceBases', 'alternateBases',
       'assemblyId'
    Optional keys in `**kwargs`:
      'datasetIds', 'includeDatasetResponses'
    """
Jacek Lebioda's avatar
Jacek Lebioda committed
195

196
197
198
    fields_required = ['referenceName', 'start', 'referenceBases',
                       'alternateBases', 'assemblyId']
    fields_optional = ['datasetIds', 'includeDatasetResponses']
Jacek Lebioda's avatar
Jacek Lebioda committed
199

200
    return populate_fields({}, fields_required, fields_optional, **kwargs)
Jacek Lebioda's avatar
Jacek Lebioda committed
201
202


Jacek Lebioda's avatar
Jacek Lebioda committed
203
def create_beacon_dataset_allele_response_error(**kwargs):
204
205
206
207
208
209
210
211
    """
    Creates an object with error dataset allele response data
    Needs the following keys in `**kwargs`:
      'datasetId', 'error'
    Optional keys in `**kwargs`:
      'exists', 'frequency', 'variantCount', 'callCount',
      'sampleCount', 'note', 'externalUrl', 'info'
    """
Jacek Lebioda's avatar
Jacek Lebioda committed
212

213
214
215
    fields_required = ['datasetId', 'error']
    fields_optional = ['exists', 'frequency', 'variantCount', 'callCount',
                       'sampleCount', 'note', 'externalUrl', 'info']
Jacek Lebioda's avatar
Jacek Lebioda committed
216

217
    return populate_fields({}, fields_required, fields_optional, **kwargs)
Jacek Lebioda's avatar
Jacek Lebioda committed
218
219
220


def create_beacon_dataset_allele_response_ok(**kwargs):
221
222
223
224
225
226
227
228
    """
    Creates an object with correct dataset allele response data
    Needs the following keys in `**kwargs`:
      'datasetId', 'exists'
    Optional keys in `**kwargs`:
      'error', 'frequency', 'variantCount', 'callCount',
      'sampleCount', 'note', 'externalUrl', 'info'
    """
Jacek Lebioda's avatar
Jacek Lebioda committed
229

230
231
232
    fields_required = ['datasetId', 'exists']
    fields_optional = ['error', 'frequency', 'variantCount', 'callCount',
                       'sampleCount', 'note', 'externalUrl', 'info']
Jacek Lebioda's avatar
Jacek Lebioda committed
233

234
    return populate_fields({}, fields_required, fields_optional, **kwargs)
Jacek Lebioda's avatar
Jacek Lebioda committed
235

Jacek Lebioda's avatar
Jacek Lebioda committed
236
237

def create_beacon_allele_response_ok(**kwargs):
238
239
240
241
242
243
244
    """
    Creates an object to be used as a correct query response
    Needs the following keys in `**kwargs`:
      'beaconId', 'exists'
    Optional keys in `**kwargs`:
      'error', 'alleleRequest', 'datasetAlleleResponses'
    """
Jacek Lebioda's avatar
Jacek Lebioda committed
245

246
247
    fields_required = ['beaconId', 'exists']
    fields_optional = ['error', 'alleleRequest', 'datasetAlleleResponses']
Jacek Lebioda's avatar
Jacek Lebioda committed
248

249
    return populate_fields({}, fields_required, fields_optional, **kwargs)
Jacek Lebioda's avatar
Jacek Lebioda committed
250
251
252


def create_beacon_allele_response_error(**kwargs):
253
254
255
256
257
258
259
    """
    Creates an object to be used as a incorrect query response
    Needs the following keys in `**kwargs`:
      'beaconId', 'error'
    Optional keys in `**kwargs`:
      'error', 'alleleRequest', 'datasetAlleleResponses'
    """
Jacek Lebioda's avatar
Jacek Lebioda committed
260

261
262
    fields_required = ['beaconId', 'error']
    fields_optional = ['error', 'alleleRequest', 'datasetAlleleResponses']
Jacek Lebioda's avatar
Jacek Lebioda committed
263

264
    return populate_fields({}, fields_required, fields_optional, **kwargs)
Jacek Lebioda's avatar
Jacek Lebioda committed
265
266


Jacek Lebioda's avatar
Jacek Lebioda committed
267
def create_beacon_error(**kwargs):
268
269
270
271
272
    """
    Creates an object used to inform about errors
    Optional keys in `**kwargs`:
      'message', 'errorCode'
    """
Jacek Lebioda's avatar
Jacek Lebioda committed
273
274
275
    error = {
        'errorCode': '400',
        'message': ''
Jacek Lebioda's avatar
Jacek Lebioda committed
276
277
    }

278
279
    fields_required = []
    fields_optional = ['message', 'errorCode']
Jacek Lebioda's avatar
Jacek Lebioda committed
280

281
    return populate_fields(error, fields_required, fields_optional, **kwargs)