models.py 9.58 KB
Newer Older
1
'''
2
Senpy Models.
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.
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

15 16
import time
import copy
17 18
import json
import os
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

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

37

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
J. Fernando Sánchez committed
42 43 44
        return os.path.join(
            os.path.dirname(os.path.realpath(__file__)), 'schemas',
            schema_file)
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
J. Fernando Sánchez committed
52 53


54 55 56 57
def dump_schema(schema):
    return jsonref.dumps(schema)


J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73
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:
74
            return context
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
75 76
    else:
        raise AttributeError('Please, provide a valid context')
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
77 78


J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
79
base_context = load_context(CONTEXT_PATH)
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
80 81


J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
82 83
def register(rsubclass, rtype=None):
    BaseMeta.register(rsubclass, rtype)
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
84 85 86 87 88 89 90 91 92


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
93 94 95 96 97 98 99 100 101 102 103 104 105 106 107
    >>> 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
108
    True
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
109 110
    >>> n2 = NewModel()
    >>> n2 == 5
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
111
    True
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
112 113 114 115 116 117 118 119 120 121
    >>> 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
122 123
    '''

J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
124
    schema_file = DEFINITIONS_FILE
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
125
    _context = base_context["@context"]
126

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

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

132 133
        if auto_id:
            self.id
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
134 135 136 137

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

138 139 140
    @property
    def id(self):
        if '@id' not in self:
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
141
            self['@id'] = '_:{}_{}'.format(type(self).__name__, time.time())
142 143 144 145 146 147
        return self['@id']

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

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

        if outformat == 'json-ld' and in_headers:
165
            headers.update({
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
166 167 168 169
                "Link":
                ('<%s>;'
                    'rel="http://www.w3.org/ns/json-ld#context";'
                    ' type="application/ld+json"' % kwargs.get('context_uri'))
170
            })
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
171
        return FlaskResponse(
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
172
            response=content,
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
173
            status=self.get('status', 200),
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
174
            headers=headers,
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
175 176 177 178
            mimetype=mimetype)

    def serialize(self, format='json-ld', with_mime=False, **kwargs):
        js = self.jsonld(**kwargs)
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
179
        content = json.dumps(js, indent=2, sort_keys=True)
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
180 181 182 183
        if format == 'json-ld':
            mimetype = "application/json"
        elif format in ['turtle', ]:
            logger.debug(js)
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
184
            base = kwargs.get('prefix')
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
185 186 187
            g = Graph().parse(
                data=content,
                format='json-ld',
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
188
                base=base,
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
189 190 191
                context=self._context)
            logger.debug(
                'Parsing with prefix: {}'.format(kwargs.get('prefix')))
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
192 193
            content = g.serialize(format='turtle',
                                  base=base).decode('utf-8')
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
194 195 196 197 198 199 200
            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
201

J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
202
    def jsonld(self,
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
203
               with_context=False,
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
204 205 206
               context_uri=None,
               prefix=None,
               expanded=False):
207 208 209

        result = self.serializable()

J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
210 211 212
        ctx = context_uri or self._context

        result['@context'] = ctx
213
        # result = jsonld.compact(result,
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
214
        #                         ctx,
215 216 217 218 219
        #                         options={
        #                             'base': prefix,
        #                             'expandContext': self._context,
        #                             'senpy': prefix
        #                         })
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
220

J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
221 222 223
        if expanded:
            result = jsonld.expand(
                result, options={'base': prefix,
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
224
                                 'expandContext': ctx})
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
225
        if not with_context:
226 227 228 229
            try:
                del result['@context']
            except KeyError:
                pass
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
230
        return result
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
231

232 233 234 235 236
    def validate(self, obj=None):
        if not obj:
            obj = self
        if hasattr(obj, "jsonld"):
            obj = obj.jsonld()
237
        self._validator.validate(obj)
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
238

J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
239 240
    def prov(self, another):
        self['prov:wasGeneratedBy'] = another.id
241 242


J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
243 244
def subtypes():
    return BaseMeta._subtypes
245 246


J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
247 248 249
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
250
        cls = BaseModel
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
251
        try:
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
252 253 254
            cls = subtypes()[target]
        except KeyError:
            pass
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
255 256 257 258 259 260 261
    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
262
            v = v[:]
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
263 264 265
            for ix, v2 in enumerate(v):
                if isinstance(v2, dict):
                    v[ix] = from_dict(v2)
266
        outdict[k] = copy.copy(v)
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
267 268 269 270 271
    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
272 273


274 275 276 277 278
def from_json(injson):
    indict = json.loads(injson)
    return from_dict(indict)


279
class Entry(BaseModel):
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
280 281
    schema = 'entry'

282 283 284 285 286
    text = alias('nif:isString')


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

288 289
    polarity = alias('marl:hasPolarity')
    polarityValue = alias('marl:hasPolarityValue')
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
290 291 292 293 294


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

295
    def __init__(self, message='Generic senpy exception', *args, **kwargs):
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326
        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
327 328 329 330
    globals()[generatedClass.__name__] = generatedClass
    del generatedClass


J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
331
for i in [
332
        'aggregatedEvaluation',
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
333
        'analysis',
334 335
        'dataset',
        'datasets',
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
336 337 338 339 340 341 342
        'emotion',
        'emotionConversion',
        'emotionConversionPlugin',
        'emotionAnalysis',
        'emotionModel',
        'emotionPlugin',
        'emotionSet',
343
        'evaluation',
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
344
        'entity',
345
        'help',
346
        'metric',
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
347 348 349 350 351 352
        'plugin',
        'plugins',
        'response',
        'results',
        'sentimentPlugin',
        'suggestion',
353
        'topic',
354

J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
355
]:
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
356
    _add_class_from_schema(i)