models.py 9.19 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
from __future__ import print_function
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
9
10
11
12
13
from future import standard_library
standard_library.install_aliases()

from future.utils import with_metaclass
from past.builtins import basestring
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
14

J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
15
16
import time
import copy
17
18
import json
import os
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
19
import jsonref
20
from flask import Response as FlaskResponse
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
21
22
from pyld import jsonld

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

J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
25
logging.getLogger('rdflib').setLevel(logging.WARN)
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
26
27
logger = logging.getLogger(__name__)

J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
28
29
30
from rdflib import Graph


31
from .meta import BaseMeta, CustomDict, alias
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
32

J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
33
DEFINITIONS_FILE = 'definitions.json'
J. Fernando Sánchez's avatar
YAPFed    
J. Fernando Sánchez committed
34
35
36
CONTEXT_PATH = os.path.join(
    os.path.dirname(os.path.realpath(__file__)), 'schemas', 'context.jsonld')

37

J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
38
39
40
41
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
42
43
44
        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
45
46
47
48
49


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
50
51
    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
52
53


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


J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
75
base_context = load_context(CONTEXT_PATH)
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
76
77


J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
78
79
def register(rsubclass, rtype=None):
    BaseMeta.register(rsubclass, rtype)
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
80
81
82
83
84
85
86
87
88


class BaseModel(with_metaclass(BaseMeta, CustomDict)):
    '''
    Entities of the base model are a special kind of dictionary that emulates
    a JSON-LD object. The structure of the dictionary is checked via JSON-schema.
    For convenience, the values can also be accessed as attributes
    (a la Javascript). e.g.:

J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
    >>> myobject.key == myobject['key']
    True
    >>> myobject.ns__name == myobject['ns:name']
    True

    Additionally, subclasses of this class can specify default values for their
    instances. These defaults are inherited by subclasses. e.g.:

    >>> class NewModel(BaseModel):
    ...     mydefault = 5
    >>> n1 = NewModel()
    >>> n1['mydefault'] == 5
    True
    >>> n1.mydefault = 3
    >>> n1['mydefault'] = 3
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
104
    True
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
105
106
    >>> n2 = NewModel()
    >>> n2 == 5
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
107
    True
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
108
109
110
111
112
113
114
115
116
117
    >>> class SubModel(NewModel):
            pass
    >>> subn = SubModel()
    >>> subn.mydefault == 5
    True

    Lastly, every subclass that also specifies a schema will get registered, so it
    is possible to deserialize JSON and get the right type.
    i.e. to recover an instance of the original class from a plain JSON.

J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
118
119
    '''

J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
120
    schema_file = DEFINITIONS_FILE
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
121
    _context = base_context["@context"]
122

J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
123
124
    def __init__(self, *args, **kwargs):
        auto_id = kwargs.pop('_auto_id', True)
125

J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
126
127
        super(BaseModel, self).__init__(*args, **kwargs)

128
129
        if auto_id:
            self.id
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
130
131
132
133

        if '@type' not in self:
            logger.warn('Created an instance of an unknown model')

134
135
136
137
138
139
140
141
142
143
    @property
    def id(self):
        if '@id' not in self:
            self['@id'] = ':{}_{}'.format(type(self).__name__, time.time())
        return self['@id']

    @id.setter
    def id(self, value):
        self['@id'] = value

J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
144
145
146
147
148
    def flask(self,
              in_headers=True,
              headers=None,
              outformat='json-ld',
              **kwargs):
149
        """
150
151
152
        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.
153
        """
154
        headers = headers or {}
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
155
156
157
158
159
160
        kwargs["with_context"] = not in_headers
        content, mimetype = self.serialize(format=outformat,
                                           with_mime=True,
                                           **kwargs)

        if outformat == 'json-ld' and in_headers:
161
            headers.update({
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
162
163
164
165
                "Link":
                ('<%s>;'
                    'rel="http://www.w3.org/ns/json-ld#context";'
                    ' type="application/ld+json"' % kwargs.get('context_uri'))
166
            })
J. Fernando Sánchez's avatar
YAPFed    
J. Fernando Sánchez committed
167
        return FlaskResponse(
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
168
            response=content,
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
169
            status=self.get('status', 200),
J. Fernando Sánchez's avatar
YAPFed    
J. Fernando Sánchez committed
170
            headers=headers,
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
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
            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
196

J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
197
    def jsonld(self,
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
198
               with_context=False,
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
199
200
201
               context_uri=None,
               prefix=None,
               expanded=False):
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
202
        ser = self.serializable()
203

J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
        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
221

222
223
224
225
226
    def validate(self, obj=None):
        if not obj:
            obj = self
        if hasattr(obj, "jsonld"):
            obj = obj.jsonld()
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
227
        self._validator.validate(obj)
J. Fernando Sánchez's avatar
YAPFed    
J. Fernando Sánchez committed
228

J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
229
230
    def prov(self, another):
        self['prov:wasGeneratedBy'] = another.id
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
231
232


J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
233
234
def subtypes():
    return BaseMeta._subtypes
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
235
236


J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
237
238
239
def from_dict(indict, cls=None):
    if not cls:
        target = indict.get('@type', None)
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
240
        cls = BaseModel
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
241
        try:
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
242
243
244
            cls = subtypes()[target]
        except KeyError:
            pass
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
245
246
247
248
249
250
251
    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):
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
252
            v = v[:]
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
253
254
255
            for ix, v2 in enumerate(v):
                if isinstance(v2, dict):
                    v[ix] = from_dict(v2)
256
        outdict[k] = copy.copy(v)
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
257
258
259
260
261
    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
262
263


264
265
266
267
268
def from_json(injson):
    indict = json.loads(injson)
    return from_dict(indict)


269
class Entry(BaseModel):
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
270
271
    schema = 'entry'

272
273
274
275
276
    text = alias('nif:isString')


class Sentiment(BaseModel):
    schema = 'sentiment'
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
277

278
279
    polarity = alias('marl:hasPolarity')
    polarityValue = alias('marl:hasPolarityValue')
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
280
281
282
283
284


class Error(BaseModel, Exception):
    schema = 'error'

285
    def __init__(self, message='Generic senpy exception', *args, **kwargs):
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
        Exception.__init__(self, message)
        super(Error, self).__init__(*args, **kwargs)
        self.message = message

    def __str__(self):
        if not hasattr(self, 'errors'):
            return self.message
        return '{}:\n\t{}'.format(self.message, self.errors)

    def __hash__(self):
        return Exception.__hash__(self)


# Add the remaining schemas programmatically

def _class_from_schema(name, schema=None, schema_file=None, base_classes=None):
    base_classes = base_classes or []
    base_classes.append(BaseModel)
    attrs = {}
    if schema:
        attrs['schema'] = schema
    elif schema_file:
        attrs['schema_file'] = schema_file
    else:
        attrs['schema'] = name
    name = "".join((name[0].upper(), name[1:]))
    return BaseMeta(name, base_classes, attrs)


def _add_class_from_schema(*args, **kwargs):
    generatedClass = _class_from_schema(*args, **kwargs)
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
317
318
319
320
    globals()[generatedClass.__name__] = generatedClass
    del generatedClass


J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
321
322
323
324
325
326
327
328
329
for i in [
        'analysis',
        'emotion',
        'emotionConversion',
        'emotionConversionPlugin',
        'emotionAnalysis',
        'emotionModel',
        'emotionPlugin',
        'emotionSet',
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
330
        'help',
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
331
332
333
334
335
336
337
        'plugin',
        'plugins',
        'response',
        'results',
        'sentimentPlugin',
        'suggestion',
]:
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
338
    _add_class_from_schema(i)