models.py 8.65 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

J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
20
21
22
23
import logging

logger = logging.getLogger(__name__)

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

28

J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
29
30
31
32
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
33
34
35
        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
36
37
38
39
40


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
41
42
    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
43
44


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

47

J. Fernando Sánchez's avatar
YAPFed    
J. Fernando Sánchez committed
48
class Context(dict):
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
49
    @staticmethod
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
50
51
52
53
54
    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
55
56
            contexts = []
            for c in context:
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
57
                contexts.append(Context.load(c))
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
58
59
            return contexts
        elif isinstance(context, dict):
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
60
            return Context(context)
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
61
        elif isinstance(context, string_types):
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
62
63
            try:
                with open(context) as f:
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
64
                    return Context(json.loads(f.read()))
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
65
66
            except IOError:
                return context
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
67
        else:
J. Fernando Sánchez's avatar
YAPFed    
J. Fernando Sánchez committed
68
69
            raise AttributeError('Please, provide a valid context')

70

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

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

J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
74
class SenpyMixin(object):
75
    context = base_context["@context"]
76

J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
77
    def flask(self, in_headers=True, headers=None, **kwargs):
78
        """
79
80
81
        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.
82
        """
83
84
85
        headers = headers or {}
        kwargs["with_context"] = True
        js = self.jsonld(**kwargs)
86
        if in_headers:
87
88
89
            url = js["@context"]
            del js["@context"]
            headers.update({
90
91
92
                "Link": ('<%s>;'
                         'rel="http://www.w3.org/ns/json-ld#context";'
                         ' type="application/ld+json"' % url)
93
            })
J. Fernando Sánchez's avatar
YAPFed    
J. Fernando Sánchez committed
94
95
96
97
98
99
        return FlaskResponse(
            json.dumps(
                js, indent=2, sort_keys=True),
            status=getattr(self, "status", 200),
            headers=headers,
            mimetype="application/json")
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
100

J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
101
102
    def serializable(self):
        def ser_or_down(item):
J. Fernando Sánchez's avatar
YAPFed    
J. Fernando Sánchez committed
103
104
105
106
107
108
109
110
111
112
113
114
            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
115

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

118
    def jsonld(self, with_context=False, context_uri=None):
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
119
        ser = self.serializable()
120

J. Fernando Sánchez's avatar
YAPFed    
J. Fernando Sánchez committed
121
        if with_context:
122
123
124
125
126
            context = []
            if context_uri:
                context = context_uri
            else:
                context = self.context.copy()
127
            if hasattr(self, 'prefix'):
128
                # This sets @base for the document, which will be used in
129
130
131
                # all relative URIs. For example, if a uri is "Example" and
                # prefix =s "http://example.com", the absolute URI after
                # expanding with JSON-LD will be "http://example.com/Example"
132

133
                prefix_context = {"@base": self.prefix}
134
135
136
137
138
                if isinstance(context, list):
                    context.append(prefix_context)
                else:
                    context = [context, prefix_context]
            ser["@context"] = context
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
139
        return ser
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
140

J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
141
    def to_JSON(self, *args, **kwargs):
J. Fernando Sánchez's avatar
YAPFed    
J. Fernando Sánchez committed
142
        js = json.dumps(self.jsonld(*args, **kwargs), indent=4, sort_keys=True)
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
143
144
        return js

145
146
147
148
149
150
    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
151

152
153
154
    def __str__(self):
        return str(self.to_JSON())

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

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

J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
158
    schema = base_schema
159

160
    def __init__(self, *args, **kwargs):
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
161
162
163
164
165
        if 'id' in kwargs:
            self.id = kwargs.pop('id')
        elif kwargs.pop('_auto_id', True):
            self.id = '_:{}_{}'.format(
                type(self).__name__, time.time())
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
166
167
        temp = dict(*args, **kwargs)

J. Fernando Sánchez's avatar
YAPFed    
J. Fernando Sánchez committed
168
        for obj in [self.schema, ] + self.schema.get('allOf', []):
169
170
171
172
            for k, v in obj.get('properties', {}).items():
                if 'default' in v:
                    temp[k] = copy.deepcopy(v['default'])

173
174
175
176
177
        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
178
179
180
181
        if 'context' in temp:
            context = temp['context']
            del temp['context']
            self.__dict__['context'] = Context.load(context)
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
182
183
184
185
186
        try:
            temp['@type'] = getattr(self, '@type')
        except AttributeError:
            logger.warn('Creating an instance of an unknown model')
        super(BaseModel, self).__init__(temp)
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208

    def _get_key(self, key):
        key = key.replace("__", ":", 1)
        return key

    def __setitem__(self, key, value):
        dict.__setitem__(self, key, value)

    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):
        self.__delitem__(self._get_key(key))
J. Fernando Sánchez's avatar
YAPFed    
J. Fernando Sánchez committed
209

J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
210
    def _plain_dict(self):
J. Fernando Sánchez's avatar
YAPFed    
J. Fernando Sánchez committed
211
        d = {k: v for (k, v) in self.items() if k[0] != "_"}
212
213
        if 'id' in d:
            d["@id"] = d.pop('id')
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
214
215
        return d

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

J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
217
_subtypes = {}
218

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

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

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

J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
224
225
226
227
228
229
230
231
232
def from_dict(indict):
    target = indict.get('@type', None)
    if target and target in _subtypes:
        cls = _subtypes[target]
    else:
        cls = BaseModel
    return cls(**indict)


233
234
235
236
237
def from_json(injson):
    indict = json.loads(injson)
    return from_dict(indict)


J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
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
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)
    class_name = '{}{}'.format(i[0].upper(), i[1:])
    newclass = type(class_name, tuple(base_classes), {})
    setattr(newclass, '@type', name)
    setattr(newclass, 'schema', read_schema(schema_file))
    register(newclass, name)
    return newclass


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


for i in ['response',
          'results',
          'entry',
          'sentiment',
          'analysis',
          'emotionSet',
          'emotion',
          'emotionModel',
          'suggestion',
          'plugin',
          'emotionPlugin',
          'sentimentPlugin',
          'plugins']:
    _add_from_schema(i)

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


J. Fernando Sánchez's avatar
YAPFed    
J. Fernando Sánchez committed
274
275
276
277
278
class Error(SenpyMixin, BaseException):
    def __init__(self,
                 message,
                 *args,
                 **kwargs):
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
279
280
        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
281
282
        self.message = message

283
284
285
286
287
288
289
290
291
    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
292
293
294
295
    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
296

J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
297
298
299
300
301
302
303
304
305
306
307
    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)


register(Error, 'error')