models.py 10.4 KB
Newer Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# Copyright 2018 Markus Scheidgen
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#   http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an"AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""
All the API flask restplus models.
"""

19
from typing import Set
20
21
22
23
24
25
from flask_restplus import fields
import datetime
import math

from nomad import config
from nomad.app.utils import RFC3339DateTime
26
from nomad.datamodel import CalcWithMetadata
27
28
29
30
31
32

from .api import api, base_url, url


# TODO error/warning objects

33
34
json_api_meta_object_model = api.model('MetaObject', {
    'query': fields.Nested(model=api.model('Query', {
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
        'representation': fields.String(
            required=True,
            description='A string with the part of the URL following the base URL')
    }, description='Information on the query that was requested.')),

    'api_version': fields.String(
        required=True,
        description='A string containing the version of the API implementation.'),

    'time_stamp': RFC3339DateTime(
        required=True,
        description='A timestamp containing the date and time at which the query was executed.'),

    'data_returned': fields.Integer(
        required=True,
        description='An integer containing the number of data objects returned for the query.'),

    'more_data_available': fields.Boolean(
        required=True,
        description='False if all data for this query has been returned, and true if not.'),

    'provider': fields.Nested(
        required=True, skip_none=True,
        description='Information on the database provider of the implementation.',
59
        model=api.model('Provider', {
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
            'name': fields.String(
                required=True,
                description='A short name for the database provider.'),
            'description': fields.String(
                required=True,
                description='A longer description of the database provider.'),
            'prefix': fields.String(
                required=True,
                description='Database-provider-specific prefix.'),
            'homepage': fields.String(
                required=False,
                description='Homepage of the database provider'),
            'index_base_url': fields.String(
                required=False,
                description='Base URL for the index meta-database.')
        })),

    'data_available': fields.Integer(
        required=False,
        description=('An integer containing the total number of data objects available in '
                     'the database.')),

    'last_id': fields.String(
        required=False,
        description='A string containing the last ID returned'),

    'response_message': fields.String(
        required=False,
        description='Response string from the server.'),

    'implementation': fields.Nested(
        required=False, skip_none=True,
        description='Server implementation details.',
93
        model=api.model('Implementation', {
94
95
96
97
98
99
100
101
102
            'name': fields.String(
                description='Name of the implementation'),
            'version': fields.String(
                description='Version string of the current implementation.'),
            'source_url': fields.String(
                description=' URL of the implementation source, either downloadable archive or version control system.'),
            'maintainer': fields.Nested(
                skip_none=True,
                description='Details about the maintainer of the implementation',
103
                model=api.model('Maintainer', {
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
                    'email': fields.String()
                })
            )
        }))
})


class Meta():

    def __init__(self, query: str, returned: int, available: int = None, last_id: str = None):
        self.query = dict(representation=query)
        self.api_version = '0.10.0'
        self.time_stamp = datetime.datetime.now()
        self.data_returned = returned
        self.more_data_available = available > returned if available is not None else False
        self.provider = dict(
            name='NOMAD',
            description='The NOvel MAterials Discovery project and database.',
            prefix='nomad',
            homepage='https//nomad-coe.eu',
            index_base_url=base_url
        )

        self.data_available = available
        self.last_id = last_id
        self.implementation = dict(
            name='nomad@fairdi',
            version=config.version,
            source_url='https://gitlab.mpcdf.mpg.de/nomad-lab/nomad-FAIR',
            maintainer=dict(email='markus.scheidgen@physik.hu-berlin.de'))


136
json_api_links_model = api.model('Links', {
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
    'base_url': fields.String(
        description='The base URL of the implementation'),

    'next': fields.String(
        description=('A link to fetch the next set of results. When the current response '
                     'is the last page of data, this field MUST be either omitted or null.')),

    'prev': fields.String(
        description=('The previous page of data. null or omitted when the current response '
                     'is the first page of data.')),

    'last': fields.String(
        description='The last page of data.'),

    'first': fields.String(
        description='The first page of data.')

})


def Links(endpoint: str, available: int, page_number: int, page_limit: int, **kwargs):
    last_page = math.ceil(available / page_limit)

    rest = dict(page_limit=page_limit)
    rest.update(**{key: value for key, value in kwargs.items() if value is not None})

    result = dict(
        base_url=url(),
        first=url(endpoint, page_number=1, **rest),
        last=url(endpoint, page_number=last_page, **rest))

    if page_number > 1:
        result['prev'] = url(endpoint, page_number=page_number - 1, **rest)
    if page_number * page_limit < available:
        result['next'] = url(endpoint, page_number=page_number + 1, **rest)

    return result


176
json_api_response_model = api.model('Response', {
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
    'links': fields.Nested(
        required=False,
        description='Links object with pagination links.',
        skip_none=True,
        model=json_api_links_model),

    'meta': fields.Nested(
        required=True,
        description='JSON API meta object.',
        model=json_api_meta_object_model),

    'included': fields.List(
        fields.Arbitrary(),
        required=False,
        description=('A list of JSON API resource objects related to the primary data '
                     'contained in data. Responses that contain related resources under '
                     'included are known as compound documents in the JSON API.'))
})

196
json_api_data_object_model = api.model('DataObject', {
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
    'type': fields.String(
        description='The type of the object [structure or calculations].'),

    'id': fields.String(
        description='The id of the object.'),

    'immutable_id': fields.String(
        description='The entries immutable id.'),

    'last_modified': RFC3339DateTime(
        description='Date and time representing when the entry was last modified.'),

    'attributes': fields.Raw(
        description='A dictionary, containing key-value pairs representing the entries properties')

    # further optional fields: links, meta, relationships
})


class CalculationDataObject:
217
218
219
220
221
222
223
224
225
226
227
228
    def __init__(self, calc: CalcWithMetadata, request_fields: Set[str] = None):

        def include(key):
            if request_fields is None or \
                    (key == 'optimade' and key in request_fields) or \
                    (key != 'optimade' and '_nomad_%s' % key in request_fields):

                return True

            return False

        attrs = {key: value for key, value in calc['optimade'].items() if include(key)}
229
230

        self.type = 'calculation'
231
232
233
        self.id = calc.calc_id
        self.immutable_id = calc.calc_id
        self.last_modified = calc.last_processing if calc.last_processing is not None else calc.upload_time
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
        self.attributes = attrs


class Property:
    @staticmethod
    def from_nomad_to_optimade(name: str):
        if name.startswith('optimade.'):
            return name[9:]
        else:
            return '_nomad_%s' % name

    @staticmethod
    def from_optimade_to_nomad(name: str):
        if name.startswith('_nomad_'):
            return name[7:]
        else:
            return 'optimade.%s' % name


json_api_single_response_model = api.inherit(
254
    'SingleResponse', json_api_response_model, {
255
256
257
258
259
260
261
        'data': fields.Nested(
            model=json_api_data_object_model,
            required=True,
            description=('The returned response object.'))
    })

json_api_list_response_model = api.inherit(
262
    'SingleResponse', json_api_response_model, {
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
        'data': fields.List(
            fields.Nested(json_api_data_object_model),
            required=True,
            description=('The list of returned response objects.'))
    })


base_endpoint_parser = api.parser()
base_endpoint_parser.add_argument(
    'response_format', type=str,
    help=('The output format requested. Defaults to the format string "json", which '
          'specifies the standard output format described in this specification.'))
base_endpoint_parser.add_argument(
    'email_address', type=str,
    help=('An email address of the user making the request. The email SHOULD be that of a '
          'person and not an automatic system.'))
base_endpoint_parser.add_argument(
    'response_fields', action='split', type=str,
    help=('A comma-delimited set of fields to be provided in the output. If provided, only '
          'these fields MUST be returned and no others.'))

entry_listing_endpoint_parser = base_endpoint_parser.copy()
entry_listing_endpoint_parser.add_argument(
    'filter', type=str, help='An optimade filter string.')
entry_listing_endpoint_parser.add_argument(
    'page_limit', type=int,
    help='Sets a numerical limit on the number of entries returned.')
entry_listing_endpoint_parser.add_argument(
    'page_number', type=int,
    help='Sets the page number to return.')
entry_listing_endpoint_parser.add_argument(
    'sort', type=str,
    help='Name of the property to sort the results by.')

single_entry_endpoint_parser = base_endpoint_parser.copy()