mirror of
https://github.com/gsi-upm/senpy
synced 2024-11-22 08:12:27 +00:00
Last batch of big changes
* Add Box plugin (i.e. black box) * Add SentimentBox, EmotionBox and MappingMixin * Refactored CustomDict
This commit is contained in:
parent
21a5a3f201
commit
3e2b8baeb2
@ -1,9 +1,19 @@
|
|||||||
This is a collection of plugins that exemplify certain aspects of plugin development with senpy.
|
This is a collection of plugins that exemplify certain aspects of plugin development with senpy.
|
||||||
In ascending order of complexity, there are:
|
|
||||||
|
|
||||||
* Basic: a very basic analysis that does sentiment analysis based on emojis.
|
The first series of plugins the `basic` ones.
|
||||||
* Configurable: a version of `basic` with a configurable map of emojis for each sentiment.
|
Their starting point is a classification function defined in `basic.py`.
|
||||||
* Parameterized: like `basic_info`, but users set the map in each query (via `extra_parameters`).
|
They all include testing and running them as a script will run all tests.
|
||||||
|
In ascending order of customization, the plugins are:
|
||||||
|
|
||||||
|
* Basic is the simplest plugin of all. It leverages the `SentimentBox` Plugin class to create a plugin out of a classification method, and `MappingMixin` to convert the labels from (`pos`, `neg`) to (`marl:Positive`, `marl:Negative`
|
||||||
|
* Basic_box is just like the previous one, but replaces the mixin with a custom function.
|
||||||
|
* Basic_configurable is a version of `basic` with a configurable map of emojis for each sentiment.
|
||||||
|
* Basic_parameterized like `basic_info`, but users set the map in each query (via `extra_parameters`).
|
||||||
|
* Basic_analyse\_entry uses the more general `analyse_entry` method and adds the annotations individually.
|
||||||
|
|
||||||
|
|
||||||
|
In rest of the plugins show advanced topics:
|
||||||
|
|
||||||
* mynoop: shows how to add a definition file with external requirements for a plugin. Doing this with a python-only module would require moving all imports of the requirements to their functions, which is considered bad practice.
|
* mynoop: shows how to add a definition file with external requirements for a plugin. Doing this with a python-only module would require moving all imports of the requirements to their functions, which is considered bad practice.
|
||||||
* Async: a barebones example of training a plugin and analyzing data in parallel.
|
* Async: a barebones example of training a plugin and analyzing data in parallel.
|
||||||
|
|
||||||
|
@ -2,13 +2,13 @@
|
|||||||
# coding: utf-8
|
# coding: utf-8
|
||||||
|
|
||||||
emoticons = {
|
emoticons = {
|
||||||
'marl:Positive': [':)', ':]', '=)', ':D'],
|
'pos': [':)', ':]', '=)', ':D'],
|
||||||
'marl:Negative': [':(', ':[', '=(']
|
'neg': [':(', ':[', '=(']
|
||||||
}
|
}
|
||||||
|
|
||||||
emojis = {
|
emojis = {
|
||||||
'marl:Positive': ['😁', '😂', '😃', '😄', '😆', '😅', '😄' '😍'],
|
'pos': ['😁', '😂', '😃', '😄', '😆', '😅', '😄' '😍'],
|
||||||
'marl:Negative': ['😢', '😡', '😠', '😞', '😖', '😔', '😓', '😒']
|
'neg': ['😢', '😡', '😠', '😞', '😖', '😔', '😓', '😒']
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
47
example-plugins/basic_analyse_entry_plugin.py
Normal file
47
example-plugins/basic_analyse_entry_plugin.py
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
#!/usr/local/bin/python
|
||||||
|
# coding: utf-8
|
||||||
|
|
||||||
|
from senpy import easy_test, models, plugins
|
||||||
|
|
||||||
|
import basic
|
||||||
|
|
||||||
|
|
||||||
|
class BasicAnalyseEntry(plugins.SentimentPlugin):
|
||||||
|
'''Equivalent to Basic, implementing the analyse_entry method'''
|
||||||
|
|
||||||
|
author = '@balkian'
|
||||||
|
version = '0.1'
|
||||||
|
|
||||||
|
mappings = {
|
||||||
|
'pos': 'marl:Positive',
|
||||||
|
'neg': 'marl:Negative',
|
||||||
|
'default': 'marl:Neutral'
|
||||||
|
}
|
||||||
|
|
||||||
|
def analyse_entry(self, entry, params):
|
||||||
|
polarity = basic.get_polarity(entry.text)
|
||||||
|
|
||||||
|
polarity = self.mappings.get(polarity, self.mappings['default'])
|
||||||
|
|
||||||
|
s = models.Sentiment(marl__hasPolarity=polarity)
|
||||||
|
s.prov(self)
|
||||||
|
entry.sentiments.append(s)
|
||||||
|
yield entry
|
||||||
|
|
||||||
|
test_cases = [{
|
||||||
|
'input': 'Hello :)',
|
||||||
|
'polarity': 'marl:Positive'
|
||||||
|
}, {
|
||||||
|
'input': 'So sad :(',
|
||||||
|
'polarity': 'marl:Negative'
|
||||||
|
}, {
|
||||||
|
'input': 'Yay! Emojis 😁',
|
||||||
|
'polarity': 'marl:Positive'
|
||||||
|
}, {
|
||||||
|
'input': 'But no emoticons 😢',
|
||||||
|
'polarity': 'marl:Negative'
|
||||||
|
}]
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
easy_test()
|
41
example-plugins/basic_box_plugin.py
Normal file
41
example-plugins/basic_box_plugin.py
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
#!/usr/local/bin/python
|
||||||
|
# coding: utf-8
|
||||||
|
|
||||||
|
from senpy import easy_test, SentimentBox
|
||||||
|
|
||||||
|
import basic
|
||||||
|
|
||||||
|
|
||||||
|
class BasicBox(SentimentBox):
|
||||||
|
''' A modified version of Basic that also does converts annotations manually'''
|
||||||
|
|
||||||
|
author = '@balkian'
|
||||||
|
version = '0.1'
|
||||||
|
|
||||||
|
mappings = {
|
||||||
|
'pos': 'marl:Positive',
|
||||||
|
'neg': 'marl:Negative',
|
||||||
|
'default': 'marl:Neutral'
|
||||||
|
}
|
||||||
|
|
||||||
|
def box(self, input, **kwargs):
|
||||||
|
output = basic.get_polarity(input)
|
||||||
|
return self.mappings.get(output, self.mappings['default'])
|
||||||
|
|
||||||
|
test_cases = [{
|
||||||
|
'input': 'Hello :)',
|
||||||
|
'polarity': 'marl:Positive'
|
||||||
|
}, {
|
||||||
|
'input': 'So sad :(',
|
||||||
|
'polarity': 'marl:Negative'
|
||||||
|
}, {
|
||||||
|
'input': 'Yay! Emojis 😁',
|
||||||
|
'polarity': 'marl:Positive'
|
||||||
|
}, {
|
||||||
|
'input': 'But no emoticons 😢',
|
||||||
|
'polarity': 'marl:Negative'
|
||||||
|
}]
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
easy_test()
|
@ -1,25 +1,25 @@
|
|||||||
#!/usr/local/bin/python
|
#!/usr/local/bin/python
|
||||||
# coding: utf-8
|
# coding: utf-8
|
||||||
|
|
||||||
from senpy import easy_test, models, plugins
|
from senpy import easy_test, SentimentBox, MappingMixin
|
||||||
|
|
||||||
import basic
|
import basic
|
||||||
|
|
||||||
|
|
||||||
class Basic(plugins.SentimentPlugin):
|
class Basic(MappingMixin, SentimentBox):
|
||||||
'''Provides sentiment annotation using a lexicon'''
|
'''Provides sentiment annotation using a lexicon'''
|
||||||
|
|
||||||
author = '@balkian'
|
author = '@balkian'
|
||||||
version = '0.1'
|
version = '0.1'
|
||||||
|
|
||||||
def analyse_entry(self, entry, params):
|
mappings = {
|
||||||
|
'pos': 'marl:Positive',
|
||||||
|
'neg': 'marl:Negative',
|
||||||
|
'default': 'marl:Neutral'
|
||||||
|
}
|
||||||
|
|
||||||
polarity = basic.get_polarity(entry.text)
|
def box(self, input, **kwargs):
|
||||||
|
return basic.get_polarity(input)
|
||||||
s = models.Sentiment(marl__hasPolarity=polarity)
|
|
||||||
s.prov(self)
|
|
||||||
entry.sentiments.append(s)
|
|
||||||
yield entry
|
|
||||||
|
|
||||||
test_cases = [{
|
test_cases = [{
|
||||||
'input': 'Hello :)',
|
'input': 'Hello :)',
|
||||||
|
@ -14,8 +14,12 @@ class Dictionary(plugins.SentimentPlugin):
|
|||||||
|
|
||||||
dictionaries = [basic.emojis, basic.emoticons]
|
dictionaries = [basic.emojis, basic.emoticons]
|
||||||
|
|
||||||
|
mappings = {'pos': 'marl:Positive', 'neg': 'marl:Negative'}
|
||||||
|
|
||||||
def analyse_entry(self, entry, params):
|
def analyse_entry(self, entry, params):
|
||||||
polarity = basic.get_polarity(entry.text, self.dictionaries)
|
polarity = basic.get_polarity(entry.text, self.dictionaries)
|
||||||
|
if polarity in self.mappings:
|
||||||
|
polarity = self.mappings[polarity]
|
||||||
|
|
||||||
s = models.Sentiment(marl__hasPolarity=polarity)
|
s = models.Sentiment(marl__hasPolarity=polarity)
|
||||||
s.prov(self)
|
s.prov(self)
|
||||||
@ -80,14 +84,14 @@ class Salutes(Dictionary):
|
|||||||
'''Sentiment annotation with a custom lexicon, for illustration purposes'''
|
'''Sentiment annotation with a custom lexicon, for illustration purposes'''
|
||||||
dictionaries = [{
|
dictionaries = [{
|
||||||
'marl:Positive': ['Hello', '!'],
|
'marl:Positive': ['Hello', '!'],
|
||||||
'marl:Negative': ['sad', ]
|
'marl:Negative': ['Good bye', ]
|
||||||
}]
|
}]
|
||||||
|
|
||||||
test_cases = [{
|
test_cases = [{
|
||||||
'input': 'Hello :)',
|
'input': 'Hello :)',
|
||||||
'polarity': 'marl:Positive'
|
'polarity': 'marl:Positive'
|
||||||
}, {
|
}, {
|
||||||
'input': 'So sad :(',
|
'input': 'Good bye :(',
|
||||||
'polarity': 'marl:Negative'
|
'polarity': 'marl:Negative'
|
||||||
}, {
|
}, {
|
||||||
'input': 'Yay! Emojis 😁',
|
'input': 'Yay! Emojis 😁',
|
||||||
|
@ -7,8 +7,8 @@ import basic
|
|||||||
|
|
||||||
|
|
||||||
class ParameterizedDictionary(plugins.SentimentPlugin):
|
class ParameterizedDictionary(plugins.SentimentPlugin):
|
||||||
|
'''This is a basic self-contained plugin'''
|
||||||
|
|
||||||
description = 'This is a basic self-contained plugin'
|
|
||||||
author = '@balkian'
|
author = '@balkian'
|
||||||
version = '0.2'
|
version = '0.2'
|
||||||
|
|
||||||
|
@ -46,9 +46,9 @@ def main():
|
|||||||
'''
|
'''
|
||||||
try:
|
try:
|
||||||
res = main_function(sys.argv[1:])
|
res = main_function(sys.argv[1:])
|
||||||
print(res.to_JSON())
|
print(res.serialize())
|
||||||
except Error as err:
|
except Error as err:
|
||||||
print(err.to_JSON())
|
print(err.serialize())
|
||||||
sys.exit(2)
|
sys.exit(2)
|
||||||
|
|
||||||
|
|
||||||
|
185
senpy/meta.py
185
senpy/meta.py
@ -8,6 +8,7 @@ import inspect
|
|||||||
import copy
|
import copy
|
||||||
|
|
||||||
from abc import ABCMeta
|
from abc import ABCMeta
|
||||||
|
from collections import MutableMapping, namedtuple
|
||||||
|
|
||||||
|
|
||||||
class BaseMeta(ABCMeta):
|
class BaseMeta(ABCMeta):
|
||||||
@ -31,24 +32,31 @@ class BaseMeta(ABCMeta):
|
|||||||
_subtypes = {}
|
_subtypes = {}
|
||||||
|
|
||||||
def __new__(mcs, name, bases, attrs, **kwargs):
|
def __new__(mcs, name, bases, attrs, **kwargs):
|
||||||
defaults = {}
|
|
||||||
register_afterwards = False
|
register_afterwards = False
|
||||||
|
defaults = {}
|
||||||
|
|
||||||
attrs = mcs.expand_with_schema(name, attrs)
|
attrs = mcs.expand_with_schema(name, attrs)
|
||||||
if 'schema' in attrs:
|
if 'schema' in attrs:
|
||||||
register_afterwards = True
|
register_afterwards = True
|
||||||
defaults = mcs.get_defaults(attrs['schema'])
|
for base in bases:
|
||||||
for b in bases:
|
if hasattr(base, '_defaults'):
|
||||||
if hasattr(b, '_defaults'):
|
defaults.update(getattr(base, '_defaults'))
|
||||||
defaults.update(b._defaults)
|
|
||||||
info, attrs = mcs.split_attrs(attrs)
|
|
||||||
defaults.update(info)
|
|
||||||
attrs['_defaults'] = defaults
|
|
||||||
|
|
||||||
cls = super(BaseMeta, mcs).__new__(mcs, name, tuple(bases), attrs)
|
info, rest = mcs.split_attrs(attrs)
|
||||||
|
|
||||||
|
for i in list(info.keys()):
|
||||||
|
if isinstance(info[i], _Alias):
|
||||||
|
fget, fset, fdel = make_property(info[i].indict)
|
||||||
|
rest[i] = property(fget=fget, fset=fset, fdel=fdel)
|
||||||
|
else:
|
||||||
|
defaults[i] = info[i]
|
||||||
|
|
||||||
|
rest['_defaults'] = defaults
|
||||||
|
|
||||||
|
cls = super(BaseMeta, mcs).__new__(mcs, name, tuple(bases), rest)
|
||||||
|
|
||||||
if register_afterwards:
|
if register_afterwards:
|
||||||
mcs.register(cls, cls._defaults['@type'])
|
mcs.register(cls, defaults['@type'])
|
||||||
return cls
|
return cls
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@ -81,17 +89,26 @@ class BaseMeta(ABCMeta):
|
|||||||
attrs['_schema_file'] = schema_file
|
attrs['_schema_file'] = schema_file
|
||||||
attrs['schema'] = schema
|
attrs['schema'] = schema
|
||||||
attrs['_validator'] = jsonschema.Draft4Validator(schema, resolver=resolver)
|
attrs['_validator'] = jsonschema.Draft4Validator(schema, resolver=resolver)
|
||||||
|
|
||||||
|
schema_defaults = BaseMeta.get_defaults(attrs['schema'])
|
||||||
|
attrs.update(schema_defaults)
|
||||||
|
|
||||||
return attrs
|
return attrs
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def is_attr(k, v):
|
def is_func(v):
|
||||||
return (not(inspect.isroutine(v) or
|
return inspect.isroutine(v) or inspect.ismethod(v) or \
|
||||||
inspect.ismethod(v) or
|
inspect.ismodule(v) or isinstance(v, property)
|
||||||
inspect.ismodule(v) or
|
|
||||||
isinstance(v, property)) and
|
@staticmethod
|
||||||
k[0] != '_' and
|
def is_internal(k):
|
||||||
k != 'schema' and
|
return k[0] == '_' or k == 'schema' or k == 'data'
|
||||||
k != 'data')
|
|
||||||
|
@staticmethod
|
||||||
|
def get_key(key):
|
||||||
|
if key[0] != '_':
|
||||||
|
key = key.replace("__", ":", 1)
|
||||||
|
return key
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def split_attrs(attrs):
|
def split_attrs(attrs):
|
||||||
@ -102,15 +119,13 @@ class BaseMeta(ABCMeta):
|
|||||||
e.g.:
|
e.g.:
|
||||||
'''
|
'''
|
||||||
isattr = {}
|
isattr = {}
|
||||||
notattr = {}
|
rest = {}
|
||||||
for key, value in attrs.items():
|
for key, value in attrs.items():
|
||||||
if BaseMeta.is_attr(key, value):
|
if not (BaseMeta.is_internal(key)) and (not BaseMeta.is_func(value)):
|
||||||
if key[0] != '_':
|
isattr[key] = value
|
||||||
key = key.replace("__", ":", 1)
|
|
||||||
isattr[key] = copy.deepcopy(value)
|
|
||||||
else:
|
else:
|
||||||
notattr[key] = value
|
rest[key] = value
|
||||||
return isattr, notattr
|
return isattr, rest
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_defaults(schema):
|
def get_defaults(schema):
|
||||||
@ -120,5 +135,123 @@ class BaseMeta(ABCMeta):
|
|||||||
] + schema.get('allOf', []):
|
] + schema.get('allOf', []):
|
||||||
for k, v in obj.get('properties', {}).items():
|
for k, v in obj.get('properties', {}).items():
|
||||||
if 'default' in v and k not in temp:
|
if 'default' in v and k not in temp:
|
||||||
temp[k] = copy.deepcopy(v['default'])
|
temp[k] = v['default']
|
||||||
return temp
|
return temp
|
||||||
|
|
||||||
|
|
||||||
|
def make_property(key):
|
||||||
|
|
||||||
|
def fget(self):
|
||||||
|
return self[key]
|
||||||
|
|
||||||
|
def fdel(self):
|
||||||
|
del self[key]
|
||||||
|
|
||||||
|
def fset(self, value):
|
||||||
|
self[key] = value
|
||||||
|
|
||||||
|
return fget, fset, fdel
|
||||||
|
|
||||||
|
|
||||||
|
class CustomDict(MutableMapping, object):
|
||||||
|
'''
|
||||||
|
A dictionary whose elements can also be accessed as attributes. Since some
|
||||||
|
characters are not valid in the dot-notation, the attribute names also
|
||||||
|
converted. e.g.:
|
||||||
|
|
||||||
|
> d = CustomDict()
|
||||||
|
> d.key = d['ns:name'] = 1
|
||||||
|
> d.key == d['key']
|
||||||
|
True
|
||||||
|
> d.ns__name == d['ns:name']
|
||||||
|
'''
|
||||||
|
|
||||||
|
_defaults = {}
|
||||||
|
_map_attr_key = {'id': '@id'}
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super(CustomDict, self).__init__()
|
||||||
|
for k, v in self._defaults.items():
|
||||||
|
self[k] = copy.copy(v)
|
||||||
|
for arg in args:
|
||||||
|
self.update(arg)
|
||||||
|
for k, v in kwargs.items():
|
||||||
|
self[self._attr_to_key(k)] = v
|
||||||
|
return self
|
||||||
|
|
||||||
|
def serializable(self):
|
||||||
|
def ser_or_down(item):
|
||||||
|
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) or isinstance(item, set):
|
||||||
|
return list(ser_or_down(i) for i in item)
|
||||||
|
else:
|
||||||
|
return item
|
||||||
|
|
||||||
|
return ser_or_down(self.as_dict())
|
||||||
|
|
||||||
|
def __getitem__(self, key):
|
||||||
|
key = self._key_to_attr(key)
|
||||||
|
return self.__dict__[key]
|
||||||
|
|
||||||
|
def __setitem__(self, key, value):
|
||||||
|
'''Do not insert data directly, there might be a property in that key. '''
|
||||||
|
key = self._key_to_attr(key)
|
||||||
|
return setattr(self, key, value)
|
||||||
|
|
||||||
|
def as_dict(self):
|
||||||
|
return {self._attr_to_key(k): v for k, v in self.__dict__.items()
|
||||||
|
if not self._internal_key(k)}
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
return (k for k in self.__dict__ if not self._internal_key(k))
|
||||||
|
|
||||||
|
def __len__(self):
|
||||||
|
return len(self.__dict__)
|
||||||
|
|
||||||
|
def __delitem__(self, key):
|
||||||
|
del self.__dict__[key]
|
||||||
|
|
||||||
|
def update(self, other):
|
||||||
|
for k, v in other.items():
|
||||||
|
self[k] = v
|
||||||
|
|
||||||
|
def _attr_to_key(self, key):
|
||||||
|
key = key.replace("__", ":", 1)
|
||||||
|
key = self._map_attr_key.get(key, key)
|
||||||
|
return key
|
||||||
|
|
||||||
|
def _key_to_attr(self, key):
|
||||||
|
if self._internal_key(key):
|
||||||
|
return key
|
||||||
|
key = key.replace(":", "__", 1)
|
||||||
|
return key
|
||||||
|
|
||||||
|
def __getattr__(self, key):
|
||||||
|
try:
|
||||||
|
return self.__dict__[self._attr_to_key(key)]
|
||||||
|
except KeyError:
|
||||||
|
raise AttributeError
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _internal_key(key):
|
||||||
|
return key[0] == '_'
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return str(self.serializable())
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return str(self.serializable())
|
||||||
|
|
||||||
|
|
||||||
|
_Alias = namedtuple('Alias', 'indict')
|
||||||
|
|
||||||
|
|
||||||
|
def alias(key):
|
||||||
|
return _Alias(key)
|
||||||
|
124
senpy/models.py
124
senpy/models.py
@ -17,8 +17,6 @@ import copy
|
|||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import jsonref
|
import jsonref
|
||||||
from collections import UserDict
|
|
||||||
|
|
||||||
from flask import Response as FlaskResponse
|
from flask import Response as FlaskResponse
|
||||||
from pyld import jsonld
|
from pyld import jsonld
|
||||||
|
|
||||||
@ -30,7 +28,7 @@ logger = logging.getLogger(__name__)
|
|||||||
from rdflib import Graph
|
from rdflib import Graph
|
||||||
|
|
||||||
|
|
||||||
from .meta import BaseMeta
|
from .meta import BaseMeta, CustomDict, alias
|
||||||
|
|
||||||
DEFINITIONS_FILE = 'definitions.json'
|
DEFINITIONS_FILE = 'definitions.json'
|
||||||
CONTEXT_PATH = os.path.join(
|
CONTEXT_PATH = os.path.join(
|
||||||
@ -81,67 +79,6 @@ def register(rsubclass, rtype=None):
|
|||||||
BaseMeta.register(rsubclass, rtype)
|
BaseMeta.register(rsubclass, rtype)
|
||||||
|
|
||||||
|
|
||||||
class CustomDict(UserDict, object):
|
|
||||||
'''
|
|
||||||
A dictionary whose elements can also be accessed as attributes. Since some
|
|
||||||
characters are not valid in the dot-notation, the attribute names also
|
|
||||||
converted. e.g.:
|
|
||||||
|
|
||||||
> d = CustomDict()
|
|
||||||
> d.key = d['ns:name'] = 1
|
|
||||||
> d.key == d['key']
|
|
||||||
True
|
|
||||||
> d.ns__name == d['ns:name']
|
|
||||||
'''
|
|
||||||
|
|
||||||
_defaults = []
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
temp = copy.deepcopy(self._defaults)
|
|
||||||
for arg in args:
|
|
||||||
temp.update(copy.deepcopy(arg))
|
|
||||||
for k, v in kwargs.items():
|
|
||||||
temp[self._get_key(k)] = v
|
|
||||||
|
|
||||||
super(CustomDict, self).__init__(temp)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _get_key(key):
|
|
||||||
if key is 'id':
|
|
||||||
key = '@id'
|
|
||||||
key = key.replace("__", ":", 1)
|
|
||||||
return key
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _internal_key(key):
|
|
||||||
return key[0] == '_' or key == 'data'
|
|
||||||
|
|
||||||
def __getattr__(self, key):
|
|
||||||
'''
|
|
||||||
__getattr__ only gets called when the attribute could not be found
|
|
||||||
in the __dict__. So we only need to look for the the element in the
|
|
||||||
dictionary, or raise an Exception.
|
|
||||||
'''
|
|
||||||
mkey = self._get_key(key)
|
|
||||||
if not self._internal_key(key) and mkey in self:
|
|
||||||
return self[mkey]
|
|
||||||
raise AttributeError(key)
|
|
||||||
|
|
||||||
def __setattr__(self, key, value):
|
|
||||||
# Work as usual for internal properties or already existing
|
|
||||||
# properties
|
|
||||||
if self._internal_key(key) or key in self.__dict__:
|
|
||||||
return super(CustomDict, self).__setattr__(key, value)
|
|
||||||
key = self._get_key(key)
|
|
||||||
return self.__setitem__(self._get_key(key), value)
|
|
||||||
|
|
||||||
def __delattr__(self, key):
|
|
||||||
if self._internal_key(key):
|
|
||||||
return object.__delattr__(self, key)
|
|
||||||
key = self._get_key(key)
|
|
||||||
self.__delitem__(self._get_key(key))
|
|
||||||
|
|
||||||
|
|
||||||
class BaseModel(with_metaclass(BaseMeta, CustomDict)):
|
class BaseModel(with_metaclass(BaseMeta, CustomDict)):
|
||||||
'''
|
'''
|
||||||
Entities of the base model are a special kind of dictionary that emulates
|
Entities of the base model are a special kind of dictionary that emulates
|
||||||
@ -185,14 +122,25 @@ class BaseModel(with_metaclass(BaseMeta, CustomDict)):
|
|||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
auto_id = kwargs.pop('_auto_id', True)
|
auto_id = kwargs.pop('_auto_id', True)
|
||||||
|
|
||||||
super(BaseModel, self).__init__(*args, **kwargs)
|
super(BaseModel, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
if '@id' not in self and auto_id:
|
if auto_id:
|
||||||
self.id = ':{}_{}'.format(type(self).__name__, time.time())
|
self.id
|
||||||
|
|
||||||
if '@type' not in self:
|
if '@type' not in self:
|
||||||
logger.warn('Created an instance of an unknown model')
|
logger.warn('Created an instance of an unknown model')
|
||||||
|
|
||||||
|
@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
|
||||||
|
|
||||||
def flask(self,
|
def flask(self,
|
||||||
in_headers=True,
|
in_headers=True,
|
||||||
headers=None,
|
headers=None,
|
||||||
@ -246,23 +194,6 @@ class BaseModel(with_metaclass(BaseMeta, CustomDict)):
|
|||||||
else:
|
else:
|
||||||
return content
|
return content
|
||||||
|
|
||||||
def serializable(self):
|
|
||||||
def ser_or_down(item):
|
|
||||||
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) or isinstance(item, set):
|
|
||||||
return list(ser_or_down(i) for i in item)
|
|
||||||
else:
|
|
||||||
return item
|
|
||||||
|
|
||||||
return ser_or_down(self.data)
|
|
||||||
|
|
||||||
def jsonld(self,
|
def jsonld(self,
|
||||||
with_context=False,
|
with_context=False,
|
||||||
context_uri=None,
|
context_uri=None,
|
||||||
@ -288,10 +219,6 @@ class BaseModel(with_metaclass(BaseMeta, CustomDict)):
|
|||||||
del result['@context']
|
del result['@context']
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def to_JSON(self, *args, **kwargs):
|
|
||||||
js = json.dumps(self.jsonld(*args, **kwargs), indent=4, sort_keys=True)
|
|
||||||
return js
|
|
||||||
|
|
||||||
def validate(self, obj=None):
|
def validate(self, obj=None):
|
||||||
if not obj:
|
if not obj:
|
||||||
obj = self
|
obj = self
|
||||||
@ -299,9 +226,6 @@ class BaseModel(with_metaclass(BaseMeta, CustomDict)):
|
|||||||
obj = obj.jsonld()
|
obj = obj.jsonld()
|
||||||
self._validator.validate(obj)
|
self._validator.validate(obj)
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return str(self.serialize())
|
|
||||||
|
|
||||||
def prov(self, another):
|
def prov(self, another):
|
||||||
self['prov:wasGeneratedBy'] = another.id
|
self['prov:wasGeneratedBy'] = another.id
|
||||||
|
|
||||||
@ -329,7 +253,7 @@ def from_dict(indict, cls=None):
|
|||||||
for ix, v2 in enumerate(v):
|
for ix, v2 in enumerate(v):
|
||||||
if isinstance(v2, dict):
|
if isinstance(v2, dict):
|
||||||
v[ix] = from_dict(v2)
|
v[ix] = from_dict(v2)
|
||||||
outdict[k] = copy.deepcopy(v)
|
outdict[k] = copy.copy(v)
|
||||||
return cls(**outdict)
|
return cls(**outdict)
|
||||||
|
|
||||||
|
|
||||||
@ -342,22 +266,23 @@ def from_json(injson):
|
|||||||
return from_dict(indict)
|
return from_dict(indict)
|
||||||
|
|
||||||
|
|
||||||
class Entry(BaseModel, Exception):
|
class Entry(BaseModel):
|
||||||
schema = 'entry'
|
schema = 'entry'
|
||||||
|
|
||||||
@property
|
text = alias('nif:isString')
|
||||||
def text(self):
|
|
||||||
return self['nif:isString']
|
|
||||||
|
|
||||||
@text.setter
|
|
||||||
def text(self, value):
|
class Sentiment(BaseModel):
|
||||||
self['nif:isString'] = value
|
schema = 'sentiment'
|
||||||
|
|
||||||
|
polarity = alias('marl:hasPolarity')
|
||||||
|
polarityValue = alias('marl:hasPolarityValue')
|
||||||
|
|
||||||
|
|
||||||
class Error(BaseModel, Exception):
|
class Error(BaseModel, Exception):
|
||||||
schema = 'error'
|
schema = 'error'
|
||||||
|
|
||||||
def __init__(self, message, *args, **kwargs):
|
def __init__(self, message='Generic senpy exception', *args, **kwargs):
|
||||||
Exception.__init__(self, message)
|
Exception.__init__(self, message)
|
||||||
super(Error, self).__init__(*args, **kwargs)
|
super(Error, self).__init__(*args, **kwargs)
|
||||||
self.message = message
|
self.message = message
|
||||||
@ -407,7 +332,6 @@ for i in [
|
|||||||
'plugins',
|
'plugins',
|
||||||
'response',
|
'response',
|
||||||
'results',
|
'results',
|
||||||
'sentiment',
|
|
||||||
'sentimentPlugin',
|
'sentimentPlugin',
|
||||||
'suggestion',
|
'suggestion',
|
||||||
]:
|
]:
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
from future import standard_library
|
from future import standard_library
|
||||||
standard_library.install_aliases()
|
standard_library.install_aliases()
|
||||||
|
|
||||||
|
|
||||||
from future.utils import with_metaclass
|
from future.utils import with_metaclass
|
||||||
|
|
||||||
import os.path
|
import os.path
|
||||||
@ -120,21 +122,20 @@ class Plugin(with_metaclass(PluginMeta, models.Plugin)):
|
|||||||
entry = models.Entry(case['entry'])
|
entry = models.Entry(case['entry'])
|
||||||
given_parameters = case.get('params', case.get('parameters', {}))
|
given_parameters = case.get('params', case.get('parameters', {}))
|
||||||
expected = case['expected']
|
expected = case['expected']
|
||||||
|
should_fail = case.get('should_fail', False)
|
||||||
try:
|
try:
|
||||||
params = api.parse_params(given_parameters, self.extra_params)
|
params = api.parse_params(given_parameters, self.extra_params)
|
||||||
res = list(self.analyse_entry(entry, params))
|
res = list(self.analyse_entries([entry, ], params))
|
||||||
except models.Error:
|
|
||||||
if not expected:
|
|
||||||
return
|
|
||||||
raise
|
|
||||||
if not expected:
|
|
||||||
raise Exception('This test should have raised an exception.')
|
|
||||||
|
|
||||||
if not isinstance(expected, list):
|
if not isinstance(expected, list):
|
||||||
expected = [expected]
|
expected = [expected]
|
||||||
utils.check_template(res, expected)
|
utils.check_template(res, expected)
|
||||||
for r in res:
|
for r in res:
|
||||||
r.validate()
|
r.validate()
|
||||||
|
except models.Error:
|
||||||
|
if should_fail:
|
||||||
|
return
|
||||||
|
assert not should_fail
|
||||||
|
|
||||||
def open(self, fpath, *args, **kwargs):
|
def open(self, fpath, *args, **kwargs):
|
||||||
if not os.path.isabs(fpath):
|
if not os.path.isabs(fpath):
|
||||||
@ -241,6 +242,92 @@ class EmotionConversion(Conversion):
|
|||||||
EmotionConversionPlugin = EmotionConversion
|
EmotionConversionPlugin = EmotionConversion
|
||||||
|
|
||||||
|
|
||||||
|
class Box(AnalysisPlugin):
|
||||||
|
'''
|
||||||
|
Black box plugins delegate analysis to a function.
|
||||||
|
The flow is like so:
|
||||||
|
|
||||||
|
.. code-block::
|
||||||
|
|
||||||
|
entry --> input() --> box() --> output() --> entry'
|
||||||
|
|
||||||
|
|
||||||
|
In other words: their ``input`` method convers a query (entry and a set of parameters) into
|
||||||
|
the input to the box method. The ``output`` method convers the results given by the box into
|
||||||
|
an entry that senpy can handle.
|
||||||
|
'''
|
||||||
|
|
||||||
|
def input(self, entry, params=None):
|
||||||
|
'''Transforms a query (entry+param) into an input for the black box'''
|
||||||
|
return entry
|
||||||
|
|
||||||
|
def output(self, output, entry=None, params=None):
|
||||||
|
'''Transforms the results of the black box into an entry'''
|
||||||
|
return output
|
||||||
|
|
||||||
|
def box(self):
|
||||||
|
raise NotImplementedError('You should define the behavior of this plugin')
|
||||||
|
|
||||||
|
def analyse_entries(self, entries, params):
|
||||||
|
for entry in entries:
|
||||||
|
input = self.input(entry=entry, params=params)
|
||||||
|
results = self.box(input=input, params=params)
|
||||||
|
yield self.output(output=results, entry=entry, params=params)
|
||||||
|
|
||||||
|
|
||||||
|
class TextBox(Box):
|
||||||
|
'''A black box plugin that takes only text as input'''
|
||||||
|
|
||||||
|
def input(self, entry, params):
|
||||||
|
entry = super(TextBox, self).input(entry, params)
|
||||||
|
return entry['nif:isString']
|
||||||
|
|
||||||
|
|
||||||
|
class SentimentBox(TextBox, SentimentPlugin):
|
||||||
|
'''
|
||||||
|
A box plugin where the output is only a polarity label or a tuple (polarity, polarityValue)
|
||||||
|
'''
|
||||||
|
|
||||||
|
def output(self, output, entry, **kwargs):
|
||||||
|
s = models.Sentiment()
|
||||||
|
try:
|
||||||
|
label, value = output
|
||||||
|
except ValueError:
|
||||||
|
label, value = output, None
|
||||||
|
s.prov(self)
|
||||||
|
s.polarity = label
|
||||||
|
if value is not None:
|
||||||
|
s.polarityValue = value
|
||||||
|
entry.sentiments.append(s)
|
||||||
|
return entry
|
||||||
|
|
||||||
|
|
||||||
|
class EmotionBox(TextBox, EmotionPlugin):
|
||||||
|
'''
|
||||||
|
A box plugin where the output is only an a tuple of emotion labels
|
||||||
|
'''
|
||||||
|
|
||||||
|
def output(self, output, entry, **kwargs):
|
||||||
|
if not isinstance(output, list):
|
||||||
|
output = [output]
|
||||||
|
s = models.EmotionSet()
|
||||||
|
entry.emotions.append(s)
|
||||||
|
for label in output:
|
||||||
|
e = models.Emotion(onyx__hasEmotionCategory=label)
|
||||||
|
s.append(e)
|
||||||
|
return entry
|
||||||
|
|
||||||
|
|
||||||
|
class MappingMixin(object):
|
||||||
|
|
||||||
|
def output(self, output, entry, params):
|
||||||
|
output = self.mappings.get(output,
|
||||||
|
self.mappings.get('default', output))
|
||||||
|
return super(MappingMixin, self).output(output=output,
|
||||||
|
entry=entry,
|
||||||
|
params=params)
|
||||||
|
|
||||||
|
|
||||||
class ShelfMixin(object):
|
class ShelfMixin(object):
|
||||||
@property
|
@property
|
||||||
def sh(self):
|
def sh(self):
|
||||||
@ -269,9 +356,13 @@ class ShelfMixin(object):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def shelf_file(self):
|
def shelf_file(self):
|
||||||
if 'shelf_file' not in self or not self['shelf_file']:
|
if not hasattr(self, '_shelf_file') or not self._shelf_file:
|
||||||
self.shelf_file = os.path.join(self.data_folder, self.name + '.p')
|
self._shelf_file = os.path.join(self.data_folder, self.name + '.p')
|
||||||
return self['shelf_file']
|
return self._shelf_file
|
||||||
|
|
||||||
|
@shelf_file.setter
|
||||||
|
def shelf_file(self, value):
|
||||||
|
self._shelf_file = value
|
||||||
|
|
||||||
def save(self):
|
def save(self):
|
||||||
logger.debug('saving pickle')
|
logger.debug('saving pickle')
|
||||||
|
@ -27,7 +27,7 @@ class Rand(SentimentPlugin):
|
|||||||
'''Run several random analyses.'''
|
'''Run several random analyses.'''
|
||||||
params = dict()
|
params = dict()
|
||||||
results = list()
|
results = list()
|
||||||
for i in range(20):
|
for i in range(50):
|
||||||
res = next(self.analyse_entry(Entry(nif__isString="Hello"),
|
res = next(self.analyse_entry(Entry(nif__isString="Hello"),
|
||||||
params))
|
params))
|
||||||
res.validate()
|
res.validate()
|
||||||
|
@ -36,6 +36,15 @@ def check_template(indict, template):
|
|||||||
pprint.pformat(template)))
|
pprint.pformat(template)))
|
||||||
|
|
||||||
|
|
||||||
|
def convert_dictionary(original, mappings):
|
||||||
|
result = {}
|
||||||
|
for key, value in original.items():
|
||||||
|
if key in mappings:
|
||||||
|
key = mappings[key]
|
||||||
|
result[key] = value
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
def easy_load(app=None, plugin_list=None, plugin_folder=None, **kwargs):
|
def easy_load(app=None, plugin_list=None, plugin_folder=None, **kwargs):
|
||||||
'''
|
'''
|
||||||
Run a server with a specific plugin.
|
Run a server with a specific plugin.
|
||||||
|
@ -15,7 +15,8 @@ from senpy.models import (Emotion,
|
|||||||
SentimentPlugin,
|
SentimentPlugin,
|
||||||
Plugins,
|
Plugins,
|
||||||
from_string,
|
from_string,
|
||||||
from_dict)
|
from_dict,
|
||||||
|
subtypes)
|
||||||
from senpy import plugins
|
from senpy import plugins
|
||||||
from pprint import pprint
|
from pprint import pprint
|
||||||
|
|
||||||
@ -134,6 +135,11 @@ class ModelsTest(TestCase):
|
|||||||
s = str(r)
|
s = str(r)
|
||||||
assert "_testing" not in s
|
assert "_testing" not in s
|
||||||
|
|
||||||
|
def test_serialize(self):
|
||||||
|
for k, v in subtypes().items():
|
||||||
|
e = v()
|
||||||
|
e.serialize()
|
||||||
|
|
||||||
def test_turtle(self):
|
def test_turtle(self):
|
||||||
"""Any model should be serializable as a turtle file"""
|
"""Any model should be serializable as a turtle file"""
|
||||||
ana = EmotionAnalysis()
|
ana = EmotionAnalysis()
|
||||||
|
@ -13,6 +13,10 @@ from senpy.plugins.conversion.emotion.centroids import CentroidConversion
|
|||||||
|
|
||||||
class ShelfDummyPlugin(plugins.SentimentPlugin, plugins.ShelfMixin):
|
class ShelfDummyPlugin(plugins.SentimentPlugin, plugins.ShelfMixin):
|
||||||
'''Dummy plugin for tests.'''
|
'''Dummy plugin for tests.'''
|
||||||
|
name = 'Shelf'
|
||||||
|
version = 0
|
||||||
|
author = 'the senpy community'
|
||||||
|
|
||||||
def activate(self, *args, **kwargs):
|
def activate(self, *args, **kwargs):
|
||||||
if 'counter' not in self.sh:
|
if 'counter' not in self.sh:
|
||||||
self.sh['counter'] = 0
|
self.sh['counter'] = 0
|
||||||
@ -41,6 +45,16 @@ class PluginsTest(TestCase):
|
|||||||
self.shelf_dir = tempfile.mkdtemp()
|
self.shelf_dir = tempfile.mkdtemp()
|
||||||
self.shelf_file = os.path.join(self.shelf_dir, "shelf")
|
self.shelf_file = os.path.join(self.shelf_dir, "shelf")
|
||||||
|
|
||||||
|
def test_serialize(self):
|
||||||
|
'''A plugin should be serializable and de-serializable'''
|
||||||
|
dummy = ShelfDummyPlugin()
|
||||||
|
dummy.serialize()
|
||||||
|
|
||||||
|
def test_jsonld(self):
|
||||||
|
'''A plugin should be serializable and de-serializable'''
|
||||||
|
dummy = ShelfDummyPlugin()
|
||||||
|
dummy.jsonld()
|
||||||
|
|
||||||
def test_shelf_file(self):
|
def test_shelf_file(self):
|
||||||
a = ShelfDummyPlugin(
|
a = ShelfDummyPlugin(
|
||||||
info={'name': 'default_shelve_file',
|
info={'name': 'default_shelve_file',
|
||||||
@ -187,6 +201,61 @@ class PluginsTest(TestCase):
|
|||||||
})
|
})
|
||||||
assert 'example' in a.extra_params
|
assert 'example' in a.extra_params
|
||||||
|
|
||||||
|
def test_box(self):
|
||||||
|
|
||||||
|
class MyBox(plugins.Box):
|
||||||
|
''' Vague description'''
|
||||||
|
|
||||||
|
author = 'me'
|
||||||
|
version = 0
|
||||||
|
|
||||||
|
def input(self, entry, **kwargs):
|
||||||
|
return entry.text
|
||||||
|
|
||||||
|
def box(self, input, **kwargs):
|
||||||
|
return 'SIGN' in input
|
||||||
|
|
||||||
|
def output(self, output, entry, **kwargs):
|
||||||
|
if output:
|
||||||
|
entry.myAnnotation = 'DETECTED'
|
||||||
|
return entry
|
||||||
|
|
||||||
|
test_cases = [
|
||||||
|
{
|
||||||
|
'input': "nothing here",
|
||||||
|
'expected': {'myAnnotation': 'DETECTED'},
|
||||||
|
'should_fail': True
|
||||||
|
}, {
|
||||||
|
'input': "SIGN",
|
||||||
|
'expected': {'myAnnotation': 'DETECTED'}
|
||||||
|
}]
|
||||||
|
|
||||||
|
MyBox().test()
|
||||||
|
|
||||||
|
def test_sentimentbox(self):
|
||||||
|
|
||||||
|
class SentimentBox(plugins.MappingMixin, plugins.SentimentBox):
|
||||||
|
''' Vague description'''
|
||||||
|
|
||||||
|
author = 'me'
|
||||||
|
version = 0
|
||||||
|
|
||||||
|
mappings = {'happy': 'marl:Positive', 'sad': 'marl:Negative'}
|
||||||
|
|
||||||
|
def box(self, input, **kwargs):
|
||||||
|
return 'happy' if ':)' in input else 'sad'
|
||||||
|
|
||||||
|
test_cases = [
|
||||||
|
{
|
||||||
|
'input': 'a happy face :)',
|
||||||
|
'polarity': 'marl:Positive'
|
||||||
|
}, {
|
||||||
|
'input': "Nothing",
|
||||||
|
'polarity': 'marl:Negative'
|
||||||
|
}]
|
||||||
|
|
||||||
|
SentimentBox().test()
|
||||||
|
|
||||||
def test_conversion_centroids(self):
|
def test_conversion_centroids(self):
|
||||||
info = {
|
info = {
|
||||||
"name": "CentroidTest",
|
"name": "CentroidTest",
|
||||||
|
Loading…
Reference in New Issue
Block a user