models.py 10.4 KB
Newer Older
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
1
'''
2
Senpy Models.
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
3
4

This implementation should mirror the JSON schema definition.
5
6
For compatibility with Py3 and for easier debugging, this new version drops
introspection and adds all arguments to the models.
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
7
'''
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
8
9
10
from __future__ import print_function
from six import string_types

J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
11
12
import time
import copy
13
14
import json
import os
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
15
16
import jsonref
import jsonschema
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
17

18
from flask import Response as FlaskResponse
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
19
20
21
from pyld import jsonld

from rdflib import Graph
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
22

J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
23
24
25
26
import logging

logger = logging.getLogger(__name__)

J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
27
DEFINITIONS_FILE = 'definitions.json'
J. Fernando Sánchez's avatar
YAPFed    
J. Fernando Sánchez committed
28
29
30
CONTEXT_PATH = os.path.join(
    os.path.dirname(os.path.realpath(__file__)), 'schemas', 'context.jsonld')

31

J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
32
33
34
35
def get_schema_path(schema_file, absolute=False):
    if absolute:
        return os.path.realpath(schema_file)
    else:
J. Fernando Sánchez's avatar
YAPFed    
J. Fernando Sánchez committed
36
37
38
        return os.path.join(
            os.path.dirname(os.path.realpath(__file__)), 'schemas',
            schema_file)
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
39
40
41
42
43


def read_schema(schema_file, absolute=False):
    schema_path = get_schema_path(schema_file, absolute)
    schema_uri = 'file://{}'.format(schema_path)
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
44
45
    with open(schema_path) as f:
        return jsonref.load(f, base_uri=schema_uri)
J. Fernando Sánchez's avatar
YAPFed    
J. Fernando Sánchez committed
46
47


J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
48
49
base_schema = read_schema(DEFINITIONS_FILE)

50

J. Fernando Sánchez's avatar
YAPFed    
J. Fernando Sánchez committed
51
class Context(dict):
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
52
    @staticmethod
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
53
54
55
56
57
    def load(context):
        logging.debug('Loading context: {}'.format(context))
        if not context:
            return context
        elif isinstance(context, list):
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
58
59
            contexts = []
            for c in context:
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
60
                contexts.append(Context.load(c))
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
61
62
            return contexts
        elif isinstance(context, dict):
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
63
            return Context(context)
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
64
        elif isinstance(context, string_types):
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
65
66
            try:
                with open(context) as f:
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
67
                    return Context(json.loads(f.read()))
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
68
69
            except IOError:
                return context
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
70
        else:
J. Fernando Sánchez's avatar
YAPFed    
J. Fernando Sánchez committed
71
72
            raise AttributeError('Please, provide a valid context')

73

J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
74
75
base_context = Context.load(CONTEXT_PATH)

J. Fernando Sánchez's avatar
YAPFed    
J. Fernando Sánchez committed
76

J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
77
class SenpyMixin(object):
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
78
    _context = base_context["@context"]
79

J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
80
81
82
83
84
    def flask(self,
              in_headers=True,
              headers=None,
              outformat='json-ld',
              **kwargs):
85
        """
86
87
88
        Return the values and error to be used in flask.
        So far, it returns a fixed context. We should store/generate different
        contexts if the plugin adds more aliases.
89
        """
90
        headers = headers or {}
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
91
92
93
94
95
96
        kwargs["with_context"] = not in_headers
        content, mimetype = self.serialize(format=outformat,
                                           with_mime=True,
                                           **kwargs)

        if outformat == 'json-ld' and in_headers:
97
            headers.update({
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
98
99
100
101
                "Link":
                ('<%s>;'
                    'rel="http://www.w3.org/ns/json-ld#context";'
                    ' type="application/ld+json"' % kwargs.get('context_uri'))
102
            })
J. Fernando Sánchez's avatar
YAPFed    
J. Fernando Sánchez committed
103
        return FlaskResponse(
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
104
            response=content,
J. Fernando Sánchez's avatar
YAPFed    
J. Fernando Sánchez committed
105
106
            status=getattr(self, "status", 200),
            headers=headers,
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
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
            mimetype=mimetype)

    def serialize(self, format='json-ld', with_mime=False, **kwargs):
        js = self.jsonld(**kwargs)
        if format == 'json-ld':
            content = json.dumps(js, indent=2, sort_keys=True)
            mimetype = "application/json"
        elif format in ['turtle', ]:
            logger.debug(js)
            content = json.dumps(js, indent=2, sort_keys=True)
            g = Graph().parse(
                data=content,
                format='json-ld',
                base=kwargs.get('prefix'),
                context=self._context)
            logger.debug(
                'Parsing with prefix: {}'.format(kwargs.get('prefix')))
            content = g.serialize(format='turtle').decode('utf-8')
            mimetype = 'text/{}'.format(format)
        else:
            raise Error('Unknown outformat: {}'.format(format))
        if with_mime:
            return content, mimetype
        else:
            return content
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
132

J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
133
134
    def serializable(self):
        def ser_or_down(item):
J. Fernando Sánchez's avatar
YAPFed    
J. Fernando Sánchez committed
135
136
137
138
139
140
141
142
143
144
145
146
            if hasattr(item, 'serializable'):
                return item.serializable()
            elif isinstance(item, dict):
                temp = dict()
                for kp in item:
                    vp = item[kp]
                    temp[kp] = ser_or_down(vp)
                return temp
            elif isinstance(item, list):
                return list(ser_or_down(i) for i in item)
            else:
                return item
147

J. Fernando Sánchez's avatar
YAPFed    
J. Fernando Sánchez committed
148
        return ser_or_down(self._plain_dict())
149

J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
150
151
152
153
154
    def jsonld(self,
               with_context=True,
               context_uri=None,
               prefix=None,
               expanded=False):
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
155
        ser = self.serializable()
156

J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
        result = jsonld.compact(
            ser,
            self._context,
            options={
                'base': prefix,
                'expandContext': self._context,
                'senpy': prefix
            })
        if context_uri:
            result['@context'] = context_uri
        if expanded:
            result = jsonld.expand(
                result, options={'base': prefix,
                                 'expandContext': self._context})
        if not with_context:
            del result['@context']
        return result
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
174

J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
175
    def to_JSON(self, *args, **kwargs):
J. Fernando Sánchez's avatar
YAPFed    
J. Fernando Sánchez committed
176
        js = json.dumps(self.jsonld(*args, **kwargs), indent=4, sort_keys=True)
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
177
178
        return js

179
180
181
182
183
184
    def validate(self, obj=None):
        if not obj:
            obj = self
        if hasattr(obj, "jsonld"):
            obj = obj.jsonld()
        jsonschema.validate(obj, self.schema)
J. Fernando Sánchez's avatar
YAPFed    
J. Fernando Sánchez committed
185

186
187
188
    def __str__(self):
        return str(self.to_JSON())

J. Fernando Sánchez's avatar
YAPFed    
J. Fernando Sánchez committed
189

J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
190
class BaseModel(SenpyMixin, dict):
191

J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
192
    schema = base_schema
193

194
    def __init__(self, *args, **kwargs):
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
195
196
197
        if 'id' in kwargs:
            self.id = kwargs.pop('id')
        elif kwargs.pop('_auto_id', True):
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
198
            self.id = '_:{}_{}'.format(type(self).__name__, time.time())
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
199
200
        temp = dict(*args, **kwargs)

J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
201
202
203
        for obj in [
                self.schema,
        ] + self.schema.get('allOf', []):
204
            for k, v in obj.get('properties', {}).items():
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
205
                if 'default' in v and k not in temp:
206
207
                    temp[k] = copy.deepcopy(v['default'])

208
209
210
211
212
        for i in temp:
            nk = self._get_key(i)
            if nk != i:
                temp[nk] = temp[i]
                del temp[i]
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
213
214
215
216
        try:
            temp['@type'] = getattr(self, '@type')
        except AttributeError:
            logger.warn('Creating an instance of an unknown model')
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
217

J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
218
        super(BaseModel, self).__init__(temp)
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
219
220

    def _get_key(self, key):
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
221
222
        if key is 'id':
            key = '@id'
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
223
224
225
        key = key.replace("__", ":", 1)
        return key

J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
226
227
228
    def __getitem__(self, key):
        return dict.__getitem__(self, self._get_key(key))

J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
229
    def __setitem__(self, key, value):
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
230
        dict.__setitem__(self, self._get_key(key), value)
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
231
232
233
234
235
236
237
238
239
240
241
242
243
244

    def __delitem__(self, key):
        dict.__delitem__(self, key)

    def __getattr__(self, key):
        try:
            return self.__getitem__(self._get_key(key))
        except KeyError:
            raise AttributeError(key)

    def __setattr__(self, key, value):
        self.__setitem__(self._get_key(key), value)

    def __delattr__(self, key):
245
246
247
248
        try:
            object.__delattr__(self, key)
        except AttributeError:
            self.__delitem__(self._get_key(key))
J. Fernando Sánchez's avatar
YAPFed    
J. Fernando Sánchez committed
249

J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
250
    def _plain_dict(self):
J. Fernando Sánchez's avatar
YAPFed    
J. Fernando Sánchez committed
251
        d = {k: v for (k, v) in self.items() if k[0] != "_"}
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
252
253
        return d

J. Fernando Sánchez's avatar
YAPFed    
J. Fernando Sánchez committed
254

J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
255
_subtypes = {}
256

J. Fernando Sánchez's avatar
YAPFed    
J. Fernando Sánchez committed
257

J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
258
259
def register(rsubclass, rtype=None):
    _subtypes[rtype or rsubclass.__name__] = rsubclass
260

J. Fernando Sánchez's avatar
YAPFed    
J. Fernando Sánchez committed
261

J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
262
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
def from_dict(indict, cls=None):
    if not cls:
        target = indict.get('@type', None)
        try:
            if target and target in _subtypes:
                cls = _subtypes[target]
            else:
                cls = BaseModel
        except Exception:
            cls = BaseModel
    outdict = dict()
    for k, v in indict.items():
        if k == '@context':
            pass
        elif isinstance(v, dict):
            v = from_dict(indict[k])
        elif isinstance(v, list):
            for ix, v2 in enumerate(v):
                if isinstance(v2, dict):
                    v[ix] = from_dict(v2)
        outdict[k] = v
    return cls(**outdict)


def from_string(string, **kwargs):
    return from_dict(json.loads(string), **kwargs)
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
288
289


290
291
292
293
294
def from_json(injson):
    indict = json.loads(injson)
    return from_dict(indict)


J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
295
296
297
298
def from_schema(name, schema_file=None, base_classes=None):
    base_classes = base_classes or []
    base_classes.append(BaseModel)
    schema_file = schema_file or '{}.json'.format(name)
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
299
    class_name = '{}{}'.format(name[0].upper(), name[1:])
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
300
301
302
    newclass = type(class_name, tuple(base_classes), {})
    setattr(newclass, '@type', name)
    setattr(newclass, 'schema', read_schema(schema_file))
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
303
    setattr(newclass, 'class_name', class_name)
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
304
305
306
307
308
309
310
311
312
313
    register(newclass, name)
    return newclass


def _add_from_schema(*args, **kwargs):
    generatedClass = from_schema(*args, **kwargs)
    globals()[generatedClass.__name__] = generatedClass
    del generatedClass


J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
for i in [
        'analysis',
        'emotion',
        'emotionConversion',
        'emotionConversionPlugin',
        'emotionAnalysis',
        'emotionModel',
        'emotionPlugin',
        'emotionSet',
        'entry',
        'plugin',
        'plugins',
        'response',
        'results',
        'sentiment',
        'sentimentPlugin',
        'suggestion',
]:
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
332
333
334
    _add_from_schema(i)

_ErrorModel = from_schema('error')
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
335
336


J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
337
class Error(SenpyMixin, Exception):
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
338
    def __init__(self, message, *args, **kwargs):
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
339
340
        super(Error, self).__init__(self, message, message)
        self._error = _ErrorModel(message=message, *args, **kwargs)
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
341
342
        self.message = message

343
344
345
346
347
348
349
350
351
    def __getitem__(self, key):
        return self._error[key]

    def __setitem__(self, key, value):
        self._error[key] = value

    def __delitem__(self, key):
        del self._error[key]

J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
352
353
354
355
    def __getattr__(self, key):
        if key != '_error' and hasattr(self._error, key):
            return getattr(self._error, key)
        raise AttributeError(key)
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
356

J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
357
358
359
360
361
362
363
364
365
    def __setattr__(self, key, value):
        if key != '_error':
            return setattr(self._error, key, value)
        else:
            super(Error, self).__setattr__(key, value)

    def __delattr__(self, key):
        delattr(self._error, key)

J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
366
367
368
    def __str__(self):
        return str(self.to_JSON(with_context=False))

J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
369
370

register(Error, 'error')