mirror of
https://github.com/gsi-upm/senpy
synced 2024-12-22 13:08:13 +00:00
WIP: working on a full refactor for v2.0
This is still not functional, because it involves a LOT of changes to the basic structure of the project. Some of the main changes can be seen in the CHANGELOG.md file, if you're interested, but it boils down to simplifying the logic of plugins (no more activation/deactivation shenanigans), more robust typing and use of schemas (pydantic) to avoid inconsistencies and user errors.
This commit is contained in:
parent
9414b0e3e6
commit
54e4dcd5d4
11
CHANGELOG.md
11
CHANGELOG.md
@ -5,17 +5,20 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
### Changed
|
||||||
|
* Changed the underlying model to use `pydantic` models
|
||||||
|
* Plugin interface slightly changed. Activation should be performed in the `activate` method
|
||||||
|
* data directory selection logic is slightly modified, and will choose one of the following (in this order): `data_folder` (argument), `$SENPY_DATA` or `$CWD`
|
||||||
|
* Plugins cannot be deactivated
|
||||||
### Fixed
|
### Fixed
|
||||||
* Tests now use strict mode to detect errors on optional plugins
|
* Tests now detect errors on optional plugins
|
||||||
### Added
|
### Added
|
||||||
* The code of many senpy community plugins have been included by default. However, additional files (e.g., licensed data) and/or installing additional dependencies may be necessary for some plugins. Read each plugin's documentation for more information.
|
* The code of many senpy community plugins have been included by default. However, additional files (e.g., licensed data) and/or installing additional dependencies may be necessary for some plugins. Read each plugin's documentation for more information.
|
||||||
* `optional` attribute in plugins. Optional plugins may fail to load or activate but the server will be started regardless, unless running in strict mode
|
* `optional` attribute in plugins. Optional plugins may fail to load or activate but the server will be started regardless, unless running in strict mode
|
||||||
* `--strict` flag to set strict mode. In this mode, the server will not start when optional plugins fail to load
|
|
||||||
* Option in shelf plugins to ignore pickling errors
|
* Option in shelf plugins to ignore pickling errors
|
||||||
### Removed
|
### Removed
|
||||||
* `--only-install`, `--only-test` and `--only-list` flags were removed in favor of `--no-run` + `--install`/`--test`/`--dependencies`
|
* `--only-install`, `--only-test` and `--only-list` flags were removed in favor of `--no-run` + `--install`/`--test`/`--dependencies`
|
||||||
### Changed
|
|
||||||
* data directory selection logic is slightly modified, and will choose one of the following (in this order): `data_folder` (argument), `$SENPY_DATA` or `$CWD`
|
|
||||||
|
|
||||||
## [1.0.6]
|
## [1.0.6]
|
||||||
### Fixed
|
### Fixed
|
||||||
|
9
TODO.md
Normal file
9
TODO.md
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
* Upload context to www.gsi.upm.es/ontologies/senpy
|
||||||
|
* Use context from gsi.upm.es
|
||||||
|
* Add example of morality analysis
|
||||||
|
|
||||||
|
* Simplify plugin loading
|
||||||
|
* Simplify models
|
||||||
|
* Remove json schemas and generate pydantic objects
|
||||||
|
* Remove some of the meta-programming magic
|
||||||
|
* Migrate to openapi/quart
|
42
docs/examples/results/example-moral.json
Normal file
42
docs/examples/results/example-moral.json
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
{
|
||||||
|
"@context": "http://senpy.gsi.upm.es/api/contexts/YXBpP2FsZ289ZW1",
|
||||||
|
"@id": ":Result1",
|
||||||
|
"@type": "results",
|
||||||
|
"activities": [
|
||||||
|
{
|
||||||
|
"@id": ":MoralAnalysis1_Activity",
|
||||||
|
"@type": "amor:MoralValueAnalysis",
|
||||||
|
"prov:wasAssociatedWith": ":MoralAnalysis1",
|
||||||
|
"amor:usedMoralValueModel": "amor-mft:MoralFoundationsTheory",
|
||||||
|
"amor:analysed": "news1",
|
||||||
|
"amor:usedMLModel": ":model1",
|
||||||
|
"prov:generated": ":annotation3"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"entries": [
|
||||||
|
{
|
||||||
|
"@id": "http://micro.blog/status1",
|
||||||
|
"@type": [
|
||||||
|
"nif:RFC5147String",
|
||||||
|
"nif:Context"
|
||||||
|
],
|
||||||
|
"nif:isString": "Dear Microsoft, put your Windows Phone on your newest #open technology program. You'll be awesome. #opensource",
|
||||||
|
"morality": [
|
||||||
|
{
|
||||||
|
"@id": ":annotation3",
|
||||||
|
"@type": "amor:MoralValueAnnotation",
|
||||||
|
"nif:beginIndex": 80,
|
||||||
|
"nif:endIndex": 97,
|
||||||
|
"amor:hasMoralValueCategory": "mft:Authority",
|
||||||
|
"amor:confidence": 0.75,
|
||||||
|
"amor-mft:hasPolarityIntensity": 0.2,
|
||||||
|
"amor:annotated": "http://micro.blog/status1",
|
||||||
|
"nif:anchorOf": "You'll be awesome.",
|
||||||
|
"prov:wasGeneratedBy": ":MoralAnalysis1_Activity"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"emotions": [
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
@ -1,53 +0,0 @@
|
|||||||
#
|
|
||||||
# Copyright 2014 Grupo de Sistemas Inteligentes (GSI) DIT, UPM
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
# you may not use this file except in compliance with the License.
|
|
||||||
# You may obtain a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
# See the License for the specific language governing permissions and
|
|
||||||
# limitations under the License.
|
|
||||||
#
|
|
||||||
|
|
||||||
from senpy import AnalysisPlugin
|
|
||||||
|
|
||||||
import multiprocessing
|
|
||||||
|
|
||||||
|
|
||||||
def _train(process_number):
|
|
||||||
return process_number
|
|
||||||
|
|
||||||
|
|
||||||
class Async(AnalysisPlugin):
|
|
||||||
'''An example of an asynchronous module'''
|
|
||||||
author = '@balkian'
|
|
||||||
version = '0.2'
|
|
||||||
sync = False
|
|
||||||
|
|
||||||
def _do_async(self, num_processes):
|
|
||||||
pool = multiprocessing.Pool(processes=num_processes)
|
|
||||||
values = sorted(pool.map(_train, range(num_processes)))
|
|
||||||
|
|
||||||
return values
|
|
||||||
|
|
||||||
def activate(self):
|
|
||||||
self.value = self._do_async(4)
|
|
||||||
|
|
||||||
def analyse_entry(self, entry, params):
|
|
||||||
values = self._do_async(2)
|
|
||||||
entry.async_values = values
|
|
||||||
yield entry
|
|
||||||
|
|
||||||
test_cases = [
|
|
||||||
{
|
|
||||||
'input': 'any',
|
|
||||||
'expected': {
|
|
||||||
'async_values': [0, 1]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
@ -24,8 +24,8 @@ import basic
|
|||||||
class BasicAnalyseEntry(plugins.SentimentPlugin):
|
class BasicAnalyseEntry(plugins.SentimentPlugin):
|
||||||
'''Equivalent to Basic, implementing the analyse_entry method'''
|
'''Equivalent to Basic, implementing the analyse_entry method'''
|
||||||
|
|
||||||
author = '@balkian'
|
author: str = '@balkian'
|
||||||
version = '0.1'
|
version: str = '0.1'
|
||||||
|
|
||||||
mappings = {
|
mappings = {
|
||||||
'pos': 'marl:Positive',
|
'pos': 'marl:Positive',
|
||||||
@ -43,7 +43,7 @@ class BasicAnalyseEntry(plugins.SentimentPlugin):
|
|||||||
entry.sentiments.append(s)
|
entry.sentiments.append(s)
|
||||||
yield entry
|
yield entry
|
||||||
|
|
||||||
test_cases = [{
|
test_cases: list[dict] = [{
|
||||||
'input': 'Hello :)',
|
'input': 'Hello :)',
|
||||||
'polarity': 'marl:Positive'
|
'polarity': 'marl:Positive'
|
||||||
}, {
|
}, {
|
||||||
|
@ -24,8 +24,8 @@ import basic
|
|||||||
class BasicBox(SentimentBox):
|
class BasicBox(SentimentBox):
|
||||||
''' A modified version of Basic that also does converts annotations manually'''
|
''' A modified version of Basic that also does converts annotations manually'''
|
||||||
|
|
||||||
author = '@balkian'
|
author: str = '@balkian'
|
||||||
version = '0.1'
|
version: str = '0.1'
|
||||||
|
|
||||||
def predict_one(self, features, **kwargs):
|
def predict_one(self, features, **kwargs):
|
||||||
output = basic.get_polarity(features[0])
|
output = basic.get_polarity(features[0])
|
||||||
@ -35,7 +35,7 @@ class BasicBox(SentimentBox):
|
|||||||
return [0, 0, 1]
|
return [0, 0, 1]
|
||||||
return [0, 1, 0]
|
return [0, 1, 0]
|
||||||
|
|
||||||
test_cases = [{
|
test_cases: list[dict] = [{
|
||||||
'input': 'Hello :)',
|
'input': 'Hello :)',
|
||||||
'polarity': 'marl:Positive'
|
'polarity': 'marl:Positive'
|
||||||
}, {
|
}, {
|
||||||
|
@ -25,8 +25,8 @@ import basic
|
|||||||
class Basic(SentimentBox):
|
class Basic(SentimentBox):
|
||||||
'''Provides sentiment annotation using a lexicon'''
|
'''Provides sentiment annotation using a lexicon'''
|
||||||
|
|
||||||
author = '@balkian'
|
author: str = '@balkian'
|
||||||
version = '0.1'
|
version: str = '0.1'
|
||||||
|
|
||||||
def predict_one(self, features, **kwargs):
|
def predict_one(self, features, **kwargs):
|
||||||
output = basic.get_polarity(features[0])
|
output = basic.get_polarity(features[0])
|
||||||
@ -36,7 +36,7 @@ class Basic(SentimentBox):
|
|||||||
return [0, 1, 0]
|
return [0, 1, 0]
|
||||||
return [0, 0, 1]
|
return [0, 0, 1]
|
||||||
|
|
||||||
test_cases = [{
|
test_cases: list[dict] = [{
|
||||||
'input': u'Hello :)',
|
'input': u'Hello :)',
|
||||||
'polarity': 'marl:Positive'
|
'polarity': 'marl:Positive'
|
||||||
}, {
|
}, {
|
||||||
|
@ -25,8 +25,8 @@ import basic
|
|||||||
class Dictionary(plugins.SentimentPlugin):
|
class Dictionary(plugins.SentimentPlugin):
|
||||||
'''Sentiment annotation using a configurable lexicon'''
|
'''Sentiment annotation using a configurable lexicon'''
|
||||||
|
|
||||||
author = '@balkian'
|
author: str = '@balkian'
|
||||||
version = '0.2'
|
version: str = '0.2'
|
||||||
|
|
||||||
dictionaries = [basic.emojis, basic.emoticons]
|
dictionaries = [basic.emojis, basic.emoticons]
|
||||||
|
|
||||||
@ -42,7 +42,7 @@ class Dictionary(plugins.SentimentPlugin):
|
|||||||
entry.sentiments.append(s)
|
entry.sentiments.append(s)
|
||||||
yield entry
|
yield entry
|
||||||
|
|
||||||
test_cases = [{
|
test_cases: list[dict] = [{
|
||||||
'input': 'Hello :)',
|
'input': 'Hello :)',
|
||||||
'polarity': 'marl:Positive'
|
'polarity': 'marl:Positive'
|
||||||
}, {
|
}, {
|
||||||
@ -61,7 +61,7 @@ class EmojiOnly(Dictionary):
|
|||||||
'''Sentiment annotation with a basic lexicon of emojis'''
|
'''Sentiment annotation with a basic lexicon of emojis'''
|
||||||
dictionaries = [basic.emojis]
|
dictionaries = [basic.emojis]
|
||||||
|
|
||||||
test_cases = [{
|
test_cases: list[dict] = [{
|
||||||
'input': 'Hello :)',
|
'input': 'Hello :)',
|
||||||
'polarity': 'marl:Neutral'
|
'polarity': 'marl:Neutral'
|
||||||
}, {
|
}, {
|
||||||
@ -80,7 +80,7 @@ class EmoticonsOnly(Dictionary):
|
|||||||
'''Sentiment annotation with a basic lexicon of emoticons'''
|
'''Sentiment annotation with a basic lexicon of emoticons'''
|
||||||
dictionaries = [basic.emoticons]
|
dictionaries = [basic.emoticons]
|
||||||
|
|
||||||
test_cases = [{
|
test_cases: list[dict] = [{
|
||||||
'input': 'Hello :)',
|
'input': 'Hello :)',
|
||||||
'polarity': 'marl:Positive'
|
'polarity': 'marl:Positive'
|
||||||
}, {
|
}, {
|
||||||
@ -102,7 +102,7 @@ class Salutes(Dictionary):
|
|||||||
'marl:Negative': ['Good bye', ]
|
'marl:Negative': ['Good bye', ]
|
||||||
}]
|
}]
|
||||||
|
|
||||||
test_cases = [{
|
test_cases: list[dict] = [{
|
||||||
'input': 'Hello :)',
|
'input': 'Hello :)',
|
||||||
'polarity': 'marl:Positive'
|
'polarity': 'marl:Positive'
|
||||||
}, {
|
}, {
|
||||||
|
@ -19,15 +19,15 @@ from senpy import AnalysisPlugin, easy
|
|||||||
|
|
||||||
class Dummy(AnalysisPlugin):
|
class Dummy(AnalysisPlugin):
|
||||||
'''This is a dummy self-contained plugin'''
|
'''This is a dummy self-contained plugin'''
|
||||||
author = '@balkian'
|
author: str = '@balkian'
|
||||||
version = '0.1'
|
version: str = '0.1'
|
||||||
|
|
||||||
def analyse_entry(self, entry, params):
|
def analyse_entry(self, entry, params):
|
||||||
entry['nif:isString'] = entry['nif:isString'][::-1]
|
entry.text = entry.text[::-1]
|
||||||
entry.reversed = entry.get('reversed', 0) + 1
|
entry.reversed = entry.get('reversed', 0) + 1
|
||||||
yield entry
|
yield entry
|
||||||
|
|
||||||
test_cases = [{
|
test_cases: list[dict] = [{
|
||||||
'entry': {
|
'entry': {
|
||||||
'nif:isString': 'Hello',
|
'nif:isString': 'Hello',
|
||||||
},
|
},
|
||||||
|
@ -19,9 +19,9 @@ from senpy import AnalysisPlugin, easy
|
|||||||
|
|
||||||
class DummyRequired(AnalysisPlugin):
|
class DummyRequired(AnalysisPlugin):
|
||||||
'''This is a dummy self-contained plugin'''
|
'''This is a dummy self-contained plugin'''
|
||||||
author = '@balkian'
|
author: str = '@balkian'
|
||||||
version = '0.1'
|
version: str = '0.1'
|
||||||
extra_params = {
|
extra_params: dict = {
|
||||||
'example': {
|
'example': {
|
||||||
'description': 'An example parameter',
|
'description': 'An example parameter',
|
||||||
'required': True,
|
'required': True,
|
||||||
@ -30,11 +30,11 @@ class DummyRequired(AnalysisPlugin):
|
|||||||
}
|
}
|
||||||
|
|
||||||
def analyse_entry(self, entry, params):
|
def analyse_entry(self, entry, params):
|
||||||
entry['nif:isString'] = entry['nif:isString'][::-1]
|
entry.text = entry.text[::-1]
|
||||||
entry.reversed = entry.get('reversed', 0) + 1
|
entry.reversed = entry.get('reversed', 0) + 1
|
||||||
yield entry
|
yield entry
|
||||||
|
|
||||||
test_cases = [{
|
test_cases: list[dict] = [{
|
||||||
'entry': {
|
'entry': {
|
||||||
'nif:isString': 'Hello',
|
'nif:isString': 'Hello',
|
||||||
},
|
},
|
||||||
|
@ -22,11 +22,11 @@ from senpy.models import EmotionSet, Emotion, Entry
|
|||||||
|
|
||||||
class EmoRand(EmotionPlugin):
|
class EmoRand(EmotionPlugin):
|
||||||
'''A sample plugin that returns a random emotion annotation'''
|
'''A sample plugin that returns a random emotion annotation'''
|
||||||
name = 'emotion-random'
|
name: str = 'emotion-random'
|
||||||
author = '@balkian'
|
author: str = '@balkian'
|
||||||
version = '0.1'
|
version: str = '0.1'
|
||||||
url = "https://github.com/gsi-upm/senpy-plugins-community"
|
url: str = "https://github.com/gsi-upm/senpy-plugins-community"
|
||||||
onyx__usesEmotionModel = "emoml:big6"
|
usesEmotionModel: str = "emoml:big6"
|
||||||
|
|
||||||
def analyse_entry(self, entry, activity):
|
def analyse_entry(self, entry, activity):
|
||||||
category = "emoml:big6happiness"
|
category = "emoml:big6happiness"
|
||||||
|
63
example-plugins/moral/example.ttl
Normal file
63
example-plugins/moral/example.ttl
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
@prefix : <http://www.gsi.upm.es/ontologies/amor/examples#> .
|
||||||
|
@prefix amor: <http://www.gsi.upm.es/ontologies/amor/ns#> .
|
||||||
|
@prefix amor-bhv: <http://www.gsi.upm.es/ontologies/amor/models/bhv/ns#> .
|
||||||
|
@prefix amor-mft: <http://www.gsi.upm.es/ontologies/amor/models/mft/ns#> .
|
||||||
|
@prefix bhv: <http://www.gsi.upm.es/ontologies/bhv#> .
|
||||||
|
@prefix mft: <http://www.gsi.upm.es/ontologies/mft/ns#> .
|
||||||
|
@prefix mls: <http://www.w3.org/ns/mls#> .
|
||||||
|
@prefix owl: <http://www.w3.org/2002/07/owl#> .
|
||||||
|
@prefix prov: <http://www.w3.org/ns/prov#> .
|
||||||
|
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
|
||||||
|
@prefix schema: <http://schema.org/> .
|
||||||
|
|
||||||
|
|
||||||
|
:news1 a owl:NamedIndividual, schema:NewsArticle ;
|
||||||
|
schema:articleBody "Director Comey says the probe into last year's US election would assess if crimes were committed."^^xsd:string ;
|
||||||
|
schema:datePublished "2017-03-20T20:30:54+00:00"^^schema:Date ;
|
||||||
|
schema:headline "Trump Russia claims: FBI's Comey confirms investigation of election 'interference'"^^xsd:string ;
|
||||||
|
schema:image <http://ichef-1.bbci.co.uk/news/560/media/images/75306000/jpg/_75306515_line976.jpg>,
|
||||||
|
<http://ichef.bbci.co.uk/news/560/cpsprodpb/8AB9/production/_95231553_comey2.jpg>,
|
||||||
|
<http://ichef.bbci.co.uk/news/560/cpsprodpb/17519/production/_95231559_committee.jpg>,
|
||||||
|
<http://ichef.bbci.co.uk/news/560/cpsprodpb/CC81/production/_95235325_f704a6dc-c017-4971-aac3-04c03eb097fb.jpg>,
|
||||||
|
<http://ichef-1.bbci.co.uk/news/560/cpsprodpb/11AA1/production/_95235327_c0b59f9e-316e-4641-aa7e-3fec6daea62b.jpg>,
|
||||||
|
<http://ichef.bbci.co.uk/news/560/cpsprodpb/0F99/production/_95239930_trumptweet.png>,
|
||||||
|
<http://ichef-1.bbci.co.uk/news/560/cpsprodpb/10DFA/production/_95241196_mediaitem95241195.jpg>,
|
||||||
|
<http://ichef.bbci.co.uk/news/560/cpsprodpb/2CA0/production/_95242411_comey.jpg>,
|
||||||
|
<http://ichef.bbci.co.uk/news/560/cpsprodpb/11318/production/_95242407_mediaitem95242406.jpg>,
|
||||||
|
<http://ichef-1.bbci.co.uk/news/560/cpsprodpb/BCED/production/_92856384_line976.jpg>,
|
||||||
|
<http://ichef-1.bbci.co.uk/news/560/cpsprodpb/12B64/production/_95244667_mediaitem95244666.jpg> ;
|
||||||
|
schema:mainEntityOfPage <http://www.bbc.com/news/world-us-canada-39324587> ;
|
||||||
|
schema:publisher :bbc ;
|
||||||
|
schema:url <http://www.bbc.com/news/world-us-canada-39324587> .
|
||||||
|
|
||||||
|
:bbc a schema:Organization ;
|
||||||
|
schema:logo <http://www.bbc.co.uk/news/special/2015/newsspec_10857/bbc_news_logo.png?cb=1> ;
|
||||||
|
schema:name "BBC News"^^xsd:string .
|
||||||
|
|
||||||
|
|
||||||
|
:robot1 a prov:SoftwareAgent .
|
||||||
|
|
||||||
|
:model1 a mls:Model .
|
||||||
|
|
||||||
|
:logisticRegression a mls:Algorithm ;
|
||||||
|
rdfs:label "Logistic Regression"@en ,
|
||||||
|
"Regresión Logística"@es .
|
||||||
|
|
||||||
|
:run1 a mls:Run ;
|
||||||
|
mls:executes :wekaLogistic ;
|
||||||
|
mls:hasInput :credit-a ;
|
||||||
|
mls:hasOutput :model1 ;
|
||||||
|
mls:realizes :logisticRegression .
|
||||||
|
|
||||||
|
:analysis3 a amor:MoralValueAnalysis ;
|
||||||
|
prov:wasAssociatedWith :robot1 ;
|
||||||
|
amor:usedMoralValueModel amor-mft:MoralFoundationsTheory ;
|
||||||
|
amor:analysed :news1 ;
|
||||||
|
amor:usedMLModel :model1 ;
|
||||||
|
prov:generated :annotation3 .
|
||||||
|
|
||||||
|
:annotation3 a amor:MoralValueAnnotation ;
|
||||||
|
amor:hasMoralValueCategory mft:Authority ;
|
||||||
|
amor:confidence "0.75"^^xsd:float ;
|
||||||
|
amor-mft:hasPolarityIntensity "0.2"^^xsd:float ;
|
||||||
|
amor:annotated :news1 .
|
10
example-plugins/moral/moral_plugin.py
Normal file
10
example-plugins/moral/moral_plugin.py
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
from senpy.ns import amor, amor_bhv, amor_mft, prov
|
||||||
|
from senpy.plugins import MoralityPlugin
|
||||||
|
|
||||||
|
|
||||||
|
class DummyMoralityPlugin(MoralityPlugin):
|
||||||
|
moralValueModel: str = amor_mft["MoralFoundationTHeory"]
|
||||||
|
|
||||||
|
def annotate(self, entry, activity, **kwargs):
|
||||||
|
yield MoralAnnotation(amor_bhv['Conservation'],
|
||||||
|
confidence=1.8)
|
@ -21,7 +21,7 @@ from senpy.plugins import SentimentPlugin
|
|||||||
class NoOp(SentimentPlugin):
|
class NoOp(SentimentPlugin):
|
||||||
'''This plugin does nothing. Literally nothing.'''
|
'''This plugin does nothing. Literally nothing.'''
|
||||||
|
|
||||||
version = 0
|
version: str = 0
|
||||||
|
|
||||||
def analyse_entry(self, entry, *args, **kwargs):
|
def analyse_entry(self, entry, *args, **kwargs):
|
||||||
yield entry
|
yield entry
|
||||||
|
@ -24,11 +24,10 @@ import basic
|
|||||||
|
|
||||||
class ParameterizedDictionary(plugins.SentimentPlugin):
|
class ParameterizedDictionary(plugins.SentimentPlugin):
|
||||||
'''This is a basic self-contained plugin'''
|
'''This is a basic self-contained plugin'''
|
||||||
|
author: str = '@balkian'
|
||||||
|
version: str = '0.2'
|
||||||
|
|
||||||
author = '@balkian'
|
extra_params: dict = {
|
||||||
version = '0.2'
|
|
||||||
|
|
||||||
extra_params = {
|
|
||||||
'positive-words': {
|
'positive-words': {
|
||||||
'description': 'Comma-separated list of words that are considered positive',
|
'description': 'Comma-separated list of words that are considered positive',
|
||||||
'aliases': ['positive'],
|
'aliases': ['positive'],
|
||||||
@ -56,7 +55,7 @@ class ParameterizedDictionary(plugins.SentimentPlugin):
|
|||||||
entry.sentiments.append(s)
|
entry.sentiments.append(s)
|
||||||
yield entry
|
yield entry
|
||||||
|
|
||||||
test_cases = [
|
test_cases: list[dict] = [
|
||||||
{
|
{
|
||||||
'input': 'Hello :)',
|
'input': 'Hello :)',
|
||||||
'polarity': 'marl:Positive',
|
'polarity': 'marl:Positive',
|
||||||
|
@ -20,12 +20,12 @@ from senpy import SentimentPlugin, Sentiment, Entry
|
|||||||
|
|
||||||
class RandSent(SentimentPlugin):
|
class RandSent(SentimentPlugin):
|
||||||
'''A sample plugin that returns a random sentiment annotation'''
|
'''A sample plugin that returns a random sentiment annotation'''
|
||||||
name = 'sentiment-random'
|
name: str = 'sentiment-random'
|
||||||
author = "@balkian"
|
author: str = "@balkian"
|
||||||
version = '0.1'
|
version: str = '0.1'
|
||||||
url = "https://github.com/gsi-upm/senpy-plugins-community"
|
url: str = "https://github.com/gsi-upm/senpy-plugins-community"
|
||||||
marl__maxPolarityValue = '1'
|
maxPolarityValue: float = 1
|
||||||
marl__minPolarityValue = "-1"
|
minPolarityValue: float = -1
|
||||||
|
|
||||||
def analyse_entry(self, entry, activity):
|
def analyse_entry(self, entry, activity):
|
||||||
polarity_value = max(-1, min(1, random.gauss(0.2, 0.2)))
|
polarity_value = max(-1, min(1, random.gauss(0.2, 0.2)))
|
||||||
|
@ -22,8 +22,8 @@ from mypipeline import pipeline
|
|||||||
class PipelineSentiment(SentimentBox):
|
class PipelineSentiment(SentimentBox):
|
||||||
'''This is a pipeline plugin that wraps a classifier defined in another module
|
'''This is a pipeline plugin that wraps a classifier defined in another module
|
||||||
(mypipeline).'''
|
(mypipeline).'''
|
||||||
author = '@balkian'
|
author: str = '@balkian'
|
||||||
version = 0.1
|
version: str = 0.1
|
||||||
maxPolarityValue = 1
|
maxPolarityValue = 1
|
||||||
minPolarityValue = -1
|
minPolarityValue = -1
|
||||||
|
|
||||||
@ -32,7 +32,7 @@ class PipelineSentiment(SentimentBox):
|
|||||||
return [1, 0, 0]
|
return [1, 0, 0]
|
||||||
return [0, 0, 1]
|
return [0, 0, 1]
|
||||||
|
|
||||||
test_cases = [
|
test_cases: list[dict] = [
|
||||||
{
|
{
|
||||||
'input': 'The sentiment for senpy should be positive :)',
|
'input': 'The sentiment for senpy should be positive :)',
|
||||||
'polarity': 'marl:Positive'
|
'polarity': 'marl:Positive'
|
||||||
|
@ -20,10 +20,10 @@ from time import sleep
|
|||||||
|
|
||||||
class Sleep(AnalysisPlugin):
|
class Sleep(AnalysisPlugin):
|
||||||
'''Dummy plugin to test async'''
|
'''Dummy plugin to test async'''
|
||||||
author = "@balkian"
|
author: str = "@balkian"
|
||||||
version = "0.2"
|
version: str = "0.2"
|
||||||
timeout = 0.05
|
timeout = 0.05
|
||||||
extra_params = {
|
extra_params: dict = {
|
||||||
"timeout": {
|
"timeout": {
|
||||||
"@id": "timeout_sleep",
|
"@id": "timeout_sleep",
|
||||||
"aliases": ["timeout", "to"],
|
"aliases": ["timeout", "to"],
|
||||||
@ -32,7 +32,8 @@ class Sleep(AnalysisPlugin):
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def activate(self, *args, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
|
super().__init__(**kwargs)
|
||||||
sleep(self.timeout)
|
sleep(self.timeout)
|
||||||
|
|
||||||
def analyse_entry(self, entry, params):
|
def analyse_entry(self, entry, params):
|
||||||
|
@ -13,3 +13,4 @@ scipy
|
|||||||
scikit-learn>=0.20
|
scikit-learn>=0.20
|
||||||
responses
|
responses
|
||||||
jmespath
|
jmespath
|
||||||
|
deprecation==2.1.0
|
||||||
|
@ -137,7 +137,7 @@ def main():
|
|||||||
'--fail',
|
'--fail',
|
||||||
action='store_true',
|
action='store_true',
|
||||||
default=False,
|
default=False,
|
||||||
help='Do not exit if some plugins fail to activate')
|
help='Do not exit if some plugins can not be instantiated')
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--enable-cors',
|
'--enable-cors',
|
||||||
'--cors',
|
'--cors',
|
||||||
@ -165,13 +165,14 @@ def main():
|
|||||||
install=args.install,
|
install=args.install,
|
||||||
strict=args.strict,
|
strict=args.strict,
|
||||||
data_folder=args.data_folder)
|
data_folder=args.data_folder)
|
||||||
|
|
||||||
folders = list(args.plugins_folder) if args.plugins_folder else []
|
folders = list(args.plugins_folder) if args.plugins_folder else []
|
||||||
if not folders:
|
if not folders:
|
||||||
folders.append(".")
|
folders.append(".")
|
||||||
for p in folders:
|
for p in folders:
|
||||||
sp.add_folder(p)
|
sp.add_folder(p)
|
||||||
|
|
||||||
plugins = sp.plugins(plugin_type=None, is_activated=False)
|
plugins = sp.plugins(plugin_type=None)
|
||||||
maxname = max(len(x.name) for x in plugins)
|
maxname = max(len(x.name) for x in plugins)
|
||||||
maxversion = max(len(str(x.version)) for x in plugins)
|
maxversion = max(len(str(x.version)) for x in plugins)
|
||||||
print('Found {} plugins:'.format(len(plugins)))
|
print('Found {} plugins:'.format(len(plugins)))
|
||||||
@ -187,7 +188,7 @@ def main():
|
|||||||
print('Listing dependencies')
|
print('Listing dependencies')
|
||||||
missing = []
|
missing = []
|
||||||
installed = []
|
installed = []
|
||||||
for plug in sp.plugins(is_activated=False):
|
for plug in sp.plugins():
|
||||||
inst, miss, nltkres = list_dependencies(plug)
|
inst, miss, nltkres = list_dependencies(plug)
|
||||||
if not any([inst, miss, nltkres]):
|
if not any([inst, miss, nltkres]):
|
||||||
continue
|
continue
|
||||||
@ -209,17 +210,11 @@ def main():
|
|||||||
sp.install_deps()
|
sp.install_deps()
|
||||||
|
|
||||||
if args.test:
|
if args.test:
|
||||||
sp.activate_all(sync=True)
|
easy_test(sp.plugins(), debug=args.debug)
|
||||||
easy_test(sp.plugins(is_activated=True), debug=args.debug)
|
|
||||||
|
|
||||||
if args.no_run:
|
if args.no_run:
|
||||||
return
|
return
|
||||||
|
|
||||||
sp.activate_all(sync=True)
|
|
||||||
if sp.strict:
|
|
||||||
inactive = sp.plugins(is_activated=False)
|
|
||||||
assert not inactive
|
|
||||||
|
|
||||||
print('Senpy version {}'.format(senpy.__version__))
|
print('Senpy version {}'.format(senpy.__version__))
|
||||||
print('Server running on port %s:%d. Ctrl+C to quit' % (args.host,
|
print('Server running on port %s:%d. Ctrl+C to quit' % (args.host,
|
||||||
args.port))
|
args.port))
|
||||||
@ -238,8 +233,6 @@ def main():
|
|||||||
debug=app.debug)
|
debug=app.debug)
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
print('Bye!')
|
print('Bye!')
|
||||||
sp.deactivate_all()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
main()
|
main()
|
||||||
|
21
senpy/api.py
21
senpy/api.py
@ -15,7 +15,8 @@
|
|||||||
#
|
#
|
||||||
|
|
||||||
from future.utils import iteritems
|
from future.utils import iteritems
|
||||||
from .models import Error, Results, Entry, from_string
|
from .models import Results, Entry, from_string
|
||||||
|
from .errors import Error, InvalidParams
|
||||||
import logging
|
import logging
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -169,7 +170,7 @@ EVAL_PARAMS = {
|
|||||||
"aliases": ["plug", "p", "plugins", "algorithms", 'algo', 'a', 'plugin'],
|
"aliases": ["plug", "p", "plugins", "algorithms", 'algo', 'a', 'plugin'],
|
||||||
"description": "Plugins to evaluate",
|
"description": "Plugins to evaluate",
|
||||||
"required": True,
|
"required": True,
|
||||||
"help": "See activated plugins in /plugins",
|
"help": "See plugins at /plugins",
|
||||||
"processor": API_PARAMS['algorithm']['processor']
|
"processor": API_PARAMS['algorithm']['processor']
|
||||||
},
|
},
|
||||||
"dataset": {
|
"dataset": {
|
||||||
@ -294,12 +295,7 @@ def parse_params(indict, *specs):
|
|||||||
wrong_params[param] = spec[param]
|
wrong_params[param] = spec[param]
|
||||||
if wrong_params:
|
if wrong_params:
|
||||||
logger.debug("Error parsing: %s", wrong_params)
|
logger.debug("Error parsing: %s", wrong_params)
|
||||||
message = Error(
|
raise InvalidParams(wrong_params)
|
||||||
status=400,
|
|
||||||
message='Missing or invalid parameters',
|
|
||||||
parameters=outdict,
|
|
||||||
errors=wrong_params)
|
|
||||||
raise message
|
|
||||||
return outdict
|
return outdict
|
||||||
|
|
||||||
|
|
||||||
@ -315,10 +311,9 @@ def get_all_params(plugins, *specs):
|
|||||||
def get_extra_params(plugins):
|
def get_extra_params(plugins):
|
||||||
'''Get a list of possible parameters given a list of plugins'''
|
'''Get a list of possible parameters given a list of plugins'''
|
||||||
params = {}
|
params = {}
|
||||||
extra_params = {}
|
extra_params: dict = {}
|
||||||
for plugin in plugins:
|
for plugin in plugins:
|
||||||
this_params = plugin.get('extra_params', {})
|
for k, v in plugin.extra_params.items():
|
||||||
for k, v in this_params.items():
|
|
||||||
if k not in extra_params:
|
if k not in extra_params:
|
||||||
extra_params[k] = {}
|
extra_params[k] = {}
|
||||||
extra_params[k][plugin.name] = v
|
extra_params[k][plugin.name] = v
|
||||||
@ -372,7 +367,7 @@ def parse_analyses(params, plugins):
|
|||||||
if not plugin:
|
if not plugin:
|
||||||
continue
|
continue
|
||||||
this_params = filter_params(params, plugin, i)
|
this_params = filter_params(params, plugin, i)
|
||||||
parsed = parse_params(this_params, plugin.get('extra_params', {}))
|
parsed = parse_params(this_params, plugin.extra_params)
|
||||||
analysis = plugin.activity(parsed)
|
analysis = plugin.activity(parsed)
|
||||||
analysis_list.append(analysis)
|
analysis_list.append(analysis)
|
||||||
return analysis_list
|
return analysis_list
|
||||||
@ -414,7 +409,7 @@ def parse_call(params):
|
|||||||
params = parse_params(params, NIF_PARAMS)
|
params = parse_params(params, NIF_PARAMS)
|
||||||
if params['informat'] == 'text':
|
if params['informat'] == 'text':
|
||||||
results = Results()
|
results = Results()
|
||||||
entry = Entry(nif__isString=params['input'], id='prefix:') # Use @base
|
entry = Entry(text=params['input'], id='prefix:') # Use @base
|
||||||
results.entries.append(entry)
|
results.entries.append(entry)
|
||||||
elif params['informat'] == 'json-ld':
|
elif params['informat'] == 'json-ld':
|
||||||
results = from_string(params['input'], cls=Results)
|
results = from_string(params['input'], cls=Results)
|
||||||
|
@ -20,7 +20,8 @@ Blueprints for Senpy
|
|||||||
"""
|
"""
|
||||||
from flask import (Blueprint, request, current_app, render_template, url_for,
|
from flask import (Blueprint, request, current_app, render_template, url_for,
|
||||||
jsonify, redirect)
|
jsonify, redirect)
|
||||||
from .models import Error, Response, Help, Plugins, read_schema, dump_schema, Datasets
|
from .models import BASE_CONTEXT, Help, Plugins, Datasets
|
||||||
|
from .errors import Error
|
||||||
from . import api
|
from . import api
|
||||||
from .version import __version__
|
from .version import __version__
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
@ -102,7 +103,7 @@ def index():
|
|||||||
|
|
||||||
@api_blueprint.route('/contexts/<code>')
|
@api_blueprint.route('/contexts/<code>')
|
||||||
def context(code=''):
|
def context(code=''):
|
||||||
context = Response._context
|
context = BASE_CONTEXT
|
||||||
context['@base'] = url_for('api.decode', code=code, _external=True)
|
context['@base'] = url_for('api.decode', code=code, _external=True)
|
||||||
context['endpoint'] = url_for('api.api_root', _external=True)
|
context['endpoint'] = url_for('api.api_root', _external=True)
|
||||||
return jsonify({"@context": context})
|
return jsonify({"@context": context})
|
||||||
@ -113,22 +114,58 @@ def decode(code):
|
|||||||
try:
|
try:
|
||||||
return redirect(decoded_url(code))
|
return redirect(decoded_url(code))
|
||||||
except Exception:
|
except Exception:
|
||||||
return Error('invalid URL').flask()
|
return to_flask(Error('invalid URL'))
|
||||||
|
|
||||||
|
|
||||||
@ns_blueprint.route('/') # noqa: F811
|
@ns_blueprint.route('/') # noqa: F811
|
||||||
def index():
|
def index():
|
||||||
context = Response._context.copy()
|
context = BASE_CONTEXT.copy()
|
||||||
context['endpoint'] = url_for('api.api_root', _external=True)
|
context['endpoint'] = url_for('api.api_root', _external=True)
|
||||||
return jsonify({"@context": context})
|
return jsonify({"@context": context})
|
||||||
|
|
||||||
|
|
||||||
@api_blueprint.route('/schemas/<schema>')
|
# from .models import read_schema, dump_schema
|
||||||
def schema(schema="definitions"):
|
#@api_blueprint.route('/schemas/<schema>')
|
||||||
try:
|
#def schema(schema="definitions"):
|
||||||
return dump_schema(read_schema(schema))
|
# try:
|
||||||
except Exception as ex: # Should be FileNotFoundError, but it's missing from py2
|
# return dump_schema(read_schema(schema))
|
||||||
return Error(message="Schema not found: {}".format(ex), status=404).flask()
|
# except Exception as ex: # Should be FileNotFoundError, but it's missing from py2
|
||||||
|
# return Error(message="Schema not found: {}".format(ex), status=404).flask()
|
||||||
|
|
||||||
|
def to_flask(resp,
|
||||||
|
in_headers=False,
|
||||||
|
headers=None,
|
||||||
|
outformat='json-ld',
|
||||||
|
**kwargs):
|
||||||
|
"""
|
||||||
|
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.
|
||||||
|
"""
|
||||||
|
headers = headers or {}
|
||||||
|
if isinstance(resp, Error):
|
||||||
|
status = resp.code
|
||||||
|
resp = resp.to_response()
|
||||||
|
else:
|
||||||
|
status = 200
|
||||||
|
|
||||||
|
kwargs["with_context"] = not in_headers
|
||||||
|
content, mimetype = resp.serialize(format=outformat,
|
||||||
|
with_mime=True,
|
||||||
|
**kwargs)
|
||||||
|
|
||||||
|
if outformat == 'json-ld' and in_headers:
|
||||||
|
headers.update({
|
||||||
|
"Link":
|
||||||
|
('<%s>;'
|
||||||
|
'rel="http://www.w3.org/ns/json-ld#context";'
|
||||||
|
' type="application/ld+json"' % kwargs.get('context_uri'))
|
||||||
|
})
|
||||||
|
return FlaskResponse(
|
||||||
|
response=content,
|
||||||
|
status=status,
|
||||||
|
headers=headers,
|
||||||
|
mimetype=mimetype)
|
||||||
|
|
||||||
|
|
||||||
def basic_api(f):
|
def basic_api(f):
|
||||||
@ -171,20 +208,20 @@ def basic_api(f):
|
|||||||
prefix = params.get('prefix')
|
prefix = params.get('prefix')
|
||||||
code = encode_url(prefix)
|
code = encode_url(prefix)
|
||||||
|
|
||||||
return response.flask(
|
return to_flask(response,
|
||||||
in_headers=params['in-headers'],
|
in_headers=params['in-headers'],
|
||||||
headers=headers,
|
headers=headers,
|
||||||
prefix=prefix or url_for_code(code),
|
prefix=prefix or url_for_code(code),
|
||||||
base=prefix,
|
base=prefix,
|
||||||
context_uri=url_for('api.context',
|
context_uri=url_for('api.context',
|
||||||
code=code,
|
code=code,
|
||||||
_external=True),
|
_external=True),
|
||||||
outformat=outformat,
|
outformat=outformat,
|
||||||
expanded=params['expanded-jsonld'],
|
expanded=params['expanded-jsonld'],
|
||||||
template=params.get('template'),
|
template=params.get('template'),
|
||||||
verbose=params['verbose'],
|
verbose=params['verbose'],
|
||||||
aliases=params['aliases'],
|
aliases=params['aliases'],
|
||||||
fields=params.get('fields'))
|
fields=params.get('fields'))
|
||||||
|
|
||||||
except (Exception) as ex:
|
except (Exception) as ex:
|
||||||
if current_app.debug or current_app.config['TESTING']:
|
if current_app.debug or current_app.config['TESTING']:
|
||||||
@ -195,11 +232,10 @@ def basic_api(f):
|
|||||||
response = ex
|
response = ex
|
||||||
response.parameters = raw_params
|
response.parameters = raw_params
|
||||||
logger.exception(ex)
|
logger.exception(ex)
|
||||||
return response.flask(
|
return to_flask(response,
|
||||||
outformat=outformat,
|
outformat=outformat,
|
||||||
expanded=params['expanded-jsonld'],
|
expanded=params['expanded-jsonld'],
|
||||||
verbose=params.get('verbose', True),
|
verbose=params.get('verbose', True))
|
||||||
)
|
|
||||||
|
|
||||||
return decorated_function
|
return decorated_function
|
||||||
|
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
from __future__ import print_function
|
from __future__ import print_function
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
from .models import Error
|
from .errors import Error
|
||||||
from .extensions import Senpy
|
from .extensions import Senpy
|
||||||
from . import api
|
from . import api
|
||||||
|
|
||||||
@ -49,11 +49,6 @@ def main_function(argv):
|
|||||||
sp = Senpy(default_plugins=default_plugins, plugin_folder=plugin_folder)
|
sp = Senpy(default_plugins=default_plugins, plugin_folder=plugin_folder)
|
||||||
request = api.parse_call(params)
|
request = api.parse_call(params)
|
||||||
algos = sp.get_plugins(request.parameters.get('algorithm', None))
|
algos = sp.get_plugins(request.parameters.get('algorithm', None))
|
||||||
if algos:
|
|
||||||
for algo in algos:
|
|
||||||
sp.activate_plugin(algo.name)
|
|
||||||
else:
|
|
||||||
sp.activate_all()
|
|
||||||
res = sp.analyse(request)
|
res = sp.analyse(request)
|
||||||
return res
|
return res
|
||||||
|
|
||||||
|
23
senpy/errors.py
Normal file
23
senpy/errors.py
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
from .models import ErrorResponse
|
||||||
|
|
||||||
|
class Error(Exception):
|
||||||
|
def __init__(self, message='Generic senpy exception', errors=[]):
|
||||||
|
Exception.__init__(self, message)
|
||||||
|
self.message = message
|
||||||
|
self.errors = errors
|
||||||
|
|
||||||
|
def toResponse(self) -> ErrorResponse:
|
||||||
|
return ErrorResponse(self.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)
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidParams(Error):
|
||||||
|
def __init__(self, wrong_params):
|
||||||
|
super().__init__(message='Wrong parameters:\n\t{}'.format(wrong_params))
|
@ -15,14 +15,15 @@
|
|||||||
#
|
#
|
||||||
"""
|
"""
|
||||||
Main class for Senpy.
|
Main class for Senpy.
|
||||||
It orchestrates plugin (de)activation and analysis.
|
It orchestrates plugin discovery, creation and analysis.
|
||||||
"""
|
"""
|
||||||
from future import standard_library
|
from future import standard_library
|
||||||
standard_library.install_aliases()
|
standard_library.install_aliases()
|
||||||
|
|
||||||
from . import config
|
from . import config
|
||||||
from . import plugins, api
|
from . import plugins, api
|
||||||
from .models import Error, AggregatedEvaluation
|
from .models import AggregatedEvaluation
|
||||||
|
from .errors import Error
|
||||||
from .plugins import AnalysisPlugin
|
from .plugins import AnalysisPlugin
|
||||||
from .blueprints import api_blueprint, demo_blueprint, ns_blueprint
|
from .blueprints import api_blueprint, demo_blueprint, ns_blueprint
|
||||||
|
|
||||||
@ -43,7 +44,7 @@ class Senpy(object):
|
|||||||
|
|
||||||
def __init__(self,
|
def __init__(self,
|
||||||
app=None,
|
app=None,
|
||||||
plugin_folder=".",
|
plugin_folders=[".", ],
|
||||||
data_folder=None,
|
data_folder=None,
|
||||||
install=False,
|
install=False,
|
||||||
strict=None,
|
strict=None,
|
||||||
@ -62,16 +63,18 @@ class Senpy(object):
|
|||||||
|
|
||||||
self._default = None
|
self._default = None
|
||||||
self.strict = strict if strict is not None else config.strict
|
self.strict = strict if strict is not None else config.strict
|
||||||
|
self.strict = True # TODO: remove this after tests pass
|
||||||
self.install = install
|
self.install = install
|
||||||
self._plugins = {}
|
self._plugins = {}
|
||||||
if plugin_folder:
|
self.plugin_folders = plugin_folders
|
||||||
self.add_folder(plugin_folder)
|
for folder in plugin_folders:
|
||||||
|
self._add_folder(folder)
|
||||||
|
|
||||||
if default_plugins:
|
if default_plugins:
|
||||||
self.add_folder('plugins', from_root=True)
|
self._add_folder('plugins', from_root=True)
|
||||||
else:
|
else:
|
||||||
# Add only conversion plugins
|
# Add only conversion plugins
|
||||||
self.add_folder(os.path.join('plugins', 'postprocessing'),
|
self._add_folder(os.path.join('plugins', 'postprocessing'),
|
||||||
from_root=True)
|
from_root=True)
|
||||||
self.app = app
|
self.app = app
|
||||||
if app is not None:
|
if app is not None:
|
||||||
@ -98,17 +101,18 @@ class Senpy(object):
|
|||||||
app.register_blueprint(demo_blueprint, url_prefix="/")
|
app.register_blueprint(demo_blueprint, url_prefix="/")
|
||||||
|
|
||||||
def add_plugin(self, plugin):
|
def add_plugin(self, plugin):
|
||||||
self._plugins[plugin.name.lower()] = plugin
|
name = plugin.name.lower()
|
||||||
|
assert name
|
||||||
|
self._plugins[name] = plugin
|
||||||
self._conversion_candidates = {}
|
self._conversion_candidates = {}
|
||||||
|
|
||||||
def delete_plugin(self, plugin):
|
def delete_plugin(self, plugin):
|
||||||
del self._plugins[plugin.name.lower()]
|
del self._plugins[plugin.name.lower()]
|
||||||
|
|
||||||
def plugins(self, plugin_type=None, is_activated=True, **kwargs):
|
def plugins(self, plugin_type=None, **kwargs):
|
||||||
""" Return the plugins registered for a given application. Filtered by criteria """
|
""" Return the plugins registered for a given application. Filtered by criteria """
|
||||||
return sorted(plugins.pfilter(self._plugins,
|
return sorted(plugins.pfilter(self._plugins,
|
||||||
plugin_type=plugin_type,
|
plugin_type=plugin_type,
|
||||||
is_activated=is_activated,
|
|
||||||
**kwargs),
|
**kwargs),
|
||||||
key=lambda x: x.id)
|
key=lambda x: x.id)
|
||||||
|
|
||||||
@ -149,7 +153,7 @@ class Senpy(object):
|
|||||||
candidates = self.plugins(**kwargs)
|
candidates = self.plugins(**kwargs)
|
||||||
return list(plugins.pfilter(candidates, plugin_type=AnalysisPlugin))
|
return list(plugins.pfilter(candidates, plugin_type=AnalysisPlugin))
|
||||||
|
|
||||||
def add_folder(self, folder, from_root=False):
|
def _add_folder(self, folder, from_root=False):
|
||||||
""" Find plugins in this folder and add them to this instance """
|
""" Find plugins in this folder and add them to this instance """
|
||||||
if from_root:
|
if from_root:
|
||||||
folder = os.path.join(os.path.dirname(__file__), folder)
|
folder = os.path.join(os.path.dirname(__file__), folder)
|
||||||
@ -163,25 +167,21 @@ class Senpy(object):
|
|||||||
else:
|
else:
|
||||||
raise AttributeError("Not a folder or does not exist: %s", folder)
|
raise AttributeError("Not a folder or does not exist: %s", folder)
|
||||||
|
|
||||||
def _process(self, req, pending, done=None):
|
def _process(self, req, pending):
|
||||||
"""
|
"""
|
||||||
Recursively process the entries with the first plugin in the list, and pass the results
|
Recursively process the entries with the first plugin in the list, and pass the results
|
||||||
to the rest of the plugins.
|
to the rest of the plugins.
|
||||||
"""
|
"""
|
||||||
done = done or []
|
|
||||||
if not pending:
|
if not pending:
|
||||||
return req
|
return req
|
||||||
|
|
||||||
analysis = pending[0]
|
analysis = pending[0]
|
||||||
results = analysis.run(req)
|
results = analysis.run(req)
|
||||||
results.activities.append(analysis)
|
results.activities.append(analysis)
|
||||||
done += analysis
|
return self._process(results, pending[1:])
|
||||||
return self._process(results, pending[1:], done)
|
|
||||||
|
|
||||||
def install_deps(self):
|
def install_deps(self):
|
||||||
logger.info('Installing dependencies')
|
logger.info('Installing dependencies')
|
||||||
# If a plugin is activated, its dependencies should already be installed
|
|
||||||
# Otherwise, it would've failed to activate.
|
|
||||||
plugins.install_deps(*self._plugins.values())
|
plugins.install_deps(*self._plugins.values())
|
||||||
|
|
||||||
def analyse(self, request, analyses=None):
|
def analyse(self, request, analyses=None):
|
||||||
@ -263,6 +263,7 @@ class Senpy(object):
|
|||||||
i.emotions = newemotions
|
i.emotions = newemotions
|
||||||
newentries.append(i)
|
newentries.append(i)
|
||||||
resp.entries = newentries
|
resp.entries = newentries
|
||||||
|
resp.activities.extend(done)
|
||||||
return resp
|
return resp
|
||||||
|
|
||||||
def _conversion_candidate(self, fromModel, toModel):
|
def _conversion_candidate(self, fromModel, toModel):
|
||||||
@ -330,7 +331,7 @@ class Senpy(object):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def default_plugin(self):
|
def default_plugin(self):
|
||||||
if not self._default or not self._default.is_activated:
|
if not self._default:
|
||||||
candidates = self.analysis_plugins()
|
candidates = self.analysis_plugins()
|
||||||
if len(candidates) > 0:
|
if len(candidates) > 0:
|
||||||
self._default = candidates[0]
|
self._default = candidates[0]
|
||||||
@ -342,84 +343,10 @@ class Senpy(object):
|
|||||||
@default_plugin.setter
|
@default_plugin.setter
|
||||||
def default_plugin(self, value):
|
def default_plugin(self, value):
|
||||||
if isinstance(value, plugins.Plugin):
|
if isinstance(value, plugins.Plugin):
|
||||||
if not value.is_activated:
|
|
||||||
raise AttributeError('The default plugin has to be activated.')
|
|
||||||
self._default = value
|
self._default = value
|
||||||
|
|
||||||
else:
|
else:
|
||||||
self._default = self._plugins[value.lower()]
|
self._default = self._plugins[value.lower()]
|
||||||
|
|
||||||
def activate_all(self, sync=True):
|
|
||||||
ps = []
|
|
||||||
for plug in self._plugins.keys():
|
|
||||||
try:
|
|
||||||
self.activate_plugin(plug, sync=sync)
|
|
||||||
except Exception as ex:
|
|
||||||
if self.strict:
|
|
||||||
raise
|
|
||||||
logger.error('Could not activate {}: {}'.format(plug, ex))
|
|
||||||
return ps
|
|
||||||
|
|
||||||
def deactivate_all(self, sync=True):
|
|
||||||
ps = []
|
|
||||||
for plug in self._plugins.keys():
|
|
||||||
ps.append(self.deactivate_plugin(plug, sync=sync))
|
|
||||||
return ps
|
|
||||||
|
|
||||||
def _activate(self, plugin):
|
|
||||||
with plugin._lock:
|
|
||||||
if plugin.is_activated:
|
|
||||||
return
|
|
||||||
try:
|
|
||||||
logger.info("Activating plugin: {}".format(plugin.name))
|
|
||||||
|
|
||||||
assert plugin._activate()
|
|
||||||
logger.info(f"Plugin activated: {plugin.name}")
|
|
||||||
except Exception as ex:
|
|
||||||
if getattr(plugin, "optional", False) and not self.strict:
|
|
||||||
logger.info(f"Plugin could NOT be activated: {plugin.name}")
|
|
||||||
return False
|
|
||||||
raise
|
|
||||||
return plugin.is_activated
|
|
||||||
|
|
||||||
def activate_plugin(self, plugin_name, sync=True):
|
|
||||||
plugin_name = plugin_name.lower()
|
|
||||||
if plugin_name not in self._plugins:
|
|
||||||
raise Error(
|
|
||||||
message="Plugin not found: {}".format(plugin_name), status=404)
|
|
||||||
plugin = self._plugins[plugin_name]
|
|
||||||
|
|
||||||
logger.info("Activating plugin: {}".format(plugin.name))
|
|
||||||
|
|
||||||
if sync or not getattr(plugin, 'async', True) or getattr(
|
|
||||||
plugin, 'sync', False):
|
|
||||||
return self._activate(plugin)
|
|
||||||
else:
|
|
||||||
th = Thread(target=partial(self._activate, plugin))
|
|
||||||
th.start()
|
|
||||||
return th
|
|
||||||
|
|
||||||
def _deactivate(self, plugin):
|
|
||||||
with plugin._lock:
|
|
||||||
if not plugin.is_activated:
|
|
||||||
return
|
|
||||||
plugin._deactivate()
|
|
||||||
logger.info("Plugin deactivated: {}".format(plugin.name))
|
|
||||||
|
|
||||||
def deactivate_plugin(self, plugin_name, sync=True):
|
|
||||||
plugin_name = plugin_name.lower()
|
|
||||||
if plugin_name not in self._plugins:
|
|
||||||
raise Error(
|
|
||||||
message="Plugin not found: {}".format(plugin_name), status=404)
|
|
||||||
plugin = self._plugins[plugin_name]
|
|
||||||
|
|
||||||
if sync or not getattr(plugin, 'async', True) or not getattr(
|
|
||||||
plugin, 'sync', False):
|
|
||||||
plugin._deactivate()
|
|
||||||
else:
|
|
||||||
th = Thread(target=plugin.deactivate)
|
|
||||||
th.start()
|
|
||||||
return th
|
|
||||||
|
|
||||||
def teardown(self, exception):
|
def teardown(self, exception):
|
||||||
pass
|
pass
|
||||||
|
303
senpy/meta.py
303
senpy/meta.py
@ -1,303 +0,0 @@
|
|||||||
#
|
|
||||||
# Copyright 2014 Grupo de Sistemas Inteligentes (GSI) DIT, UPM
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
# you may not use this file except in compliance with the License.
|
|
||||||
# You may obtain a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
# See the License for the specific language governing permissions and
|
|
||||||
# limitations under the License.
|
|
||||||
#
|
|
||||||
'''
|
|
||||||
Meta-programming for the models.
|
|
||||||
'''
|
|
||||||
import os
|
|
||||||
import json
|
|
||||||
import jsonschema
|
|
||||||
import inspect
|
|
||||||
import copy
|
|
||||||
|
|
||||||
from abc import ABCMeta
|
|
||||||
from collections import namedtuple
|
|
||||||
from collections.abc import MutableMapping
|
|
||||||
|
|
||||||
|
|
||||||
class BaseMeta(ABCMeta):
|
|
||||||
'''
|
|
||||||
Metaclass for models. It extracts the default values for the fields in
|
|
||||||
the model.
|
|
||||||
|
|
||||||
For instance, instances of the following class wouldn't need to mark
|
|
||||||
their version or description on initialization:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
class MyPlugin(Plugin):
|
|
||||||
version=0.3
|
|
||||||
description='A dull plugin'
|
|
||||||
|
|
||||||
|
|
||||||
Note that these operations could be included in the __init__ of the
|
|
||||||
class, but it would be very inefficient.
|
|
||||||
'''
|
|
||||||
_subtypes = {}
|
|
||||||
|
|
||||||
def __new__(mcs, name, bases, attrs, **kwargs):
|
|
||||||
register_afterwards = False
|
|
||||||
defaults = {}
|
|
||||||
aliases = {}
|
|
||||||
|
|
||||||
attrs = mcs.expand_with_schema(name, attrs)
|
|
||||||
if 'schema' in attrs:
|
|
||||||
register_afterwards = True
|
|
||||||
for base in bases:
|
|
||||||
if hasattr(base, '_defaults'):
|
|
||||||
defaults.update(getattr(base, '_defaults'))
|
|
||||||
if hasattr(base, '_aliases'):
|
|
||||||
aliases.update(getattr(base, '_aliases'))
|
|
||||||
|
|
||||||
info, rest = mcs.split_attrs(attrs)
|
|
||||||
|
|
||||||
for i in list(info.keys()):
|
|
||||||
if isinstance(info[i], _Alias):
|
|
||||||
aliases[i] = info[i].indict
|
|
||||||
if info[i].default is not None:
|
|
||||||
defaults[i] = info[i].default
|
|
||||||
else:
|
|
||||||
defaults[i] = info[i]
|
|
||||||
|
|
||||||
rest['_defaults'] = defaults
|
|
||||||
rest['_aliases'] = aliases
|
|
||||||
|
|
||||||
cls = super(BaseMeta, mcs).__new__(mcs, name, tuple(bases), rest)
|
|
||||||
|
|
||||||
if register_afterwards:
|
|
||||||
mcs.register(cls, defaults['@type'])
|
|
||||||
return cls
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def register(mcs, rsubclass, rtype=None):
|
|
||||||
mcs._subtypes[rtype or rsubclass.__name__] = rsubclass
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def expand_with_schema(name, attrs):
|
|
||||||
if 'schema' in attrs: # Schema specified by name
|
|
||||||
schema_file = '{}.json'.format(attrs['schema'])
|
|
||||||
elif 'schema_file' in attrs:
|
|
||||||
schema_file = attrs['schema_file']
|
|
||||||
del attrs['schema_file']
|
|
||||||
else:
|
|
||||||
return attrs
|
|
||||||
|
|
||||||
if '/' not in 'schema_file':
|
|
||||||
thisdir = os.path.dirname(os.path.realpath(__file__))
|
|
||||||
schema_file = os.path.join(thisdir,
|
|
||||||
'schemas',
|
|
||||||
schema_file)
|
|
||||||
|
|
||||||
schema_path = 'file://' + schema_file
|
|
||||||
|
|
||||||
with open(schema_file) as f:
|
|
||||||
schema = json.load(f)
|
|
||||||
|
|
||||||
resolver = jsonschema.RefResolver(schema_path, schema)
|
|
||||||
if '@type' not in attrs:
|
|
||||||
attrs['@type'] = name
|
|
||||||
attrs['_schema_file'] = schema_file
|
|
||||||
attrs['schema'] = schema
|
|
||||||
attrs['_validator'] = jsonschema.Draft4Validator(schema, resolver=resolver)
|
|
||||||
|
|
||||||
schema_defaults = BaseMeta.get_defaults(attrs['schema'])
|
|
||||||
attrs.update(schema_defaults)
|
|
||||||
|
|
||||||
return attrs
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def is_func(v):
|
|
||||||
return inspect.isroutine(v) or inspect.ismethod(v) or \
|
|
||||||
inspect.ismodule(v) or isinstance(v, property)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def is_internal(k):
|
|
||||||
return k[0] == '_' or k == 'schema' or k == 'data'
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_key(key):
|
|
||||||
if key[0] != '_':
|
|
||||||
key = key.replace("__", ":", 1)
|
|
||||||
return key
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def split_attrs(attrs):
|
|
||||||
'''
|
|
||||||
Extract the attributes of the class.
|
|
||||||
|
|
||||||
This allows adding default values in the class definition.
|
|
||||||
e.g.:
|
|
||||||
'''
|
|
||||||
isattr = {}
|
|
||||||
rest = {}
|
|
||||||
for key, value in attrs.items():
|
|
||||||
if not (BaseMeta.is_internal(key)) and (not BaseMeta.is_func(value)):
|
|
||||||
isattr[key] = value
|
|
||||||
else:
|
|
||||||
rest[key] = value
|
|
||||||
return isattr, rest
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_defaults(schema):
|
|
||||||
temp = {}
|
|
||||||
for obj in [
|
|
||||||
schema,
|
|
||||||
] + schema.get('allOf', []):
|
|
||||||
for k, v in obj.get('properties', {}).items():
|
|
||||||
if 'default' in v and k not in temp:
|
|
||||||
temp[k] = v['default']
|
|
||||||
return temp
|
|
||||||
|
|
||||||
|
|
||||||
def make_property(key, default=None):
|
|
||||||
|
|
||||||
def fget(self):
|
|
||||||
if default:
|
|
||||||
return self.get(key, copy.copy(default))
|
|
||||||
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 = {}
|
|
||||||
_aliases = {'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[k] = v
|
|
||||||
return self
|
|
||||||
|
|
||||||
def serializable(self, **kwargs):
|
|
||||||
def ser_or_down(item):
|
|
||||||
if hasattr(item, 'serializable'):
|
|
||||||
return item.serializable(**kwargs)
|
|
||||||
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(**kwargs))
|
|
||||||
|
|
||||||
def __getitem__(self, 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 __delitem__(self, key):
|
|
||||||
key = self._key_to_attr(key)
|
|
||||||
del self.__dict__[key]
|
|
||||||
|
|
||||||
def as_dict(self, verbose=True, aliases=False):
|
|
||||||
attrs = self.__dict__.keys()
|
|
||||||
if not verbose and hasattr(self, '_terse_keys'):
|
|
||||||
attrs = self._terse_keys + ['@type', '@id']
|
|
||||||
res = {k: getattr(self, k) for k in attrs
|
|
||||||
if not self._internal_key(k) and hasattr(self, k)}
|
|
||||||
if not aliases:
|
|
||||||
return res
|
|
||||||
for k, ok in self._aliases.items():
|
|
||||||
if ok in res:
|
|
||||||
res[k] = getattr(res, ok)
|
|
||||||
del res[ok]
|
|
||||||
return res
|
|
||||||
|
|
||||||
def __iter__(self):
|
|
||||||
return (k for k in self.__dict__ if not self._internal_key(k))
|
|
||||||
|
|
||||||
def __len__(self):
|
|
||||||
return len(self.__dict__)
|
|
||||||
|
|
||||||
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._aliases.get(key, key)
|
|
||||||
return key
|
|
||||||
|
|
||||||
def _key_to_attr(self, key):
|
|
||||||
if self._internal_key(key):
|
|
||||||
return key
|
|
||||||
|
|
||||||
if key in self._aliases:
|
|
||||||
key = self._aliases[key]
|
|
||||||
else:
|
|
||||||
key = key.replace(":", "__", 1)
|
|
||||||
return key
|
|
||||||
|
|
||||||
def __getattr__(self, key):
|
|
||||||
nkey = self._attr_to_key(key)
|
|
||||||
if nkey in self.__dict__:
|
|
||||||
return self.__dict__[nkey]
|
|
||||||
elif nkey == key:
|
|
||||||
raise AttributeError("Key not found: {}".format(key))
|
|
||||||
return getattr(self, nkey)
|
|
||||||
|
|
||||||
def __setattr__(self, key, value):
|
|
||||||
super(CustomDict, self).__setattr__(self._attr_to_key(key), value)
|
|
||||||
|
|
||||||
def __delattr__(self, key):
|
|
||||||
super(CustomDict, self).__delattr__(self._attr_to_key(key))
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _internal_key(key):
|
|
||||||
return key[0] == '_'
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return json.dumps(self.serializable(), sort_keys=True, indent=4)
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return json.dumps(self.serializable(), sort_keys=True, indent=4)
|
|
||||||
|
|
||||||
|
|
||||||
_Alias = namedtuple('Alias', ['indict', 'default'])
|
|
||||||
|
|
||||||
|
|
||||||
def alias(key, default=None):
|
|
||||||
return _Alias(key, default)
|
|
549
senpy/models.py
549
senpy/models.py
@ -15,141 +15,49 @@
|
|||||||
#
|
#
|
||||||
'''
|
'''
|
||||||
Senpy Models.
|
Senpy Models.
|
||||||
|
|
||||||
This implementation should mirror the JSON schema definition.
|
|
||||||
For compatibility with Py3 and for easier debugging, this new version drops
|
|
||||||
introspection and adds all arguments to the models.
|
|
||||||
'''
|
'''
|
||||||
from __future__ import print_function
|
|
||||||
from future import standard_library
|
|
||||||
standard_library.install_aliases()
|
|
||||||
|
|
||||||
from future.utils import with_metaclass
|
|
||||||
from past.builtins import basestring
|
|
||||||
|
|
||||||
from jinja2 import Environment, BaseLoader
|
from typing import *
|
||||||
|
|
||||||
import time
|
|
||||||
import copy
|
|
||||||
import json
|
|
||||||
import os
|
import os
|
||||||
import jsonref
|
|
||||||
from flask import Response as FlaskResponse
|
|
||||||
from pyld import jsonld
|
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import jmespath
|
import jmespath
|
||||||
|
import json
|
||||||
|
|
||||||
|
from textwrap import dedent
|
||||||
|
|
||||||
|
from pydantic import BaseModel as PydanticModel
|
||||||
|
from pydantic import computed_field, field_validator, ConfigDict, Field
|
||||||
|
from pydantic_core import from_json
|
||||||
|
|
||||||
logging.getLogger('rdflib').setLevel(logging.WARN)
|
logging.getLogger('rdflib').setLevel(logging.WARN)
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
from rdflib import Graph
|
|
||||||
|
|
||||||
|
|
||||||
from .meta import BaseMeta, CustomDict, alias
|
|
||||||
|
|
||||||
DEFINITIONS_FILE = 'definitions.json'
|
|
||||||
CONTEXT_PATH = os.path.join(os.path.dirname(os.path.realpath(__file__)),
|
CONTEXT_PATH = os.path.join(os.path.dirname(os.path.realpath(__file__)),
|
||||||
'schemas',
|
'schemas',
|
||||||
'context.jsonld')
|
'context.jsonld')
|
||||||
|
|
||||||
|
with open(CONTEXT_PATH) as f:
|
||||||
def get_schema_path(schema_file, absolute=False):
|
BASE_CONTEXT = dict(json.loads(f.read()))
|
||||||
if absolute:
|
|
||||||
return os.path.realpath(schema_file)
|
|
||||||
else:
|
|
||||||
return os.path.join(
|
|
||||||
os.path.dirname(os.path.realpath(__file__)), 'schemas',
|
|
||||||
schema_file)
|
|
||||||
|
|
||||||
|
|
||||||
def read_schema(schema_file, absolute=False):
|
class BaseModel(PydanticModel):
|
||||||
schema_path = get_schema_path(schema_file, absolute)
|
|
||||||
schema_uri = 'file://{}'.format(schema_path)
|
|
||||||
with open(schema_path) as f:
|
|
||||||
return jsonref.load(f, base_uri=schema_uri)
|
|
||||||
|
|
||||||
|
|
||||||
def dump_schema(schema):
|
|
||||||
return jsonref.dumps(schema)
|
|
||||||
|
|
||||||
|
|
||||||
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:
|
|
||||||
return context
|
|
||||||
else:
|
|
||||||
raise AttributeError('Please, provide a valid context')
|
|
||||||
|
|
||||||
|
|
||||||
base_context = load_context(CONTEXT_PATH)
|
|
||||||
|
|
||||||
|
|
||||||
def register(rsubclass, rtype=None):
|
|
||||||
BaseMeta.register(rsubclass, rtype)
|
|
||||||
|
|
||||||
|
|
||||||
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
|
||||||
a JSON-LD object. The structure of the dictionary is checked via JSON-schema.
|
a JSON-LD object.
|
||||||
For convenience, the values can also be accessed as attributes
|
The basic attributes should be set in the class, but additional values can
|
||||||
(a la Javascript). e.g.:
|
also be provided/set via attributes (à la Javascript). e.g.:
|
||||||
|
|
||||||
>>> myobject.key == myobject['key']
|
>>> myobject.key == myobject['key']
|
||||||
True
|
True
|
||||||
>>> myobject.ns__name == myobject['ns:name']
|
>>> myobject['ns:name'] = 'Peter'
|
||||||
True
|
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
|
|
||||||
True
|
|
||||||
>>> n2 = NewModel()
|
|
||||||
>>> n2 == 5
|
|
||||||
True
|
|
||||||
>>> 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.
|
|
||||||
|
|
||||||
'''
|
'''
|
||||||
|
model_config = ConfigDict(extra='ignore', ignored_types=(object, ))
|
||||||
# schema_file = DEFINITIONS_FILE
|
context: Optional[Union[str, Dict]] = Field(default=None, serialization_alias="@context")
|
||||||
_context = base_context["@context"]
|
wasGeneratedBy: Optional[str] = Field(default=None, serialization_alias="prov:wasGeneratedBy")
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
auto_id = kwargs.pop('_auto_id', False)
|
|
||||||
|
|
||||||
super(BaseModel, self).__init__(*args, **kwargs)
|
|
||||||
|
|
||||||
if auto_id:
|
|
||||||
self.id
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def id(self):
|
def id(self):
|
||||||
@ -161,35 +69,6 @@ class BaseModel(with_metaclass(BaseMeta, CustomDict)):
|
|||||||
def id(self, value):
|
def id(self, value):
|
||||||
self['@id'] = value
|
self['@id'] = value
|
||||||
|
|
||||||
def flask(self,
|
|
||||||
in_headers=False,
|
|
||||||
headers=None,
|
|
||||||
outformat='json-ld',
|
|
||||||
**kwargs):
|
|
||||||
"""
|
|
||||||
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.
|
|
||||||
"""
|
|
||||||
headers = headers or {}
|
|
||||||
kwargs["with_context"] = not in_headers
|
|
||||||
content, mimetype = self.serialize(format=outformat,
|
|
||||||
with_mime=True,
|
|
||||||
**kwargs)
|
|
||||||
|
|
||||||
if outformat == 'json-ld' and in_headers:
|
|
||||||
headers.update({
|
|
||||||
"Link":
|
|
||||||
('<%s>;'
|
|
||||||
'rel="http://www.w3.org/ns/json-ld#context";'
|
|
||||||
' type="application/ld+json"' % kwargs.get('context_uri'))
|
|
||||||
})
|
|
||||||
return FlaskResponse(
|
|
||||||
response=content,
|
|
||||||
status=self.get('status', 200),
|
|
||||||
headers=headers,
|
|
||||||
mimetype=mimetype)
|
|
||||||
|
|
||||||
def serialize(self, format='json-ld', with_mime=False,
|
def serialize(self, format='json-ld', with_mime=False,
|
||||||
template=None, prefix=None, fields=None, **kwargs):
|
template=None, prefix=None, fields=None, **kwargs):
|
||||||
js = self.jsonld(prefix=prefix, **kwargs)
|
js = self.jsonld(prefix=prefix, **kwargs)
|
||||||
@ -240,7 +119,7 @@ class BaseModel(with_metaclass(BaseMeta, CustomDict)):
|
|||||||
result,
|
result,
|
||||||
options={
|
options={
|
||||||
'expandContext': [
|
'expandContext': [
|
||||||
self._context,
|
self.context,
|
||||||
{
|
{
|
||||||
'prefix': prefix,
|
'prefix': prefix,
|
||||||
'endpoint': prefix
|
'endpoint': prefix
|
||||||
@ -271,261 +150,177 @@ class BaseModel(with_metaclass(BaseMeta, CustomDict)):
|
|||||||
self['prov:wasGeneratedBy'] = another.id
|
self['prov:wasGeneratedBy'] = another.id
|
||||||
|
|
||||||
|
|
||||||
def subtypes():
|
|
||||||
return BaseMeta._subtypes
|
|
||||||
|
|
||||||
|
|
||||||
def from_dict(indict, cls=None, warn=True):
|
|
||||||
if not cls:
|
|
||||||
target = indict.get('@type', None)
|
|
||||||
cls = BaseModel
|
|
||||||
try:
|
|
||||||
cls = subtypes()[target]
|
|
||||||
except KeyError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
if cls == BaseModel and warn:
|
|
||||||
logger.warning('Created an instance of an unknown model')
|
|
||||||
|
|
||||||
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):
|
|
||||||
v = v[:]
|
|
||||||
for ix, v2 in enumerate(v):
|
|
||||||
if isinstance(v2, dict):
|
|
||||||
v[ix] = from_dict(v2)
|
|
||||||
outdict[k] = copy.copy(v)
|
|
||||||
return cls(**outdict)
|
|
||||||
|
|
||||||
|
|
||||||
def from_string(string, **kwargs):
|
|
||||||
return from_dict(json.loads(string), **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
def from_json(injson, **kwargs):
|
|
||||||
indict = json.loads(injson)
|
|
||||||
return from_dict(indict, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
class Entry(BaseModel):
|
class Entry(BaseModel):
|
||||||
schema = 'entry'
|
text: str = Field(serialization_alias="nif:isString")
|
||||||
|
|
||||||
text = alias('nif:isString')
|
class Activity(BaseModel):
|
||||||
sentiments = alias('marl:hasOpinion', [])
|
plugin: str
|
||||||
emotions = alias('onyx:hasEmotionSet', [])
|
params: Dict[str, Any]
|
||||||
|
analyzed: List[str] = []
|
||||||
|
|
||||||
class Sentiment(BaseModel):
|
|
||||||
schema = 'sentiment'
|
|
||||||
|
|
||||||
polarity = alias('marl:hasPolarity')
|
|
||||||
polarityValue = alias('marl:polarityValue')
|
|
||||||
|
|
||||||
|
|
||||||
class Error(BaseModel, Exception):
|
|
||||||
schema = 'error'
|
|
||||||
|
|
||||||
def __init__(self, message='Generic senpy exception', *args, **kwargs):
|
|
||||||
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)
|
|
||||||
|
|
||||||
|
|
||||||
class AggregatedEvaluation(BaseModel):
|
|
||||||
schema = 'aggregatedEvaluation'
|
|
||||||
|
|
||||||
evaluations = alias('senpy:evaluations', [])
|
|
||||||
|
|
||||||
|
|
||||||
class Dataset(BaseModel):
|
|
||||||
schema = 'dataset'
|
|
||||||
|
|
||||||
|
|
||||||
class Datasets(BaseModel):
|
|
||||||
schema = 'datasets'
|
|
||||||
|
|
||||||
datasets = []
|
|
||||||
|
|
||||||
|
|
||||||
class Emotion(BaseModel):
|
|
||||||
schema = 'emotion'
|
|
||||||
|
|
||||||
|
|
||||||
class EmotionConversion(BaseModel):
|
|
||||||
schema = 'emotionConversion'
|
|
||||||
|
|
||||||
|
|
||||||
class EmotionConversionPlugin(BaseModel):
|
|
||||||
schema = 'emotionConversionPlugin'
|
|
||||||
|
|
||||||
|
|
||||||
class EmotionAnalysis(BaseModel):
|
|
||||||
schema = 'emotionAnalysis'
|
|
||||||
|
|
||||||
|
|
||||||
class EmotionModel(BaseModel):
|
|
||||||
schema = 'emotionModel'
|
|
||||||
onyx__hasEmotionCategory = []
|
|
||||||
|
|
||||||
|
|
||||||
class EmotionPlugin(BaseModel):
|
|
||||||
schema = 'emotionPlugin'
|
|
||||||
|
|
||||||
|
|
||||||
class EmotionSet(BaseModel):
|
|
||||||
schema = 'emotionSet'
|
|
||||||
|
|
||||||
onyx__hasEmotion = []
|
|
||||||
|
|
||||||
|
|
||||||
class Evaluation(BaseModel):
|
|
||||||
schema = 'evaluation'
|
|
||||||
|
|
||||||
metrics = alias('senpy:metrics', [])
|
|
||||||
|
|
||||||
|
|
||||||
class Entity(BaseModel):
|
|
||||||
schema = 'entity'
|
|
||||||
|
|
||||||
|
|
||||||
class Help(BaseModel):
|
|
||||||
schema = 'help'
|
|
||||||
|
|
||||||
|
|
||||||
class Metric(BaseModel):
|
|
||||||
schema = 'metric'
|
|
||||||
|
|
||||||
|
|
||||||
class Parameter(BaseModel):
|
|
||||||
schema = 'parameter'
|
|
||||||
|
|
||||||
|
|
||||||
class Plugins(BaseModel):
|
|
||||||
schema = 'plugins'
|
|
||||||
|
|
||||||
plugins = []
|
|
||||||
|
|
||||||
|
|
||||||
class Response(BaseModel):
|
|
||||||
schema = 'response'
|
|
||||||
|
|
||||||
|
|
||||||
class Results(BaseModel):
|
class Results(BaseModel):
|
||||||
schema = 'results'
|
activities: List[Activity] = []
|
||||||
|
entries: List[Entry] = []
|
||||||
_terse_keys = ['entries', ]
|
parameters: Dict[str, str] = {}
|
||||||
|
|
||||||
activities = []
|
|
||||||
entries = []
|
|
||||||
|
|
||||||
def activity(self, id):
|
|
||||||
for i in self.activities:
|
|
||||||
if i.id == id:
|
|
||||||
return i
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
class SentimentPlugin(BaseModel):
|
|
||||||
schema = 'sentimentPlugin'
|
|
||||||
|
|
||||||
|
|
||||||
class Suggestion(BaseModel):
|
|
||||||
schema = 'suggestion'
|
|
||||||
|
|
||||||
|
|
||||||
class Topic(BaseModel):
|
|
||||||
schema = 'topic'
|
|
||||||
|
|
||||||
|
|
||||||
class Analysis(BaseModel):
|
class Analysis(BaseModel):
|
||||||
'''
|
plugin: 'Plugin'
|
||||||
A prov:Activity that results of executing a Plugin on an entry with a set of
|
params: dict[str, object] = {}
|
||||||
parameters.
|
|
||||||
'''
|
|
||||||
schema = 'analysis'
|
|
||||||
|
|
||||||
parameters = alias('prov:used', [])
|
|
||||||
algorithm = alias('prov:wasAssociatedWith', [])
|
|
||||||
|
|
||||||
@property
|
|
||||||
def params(self):
|
|
||||||
outdict = {}
|
|
||||||
outdict['algorithm'] = self.algorithm
|
|
||||||
for param in self.parameters:
|
|
||||||
outdict[param['name']] = param['value']
|
|
||||||
return outdict
|
|
||||||
|
|
||||||
@params.setter
|
|
||||||
def params(self, value):
|
|
||||||
for k, v in value.items():
|
|
||||||
for param in self.parameters:
|
|
||||||
if param.name == k:
|
|
||||||
param.value = v
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
self.parameters.append(Parameter(name=k, value=v)) # noqa: F821
|
|
||||||
|
|
||||||
def param(self, key, default=None):
|
|
||||||
for param in self.parameters:
|
|
||||||
if param['name'] == key:
|
|
||||||
return param['value']
|
|
||||||
return default
|
|
||||||
|
|
||||||
@property
|
|
||||||
def plugin(self):
|
|
||||||
return self._plugin
|
|
||||||
|
|
||||||
@plugin.setter
|
|
||||||
def plugin(self, value):
|
|
||||||
self._plugin = value
|
|
||||||
self['prov:wasAssociatedWith'] = value.id
|
|
||||||
|
|
||||||
def run(self, request):
|
def run(self, request):
|
||||||
return self.plugin.process(request, self)
|
return self.plugin.process(request, self)
|
||||||
|
|
||||||
|
|
||||||
class Plugin(BaseModel):
|
class MoralityAnalysis(Analysis):
|
||||||
schema = 'plugin'
|
usedMoralValueModel: str
|
||||||
extra_params = {}
|
usedMLModel: Optional[str] = None
|
||||||
|
|
||||||
def activity(self, parameters=None):
|
|
||||||
|
class Annotation(BaseModel):
|
||||||
|
annotated: Optional[str] = None
|
||||||
|
wasGeneratedBy: Optional[str] = None
|
||||||
|
|
||||||
|
def add_provenance(self, entry, activity):
|
||||||
|
self.annotated = entry.id
|
||||||
|
self.wasGeneratedBy = activity.id
|
||||||
|
self[prov['wasGeneratedBy']] = activity.id
|
||||||
|
activity.analysed.append(entry.id)
|
||||||
|
|
||||||
|
|
||||||
|
class MoralAnnotation(Annotation):
|
||||||
|
def __init__(self, category, confidence=None, intensity=None,
|
||||||
|
entry=None, activity=None):
|
||||||
|
super().__init__()
|
||||||
|
if confidence is not None:
|
||||||
|
self[amor['confidence']] = confidence
|
||||||
|
if intensity is not None:
|
||||||
|
self[amor_mft['hasPolarityIntensity']] = intensity
|
||||||
|
if entry and activity:
|
||||||
|
self.add_provenance(entry, activity)
|
||||||
|
elif entry or activity:
|
||||||
|
raise Exception()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_label(cls, label):
|
||||||
|
return cls(label.category, label.confidence, label.intensity)
|
||||||
|
|
||||||
|
|
||||||
|
class Plugin(BaseModel):
|
||||||
|
name: str = Field(default='', validate_default=True)
|
||||||
|
version: Optional[str] = 'dev'
|
||||||
|
author: Optional[str] = None
|
||||||
|
extra_params: Dict[str, Any] = {}
|
||||||
|
description: str = Field(default="", json_schema_extra=dict(hidden=True))
|
||||||
|
|
||||||
|
@field_validator("description", mode="before")
|
||||||
|
@classmethod
|
||||||
|
def dedentdoc(cls, doc: str) -> str:
|
||||||
|
doc = doc or cls.get('__doc__', None)
|
||||||
|
return dedent(doc)
|
||||||
|
|
||||||
|
@field_validator("name", mode="before")
|
||||||
|
@classmethod
|
||||||
|
def clsname(cls, name: str) -> str:
|
||||||
|
name = name or cls.__name__.capitalize()
|
||||||
|
return name
|
||||||
|
|
||||||
|
@computed_field
|
||||||
|
@property
|
||||||
|
def id(self) -> str:
|
||||||
|
return 'endpoint:plugins/{}_{}'.format(self.name, self.version)
|
||||||
|
|
||||||
|
def activity(self, params, *, cls=Analysis):
|
||||||
'''Generate an Analysis (prov:Activity) from this plugin and the given parameters'''
|
'''Generate an Analysis (prov:Activity) from this plugin and the given parameters'''
|
||||||
a = Analysis()
|
a = cls(plugin=self, params=params)
|
||||||
a.plugin = self
|
|
||||||
if parameters:
|
|
||||||
a.params = parameters
|
|
||||||
return a
|
return a
|
||||||
|
|
||||||
|
|
||||||
# More classes could be added programmatically
|
class Plugins(BaseModel):
|
||||||
|
plugins: List[Plugin] = []
|
||||||
|
|
||||||
def _class_from_schema(name, schema=None, schema_file=None, base_classes=None):
|
class Emotion:
|
||||||
base_classes = base_classes or []
|
hasEmotionIntensity: str = Field(serialization_alias="onyx:hasEmotionIntensity")
|
||||||
base_classes.append(BaseModel)
|
hasEmotionCategory: str = Field(serialization_alias="onyx:hasEmotionCategory")
|
||||||
attrs = {}
|
|
||||||
if schema:
|
class Sentiment:
|
||||||
attrs['schema'] = schema
|
pass
|
||||||
elif schema_file:
|
|
||||||
attrs['schema_file'] = schema_file
|
class SentimentPlugin(Plugin):
|
||||||
else:
|
pass
|
||||||
attrs['schema'] = name
|
|
||||||
name = "".join((name[0].upper(), name[1:]))
|
class Suggestion:
|
||||||
return BaseMeta(name, base_classes, attrs)
|
pass
|
||||||
|
|
||||||
|
class Topic:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
def _add_class_from_schema(*args, **kwargs):
|
class Context:
|
||||||
generatedClass = _class_from_schema(*args, **kwargs)
|
pass
|
||||||
globals()[generatedClass.__name__] = generatedClass
|
|
||||||
del generatedClass
|
class Dataset:
|
||||||
|
pass
|
||||||
|
|
||||||
|
class Datasets:
|
||||||
|
pass
|
||||||
|
|
||||||
|
class Definitions:
|
||||||
|
pass
|
||||||
|
|
||||||
|
class Dimensions:
|
||||||
|
pass
|
||||||
|
|
||||||
|
class EmotionAnalysis:
|
||||||
|
pass
|
||||||
|
|
||||||
|
class EmotionConversion:
|
||||||
|
pass
|
||||||
|
|
||||||
|
class EmotionConversionPlugin(Plugin):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class Emotion:
|
||||||
|
pass
|
||||||
|
|
||||||
|
class EmotionModel:
|
||||||
|
pass
|
||||||
|
|
||||||
|
class EmotionPlugin(Plugin):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class EmotionSet:
|
||||||
|
pass
|
||||||
|
|
||||||
|
class Entity:
|
||||||
|
pass
|
||||||
|
|
||||||
|
class ErrorResponse(BaseModel):
|
||||||
|
code: Optional[int] = 500
|
||||||
|
message: str
|
||||||
|
|
||||||
|
|
||||||
|
class Metric:
|
||||||
|
pass
|
||||||
|
|
||||||
|
class Evaluation:
|
||||||
|
pass
|
||||||
|
|
||||||
|
class AggregatedEvaluation:
|
||||||
|
pass
|
||||||
|
|
||||||
|
class Help:
|
||||||
|
pass
|
||||||
|
|
||||||
|
class Parameter:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def from_dict(c, cls):
|
||||||
|
return cls.model_validate(d)
|
||||||
|
|
||||||
|
|
||||||
|
def from_string(s, cls):
|
||||||
|
return from_dict(from_json(s, allow_partial=True))
|
||||||
|
7
senpy/ns.py
Normal file
7
senpy/ns.py
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
from rdflib import Namespace
|
||||||
|
|
||||||
|
amor = Namespace('http://www.gsi.upm.es/ontologies/amor/ns#')
|
||||||
|
amor_bhv = Namespace('http://www.gsi.upm.es/ontologies/amor-bhv/ns#')
|
||||||
|
amor_mft = Namespace('http://www.gsi.upm.es/ontologies/amor-mft/ns#')
|
||||||
|
prov = Namespace('http://www.w3.org/ns/prov#')
|
||||||
|
emoml = Namespace('http://www.gsi.upm.es/ontologies/onyx/vocabularies/emotionml/ns#')
|
@ -16,10 +16,7 @@
|
|||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
#
|
#
|
||||||
|
|
||||||
from future import standard_library
|
from typing import *
|
||||||
standard_library.install_aliases()
|
|
||||||
|
|
||||||
from future.utils import with_metaclass
|
|
||||||
from functools import partial
|
from functools import partial
|
||||||
|
|
||||||
import os.path
|
import os.path
|
||||||
@ -29,6 +26,8 @@ import pickle
|
|||||||
import logging
|
import logging
|
||||||
import pprint
|
import pprint
|
||||||
|
|
||||||
|
import deprecation
|
||||||
|
|
||||||
import inspect
|
import inspect
|
||||||
import sys
|
import sys
|
||||||
import subprocess
|
import subprocess
|
||||||
@ -38,59 +37,21 @@ import threading
|
|||||||
import multiprocessing
|
import multiprocessing
|
||||||
import pkg_resources
|
import pkg_resources
|
||||||
from nltk import download
|
from nltk import download
|
||||||
from textwrap import dedent
|
|
||||||
from sklearn.base import TransformerMixin, BaseEstimator
|
from sklearn.base import TransformerMixin, BaseEstimator
|
||||||
from itertools import product
|
from itertools import product
|
||||||
|
|
||||||
from .. import models, utils
|
from .. import models, utils
|
||||||
|
from ..errors import Error
|
||||||
from .. import api
|
from .. import api
|
||||||
from .. import gsitk_compat
|
from .. import gsitk_compat
|
||||||
from .. import testing
|
from .. import testing
|
||||||
from .. import config
|
from .. import config
|
||||||
|
from .. import ns
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class PluginMeta(models.BaseMeta):
|
class Plugin(models.Plugin):
|
||||||
_classes = {}
|
|
||||||
|
|
||||||
def __new__(mcs, name, bases, attrs, **kwargs):
|
|
||||||
plugin_type = set()
|
|
||||||
for base in bases:
|
|
||||||
if hasattr(base, '_plugin_type'):
|
|
||||||
plugin_type |= base._plugin_type
|
|
||||||
plugin_type.add(name)
|
|
||||||
alias = attrs.get('name', name).lower()
|
|
||||||
attrs['_plugin_type'] = plugin_type
|
|
||||||
logger.debug('Adding new plugin class: %s %s %s %s', name, bases, attrs, plugin_type)
|
|
||||||
attrs['name'] = alias
|
|
||||||
if 'description' not in attrs:
|
|
||||||
doc = attrs.get('__doc__', None)
|
|
||||||
if doc:
|
|
||||||
attrs['description'] = dedent(doc)
|
|
||||||
else:
|
|
||||||
logger.warning(
|
|
||||||
('Plugin {} does not have a description. '
|
|
||||||
'Please, add a short summary to help other developers'
|
|
||||||
).format(name))
|
|
||||||
cls = super(PluginMeta, mcs).__new__(mcs, name, bases, attrs)
|
|
||||||
|
|
||||||
if alias in mcs._classes:
|
|
||||||
if config.testing:
|
|
||||||
raise Exception(
|
|
||||||
('The type of plugin {} already exists. '
|
|
||||||
'Please, choose a different name').format(name))
|
|
||||||
else:
|
|
||||||
logger.warning('Overloading plugin class: {}'.format(alias))
|
|
||||||
mcs._classes[alias] = cls
|
|
||||||
return cls
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def for_type(cls, ptype):
|
|
||||||
return cls._classes[ptype]
|
|
||||||
|
|
||||||
|
|
||||||
class Plugin(with_metaclass(PluginMeta, models.Plugin)):
|
|
||||||
'''
|
'''
|
||||||
Base class for all plugins in senpy.
|
Base class for all plugins in senpy.
|
||||||
A plugin must provide at least these attributes:
|
A plugin must provide at least these attributes:
|
||||||
@ -103,23 +64,16 @@ class Plugin(with_metaclass(PluginMeta, models.Plugin)):
|
|||||||
|
|
||||||
'''
|
'''
|
||||||
|
|
||||||
_terse_keys = ['name', '@id', '@type', 'author', 'description',
|
data_folder: str = "."
|
||||||
'extra_params', 'is_activated', 'url', 'version']
|
test_cases: List[Dict] = []
|
||||||
|
|
||||||
def __init__(self, info=None, data_folder=None, **kwargs):
|
|
||||||
|
def __init__(self, *, data_folder=None, **data):
|
||||||
"""
|
"""
|
||||||
Provides a canonical name for plugins and serves as base for other
|
Provides a canonical name for plugins and serves as base for other
|
||||||
kinds of plugins.
|
kinds of plugins.
|
||||||
"""
|
"""
|
||||||
logger.debug("Initialising %s", info)
|
super().__init__(**data)
|
||||||
super(Plugin, self).__init__(**kwargs)
|
|
||||||
if info:
|
|
||||||
self.update(info)
|
|
||||||
self.validate()
|
|
||||||
self.id = 'endpoint:plugins/{}_{}'.format(self['name'],
|
|
||||||
self['version'])
|
|
||||||
self.is_activated = False
|
|
||||||
self._lock = threading.Lock()
|
|
||||||
self._directory = os.path.abspath(
|
self._directory = os.path.abspath(
|
||||||
os.path.dirname(inspect.getfile(self.__class__)))
|
os.path.dirname(inspect.getfile(self.__class__)))
|
||||||
|
|
||||||
@ -128,7 +82,6 @@ class Plugin(with_metaclass(PluginMeta, models.Plugin)):
|
|||||||
if not data_folder:
|
if not data_folder:
|
||||||
data_folder = os.getcwd()
|
data_folder = os.getcwd()
|
||||||
|
|
||||||
|
|
||||||
data_folder = os.path.abspath(data_folder)
|
data_folder = os.path.abspath(data_folder)
|
||||||
subdir = os.path.join(data_folder, self.name)
|
subdir = os.path.join(data_folder, self.name)
|
||||||
|
|
||||||
@ -149,35 +102,9 @@ class Plugin(with_metaclass(PluginMeta, models.Plugin)):
|
|||||||
def log(self):
|
def log(self):
|
||||||
return self._log
|
return self._log
|
||||||
|
|
||||||
def validate(self):
|
|
||||||
missing = []
|
|
||||||
for x in ['name', 'description', 'version']:
|
|
||||||
if x not in self:
|
|
||||||
missing.append(x)
|
|
||||||
if missing:
|
|
||||||
raise models.Error(
|
|
||||||
'Missing configuration parameters: {}'.format(missing))
|
|
||||||
|
|
||||||
def get_folder(self):
|
def get_folder(self):
|
||||||
return os.path.dirname(inspect.getfile(self.__class__))
|
return os.path.dirname(inspect.getfile(self.__class__))
|
||||||
|
|
||||||
def _activate(self):
|
|
||||||
if self.is_activated:
|
|
||||||
return
|
|
||||||
self.activate()
|
|
||||||
self.is_activated = True
|
|
||||||
return self.is_activated
|
|
||||||
|
|
||||||
def _deactivate(self):
|
|
||||||
self.is_activated = False
|
|
||||||
self.deactivate()
|
|
||||||
|
|
||||||
def activate(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def deactivate(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def process(self, request, activity, **kwargs):
|
def process(self, request, activity, **kwargs):
|
||||||
"""
|
"""
|
||||||
An implemented plugin should override this method.
|
An implemented plugin should override this method.
|
||||||
@ -195,7 +122,7 @@ class Plugin(with_metaclass(PluginMeta, models.Plugin)):
|
|||||||
if inspect.isgenerator(results):
|
if inspect.isgenerator(results):
|
||||||
for result in results:
|
for result in results:
|
||||||
yield result
|
yield result
|
||||||
else:
|
elif results:
|
||||||
yield results
|
yield results
|
||||||
|
|
||||||
def process_entry(self, entry, activity):
|
def process_entry(self, entry, activity):
|
||||||
@ -210,8 +137,6 @@ class Plugin(with_metaclass(PluginMeta, models.Plugin)):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def test(self, test_cases=None):
|
def test(self, test_cases=None):
|
||||||
if not self.is_activated:
|
|
||||||
self._activate()
|
|
||||||
if not test_cases:
|
if not test_cases:
|
||||||
if not hasattr(self, 'test_cases'):
|
if not hasattr(self, 'test_cases'):
|
||||||
raise AttributeError(
|
raise AttributeError(
|
||||||
@ -235,7 +160,7 @@ class Plugin(with_metaclass(PluginMeta, models.Plugin)):
|
|||||||
def test_case(self, case, mock=testing.MOCK_REQUESTS):
|
def test_case(self, case, mock=testing.MOCK_REQUESTS):
|
||||||
if 'entry' not in case and 'input' in case:
|
if 'entry' not in case and 'input' in case:
|
||||||
entry = models.Entry(_auto_id=False)
|
entry = models.Entry(_auto_id=False)
|
||||||
entry.nif__isString = case['input']
|
entry.text = case['input']
|
||||||
case['entry'] = entry
|
case['entry'] = entry
|
||||||
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', {}))
|
||||||
@ -265,7 +190,7 @@ class Plugin(with_metaclass(PluginMeta, models.Plugin)):
|
|||||||
expected = [expected]
|
expected = [expected]
|
||||||
utils.check_template(res.entries, expected)
|
utils.check_template(res.entries, expected)
|
||||||
res.validate()
|
res.validate()
|
||||||
except models.Error:
|
except Error:
|
||||||
if should_fail:
|
if should_fail:
|
||||||
return
|
return
|
||||||
raise
|
raise
|
||||||
@ -302,38 +227,16 @@ class Plugin(with_metaclass(PluginMeta, models.Plugin)):
|
|||||||
SenpyPlugin = Plugin
|
SenpyPlugin = Plugin
|
||||||
|
|
||||||
|
|
||||||
class FailedPlugin(Plugin):
|
|
||||||
"""A plugin that has failed to initialize."""
|
|
||||||
version = 0
|
|
||||||
|
|
||||||
def __init__(self, info, function):
|
|
||||||
super().__init__(info)
|
|
||||||
a = info.get('name', info.get('module', self.name))
|
|
||||||
self['name'] == a
|
|
||||||
self._function = function
|
|
||||||
self.is_activated = False
|
|
||||||
|
|
||||||
def retry(self):
|
|
||||||
return self._function()
|
|
||||||
|
|
||||||
def test(self):
|
|
||||||
'''
|
|
||||||
A module that failed to load cannot be tested. But non-optional
|
|
||||||
plugins should not fail to load in strict mode.
|
|
||||||
'''
|
|
||||||
assert self.optional and not config.strict
|
|
||||||
|
|
||||||
|
|
||||||
class Analyser(Plugin):
|
class Analyser(Plugin):
|
||||||
'''
|
'''
|
||||||
A subclass of Plugin that analyses text and provides an annotation.
|
A subclass of Plugin that analyses text and provides an annotation.
|
||||||
'''
|
'''
|
||||||
|
|
||||||
# Deprecated
|
@deprecation.deprecated(details="Use process instead")
|
||||||
def analyse(self, request, activity):
|
def analyse(self, request, activity):
|
||||||
return super(Analyser, self).process(request, activity)
|
return super(Analyser, self).process(request, activity)
|
||||||
|
|
||||||
# Deprecated
|
@deprecation.deprecated(details="Use process_entries instead")
|
||||||
def analyse_entries(self, entries, activity):
|
def analyse_entries(self, entries, activity):
|
||||||
for i in super(Analyser, self).process_entries(entries, activity):
|
for i in super(Analyser, self).process_entries(entries, activity):
|
||||||
yield i
|
yield i
|
||||||
@ -402,14 +305,12 @@ class Evaluable(Plugin):
|
|||||||
return evaluate([self], *args, **kwargs)
|
return evaluate([self], *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class SentimentPlugin(Analyser, Evaluable, models.SentimentPlugin):
|
class SentimentPlugin(Analyser, Evaluable):
|
||||||
'''
|
'''
|
||||||
Sentiment plugins provide sentiment annotation (using Marl)
|
Sentiment plugins provide sentiment annotation (using Marl)
|
||||||
'''
|
'''
|
||||||
minPolarityValue = 0
|
minPolarityValue: float = 0
|
||||||
maxPolarityValue = 1
|
maxPolarityValue: float = 1
|
||||||
|
|
||||||
_terse_keys = Analyser._terse_keys + ['minPolarityValue', 'maxPolarityValue']
|
|
||||||
|
|
||||||
def test_case(self, case):
|
def test_case(self, case):
|
||||||
if 'polarity' in case:
|
if 'polarity' in case:
|
||||||
@ -441,7 +342,7 @@ class SentimentPlugin(Analyser, Evaluable, models.SentimentPlugin):
|
|||||||
for feat in X:
|
for feat in X:
|
||||||
if isinstance(feat, list):
|
if isinstance(feat, list):
|
||||||
feat = ' '.join(feat)
|
feat = ' '.join(feat)
|
||||||
entries.append(models.Entry(nif__isString=feat))
|
entries.append(models.Entry(text=feat))
|
||||||
labels = []
|
labels = []
|
||||||
for e in self.process_entries(entries, activity):
|
for e in self.process_entries(entries, activity):
|
||||||
sent = e.sentiments[0].polarity
|
sent = e.sentiments[0].polarity
|
||||||
@ -458,10 +359,27 @@ class EmotionPlugin(Analyser, models.EmotionPlugin):
|
|||||||
'''
|
'''
|
||||||
Emotion plugins provide emotion annotation (using Onyx)
|
Emotion plugins provide emotion annotation (using Onyx)
|
||||||
'''
|
'''
|
||||||
minEmotionValue = 0
|
minEmotionValue: float = 0
|
||||||
maxEmotionValue = 1
|
maxEmotionValue: float = 1
|
||||||
|
|
||||||
_terse_keys = Analyser._terse_keys + ['minEmotionValue', 'maxEmotionValue']
|
|
||||||
|
class AnnotationPlugin(AnalysisPlugin):
|
||||||
|
def process_entry(self, entry, activity, **kwargs):
|
||||||
|
for annotation in self.annotate(entry):
|
||||||
|
annotation.add_provenance(entry, activity)
|
||||||
|
yield entry
|
||||||
|
|
||||||
|
def annotate(self, entry, **kwargs):
|
||||||
|
raise NotImplemented("this should be implemented in subclasses")
|
||||||
|
|
||||||
|
|
||||||
|
class MoralityPlugin(AnnotationPlugin):
|
||||||
|
moralValueModel: str = ns.amor_mft['MoralFoundationTHeory']
|
||||||
|
|
||||||
|
def activity(self, parameters=None):
|
||||||
|
return models.MoralityAnalysis(plugin=self,
|
||||||
|
usedMLModel=None,
|
||||||
|
usedMoralValueModel=self.model)
|
||||||
|
|
||||||
|
|
||||||
class EmotionConversion(Conversion):
|
class EmotionConversion(Conversion):
|
||||||
@ -545,7 +463,7 @@ class TextBox(Box):
|
|||||||
'''A black box plugin that takes only text as input'''
|
'''A black box plugin that takes only text as input'''
|
||||||
|
|
||||||
def to_features(self, entry, activity):
|
def to_features(self, entry, activity):
|
||||||
return [entry['nif:isString']]
|
return [entry.text]
|
||||||
|
|
||||||
|
|
||||||
class SentimentBox(TextBox, SentimentPlugin):
|
class SentimentBox(TextBox, SentimentPlugin):
|
||||||
@ -553,14 +471,14 @@ class SentimentBox(TextBox, SentimentPlugin):
|
|||||||
A box plugin where the output is only a polarity label or a tuple (polarity, polarityValue)
|
A box plugin where the output is only a polarity label or a tuple (polarity, polarityValue)
|
||||||
'''
|
'''
|
||||||
|
|
||||||
classes = ['marl:Positive', 'marl:Neutral', 'marl:Negative']
|
classes: List[str] = ['marl:Positive', 'marl:Neutral', 'marl:Negative']
|
||||||
binary = True
|
binary: bool = True
|
||||||
|
|
||||||
def to_entry(self, features, entry, activity, **kwargs):
|
def to_entry(self, features, entry, activity, **kwargs):
|
||||||
|
|
||||||
if len(features) != len(self.classes):
|
if len(features) != len(self.classes):
|
||||||
raise models.Error('The number of features ({}) does not match the classes '
|
raise Error('The number of features ({}) does not match the classes '
|
||||||
'(plugin.classes ({})'.format(len(features), len(self.classes)))
|
'(plugin.classes ({})'.format(len(features), len(self.classes)))
|
||||||
|
|
||||||
minValue = activity.param('marl:minPolarityValue', 0)
|
minValue = activity.param('marl:minPolarityValue', 0)
|
||||||
maxValue = activity.param('marl:minPolarityValue', 1)
|
maxValue = activity.param('marl:minPolarityValue', 1)
|
||||||
@ -590,8 +508,8 @@ class EmotionBox(TextBox, EmotionPlugin):
|
|||||||
A box plugin where the output is only an a tuple of emotion labels
|
A box plugin where the output is only an a tuple of emotion labels
|
||||||
'''
|
'''
|
||||||
|
|
||||||
EMOTIONS = []
|
EMOTIONS: List[str] = []
|
||||||
with_intensity = True
|
with_intensity: bool = True
|
||||||
|
|
||||||
def to_entry(self, features, entry, activity, **kwargs):
|
def to_entry(self, features, entry, activity, **kwargs):
|
||||||
s = models.EmotionSet()
|
s = models.EmotionSet()
|
||||||
@ -697,7 +615,7 @@ def pfilter(plugins, plugin_type=Analyser, **kwargs):
|
|||||||
logger.debug('Class: {}'.format(pclass))
|
logger.debug('Class: {}'.format(pclass))
|
||||||
candidates = filter(lambda x: isinstance(x, pclass), plugins)
|
candidates = filter(lambda x: isinstance(x, pclass), plugins)
|
||||||
except KeyError:
|
except KeyError:
|
||||||
raise models.Error('{} is not a valid type'.format(plugin_type))
|
raise Error('{} is not a valid type'.format(plugin_type))
|
||||||
else:
|
else:
|
||||||
candidates = plugins
|
candidates = plugins
|
||||||
|
|
||||||
@ -775,7 +693,7 @@ def install_deps(*plugins):
|
|||||||
exitcode = process.wait()
|
exitcode = process.wait()
|
||||||
installed = True
|
installed = True
|
||||||
if exitcode != 0:
|
if exitcode != 0:
|
||||||
raise models.Error(
|
raise Error(
|
||||||
"Dependencies not properly installed: {}".format(pip_args))
|
"Dependencies not properly installed: {}".format(pip_args))
|
||||||
installed_nltk = download(list(nltk_resources))
|
installed_nltk = download(list(nltk_resources))
|
||||||
return installed or installed_nltk
|
return installed or installed_nltk
|
||||||
@ -855,7 +773,7 @@ def from_module(module, **kwargs):
|
|||||||
def one_from_module(module, root, info, **kwargs):
|
def one_from_module(module, root, info, **kwargs):
|
||||||
if '@type' in info:
|
if '@type' in info:
|
||||||
cls = PluginMeta.from_type(info['@type'])
|
cls = PluginMeta.from_type(info['@type'])
|
||||||
return cls(info=info, **kwargs)
|
return cls(**info, **kwargs)
|
||||||
instance = next(
|
instance = next(
|
||||||
from_module(module=module, root=root, info=info, **kwargs), None)
|
from_module(module=module, root=root, info=info, **kwargs), None)
|
||||||
if not instance:
|
if not instance:
|
||||||
@ -888,7 +806,7 @@ def _from_module_name(module, root, info=None, **kwargs):
|
|||||||
|
|
||||||
def _from_loaded_module(module, info=None, **kwargs):
|
def _from_loaded_module(module, info=None, **kwargs):
|
||||||
for cls in _classes_in_module(module):
|
for cls in _classes_in_module(module):
|
||||||
yield cls(info=info, **kwargs)
|
yield cls(**(info or {}), **kwargs)
|
||||||
for instance in _instances_in_module(module):
|
for instance in _instances_in_module(module):
|
||||||
yield instance
|
yield instance
|
||||||
|
|
||||||
@ -899,7 +817,7 @@ cached_evs = {}
|
|||||||
def evaluate(plugins, datasets, **kwargs):
|
def evaluate(plugins, datasets, **kwargs):
|
||||||
for plug in plugins:
|
for plug in plugins:
|
||||||
if not hasattr(plug, 'as_pipe'):
|
if not hasattr(plug, 'as_pipe'):
|
||||||
raise models.Error('Plugin {} cannot be evaluated'.format(plug.name))
|
raise Error('Plugin {} cannot be evaluated'.format(plug.name))
|
||||||
|
|
||||||
if not isinstance(datasets, dict):
|
if not isinstance(datasets, dict):
|
||||||
datasets = gsitk_compat.prepare(datasets, download=True)
|
datasets = gsitk_compat.prepare(datasets, download=True)
|
||||||
@ -961,12 +879,11 @@ def evaluations_to_JSONLD(results, flatten=False):
|
|||||||
|
|
||||||
|
|
||||||
class ScikitWrapper(BaseEstimator, TransformerMixin):
|
class ScikitWrapper(BaseEstimator, TransformerMixin):
|
||||||
def __init__(self, plugin=None):
|
def __init__(self, plugin=None, **data):
|
||||||
|
super().__init__(**data)
|
||||||
self.plugin = plugin
|
self.plugin = plugin
|
||||||
|
|
||||||
def fit(self, X=None, y=None):
|
def fit(self, X=None, y=None):
|
||||||
if self.plugin is not None and not self.plugin.is_activated:
|
|
||||||
self.plugin.activate()
|
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def transform(self, X):
|
def transform(self, X):
|
||||||
|
@ -70,11 +70,11 @@ pattern.text._read = _read
|
|||||||
|
|
||||||
class ANEW(EmotionPlugin):
|
class ANEW(EmotionPlugin):
|
||||||
description = "This plugin consists on an emotion classifier using ANEW lexicon dictionary. It averages the VAD (valence-arousal-dominance) value of each word in the text that is also in the ANEW dictionary. To obtain a categorical value (e.g., happy) use the emotion conversion API (e.g., `emotion-model=emoml:big6`)."
|
description = "This plugin consists on an emotion classifier using ANEW lexicon dictionary. It averages the VAD (valence-arousal-dominance) value of each word in the text that is also in the ANEW dictionary. To obtain a categorical value (e.g., happy) use the emotion conversion API (e.g., `emotion-model=emoml:big6`)."
|
||||||
author = "@icorcuera"
|
author: str = "@icorcuera"
|
||||||
version = "0.5.2"
|
version: str = "0.5.2"
|
||||||
name = "emotion-anew"
|
name = "emotion-anew"
|
||||||
|
|
||||||
extra_params = {
|
extra_params: dict = {
|
||||||
"language": {
|
"language": {
|
||||||
"description": "language of the input",
|
"description": "language of the input",
|
||||||
"aliases": ["language", "l"],
|
"aliases": ["language", "l"],
|
||||||
@ -89,7 +89,8 @@ class ANEW(EmotionPlugin):
|
|||||||
onyx__usesEmotionModel = MODEL
|
onyx__usesEmotionModel = MODEL
|
||||||
nltk_resources = ['stopwords']
|
nltk_resources = ['stopwords']
|
||||||
|
|
||||||
def activate(self, *args, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
|
super().__init__(**kwargs)
|
||||||
self._stopwords = stopwords.words('english')
|
self._stopwords = stopwords.words('english')
|
||||||
dictionary={}
|
dictionary={}
|
||||||
dictionary['es'] = {}
|
dictionary['es'] = {}
|
||||||
@ -204,7 +205,7 @@ class ANEW(EmotionPlugin):
|
|||||||
|
|
||||||
yield entry
|
yield entry
|
||||||
|
|
||||||
test_cases = [
|
test_cases: list[dict] = [
|
||||||
{
|
{
|
||||||
'name': 'anger with VAD=(2.12, 6.95, 5.05)',
|
'name': 'anger with VAD=(2.12, 6.95, 5.05)',
|
||||||
'input': 'I hate you',
|
'input': 'I hate you',
|
||||||
|
@ -31,14 +31,15 @@ class DepecheMood(EmotionBox):
|
|||||||
DepecheMood is an emotion lexicon automatically generated from news articles where users expressed their associated emotions. It contains two languages (English and Italian), as well as three types of word representations (token, lemma and lemma#PoS). For English, the lexicon contains 165k tokens, while the Italian version contains 116k. Unsupervised techniques can be applied to generate simple but effective baselines. To learn more, please visit https://github.com/marcoguerini/DepecheMood and http://www.depechemood.eu/
|
DepecheMood is an emotion lexicon automatically generated from news articles where users expressed their associated emotions. It contains two languages (English and Italian), as well as three types of word representations (token, lemma and lemma#PoS). For English, the lexicon contains 165k tokens, while the Italian version contains 116k. Unsupervised techniques can be applied to generate simple but effective baselines. To learn more, please visit https://github.com/marcoguerini/DepecheMood and http://www.depechemood.eu/
|
||||||
'''
|
'''
|
||||||
|
|
||||||
author = 'Oscar Araque'
|
author: str = 'Oscar Araque'
|
||||||
name = 'emotion-depechemood'
|
name: str = 'emotion-depechemood'
|
||||||
version = '0.1'
|
version: str = '0.1'
|
||||||
requirements = ['pandas']
|
requirements = ['pandas']
|
||||||
optional = True
|
optional = True
|
||||||
|
usesEmotionModel: str = 'wna:WNAModel'
|
||||||
|
|
||||||
nltk_resources = ["stopwords"]
|
nltk_resources = ["stopwords"]
|
||||||
|
|
||||||
onyx__usesEmotionModel = 'wna:WNAModel'
|
|
||||||
|
|
||||||
EMOTIONS = ['wna:negative-fear',
|
EMOTIONS = ['wna:negative-fear',
|
||||||
'wna:amusement',
|
'wna:amusement',
|
||||||
@ -50,16 +51,12 @@ class DepecheMood(EmotionBox):
|
|||||||
'wna:sadness']
|
'wna:sadness']
|
||||||
|
|
||||||
DM_EMOTIONS = ['AFRAID', 'AMUSED', 'ANGRY', 'ANNOYED', 'DONT_CARE', 'HAPPY', 'INSPIRED', 'SAD',]
|
DM_EMOTIONS = ['AFRAID', 'AMUSED', 'ANGRY', 'ANNOYED', 'DONT_CARE', 'HAPPY', 'INSPIRED', 'SAD',]
|
||||||
|
LEXICON_URL = "https://github.com/marcoguerini/DepecheMood/raw/master/DepecheMood%2B%2B/DepecheMood_english_token_full.tsv"
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, **data):
|
||||||
super(DepecheMood, self).__init__(*args, **kwargs)
|
super().__init__(**data)
|
||||||
self.LEXICON_URL = "https://github.com/marcoguerini/DepecheMood/raw/master/DepecheMood%2B%2B/DepecheMood_english_token_full.tsv"
|
|
||||||
self._denoise = ignore(set(string.punctuation)|set('«»'))
|
self._denoise = ignore(set(string.punctuation)|set('«»'))
|
||||||
self._stop_words = []
|
self._stop_words = []
|
||||||
self._lex_vocab = None
|
|
||||||
self._lex = None
|
|
||||||
|
|
||||||
def activate(self):
|
|
||||||
self._lex = self.download_lex()
|
self._lex = self.download_lex()
|
||||||
self._lex_vocab = set(list(self._lex.keys()))
|
self._lex_vocab = set(list(self._lex.keys()))
|
||||||
self._stop_words = stopwords.words('english') + ['']
|
self._stop_words = stopwords.words('english') + ['']
|
||||||
@ -127,7 +124,7 @@ class DepecheMood(EmotionBox):
|
|||||||
estimation = self.estimate_all_emotions(tokens)
|
estimation = self.estimate_all_emotions(tokens)
|
||||||
return estimation
|
return estimation
|
||||||
|
|
||||||
test_cases = [
|
test_cases: list[dict] = [
|
||||||
{
|
{
|
||||||
'entry': {
|
'entry': {
|
||||||
'nif:isString': 'My cat is very happy',
|
'nif:isString': 'My cat is very happy',
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from __future__ import division
|
from __future__ import division
|
||||||
|
from typing import *
|
||||||
import re
|
import re
|
||||||
import nltk
|
import nltk
|
||||||
import os
|
import os
|
||||||
@ -19,10 +20,11 @@ class WNAffect(EmotionPlugin, ShelfMixin):
|
|||||||
of each emotion. This plugin classifies among 6 emotions: anger,fear,disgust,joy,sadness
|
of each emotion. This plugin classifies among 6 emotions: anger,fear,disgust,joy,sadness
|
||||||
or neutral. The only available language is English (en)
|
or neutral. The only available language is English (en)
|
||||||
'''
|
'''
|
||||||
name = 'emotion-wnaffect'
|
name: str = 'emotion-wnaffect'
|
||||||
author = ["@icorcuera", "@balkian"]
|
author: List[str] = ["@icorcuera", "@balkian"]
|
||||||
version = '0.2'
|
version: str = '0.2'
|
||||||
extra_params = {
|
|
||||||
|
extra_params: Dict[str, Dict] = {
|
||||||
'language': {
|
'language': {
|
||||||
"@id": 'lang_wnaffect',
|
"@id": 'lang_wnaffect',
|
||||||
'description': 'language of the input',
|
'description': 'language of the input',
|
||||||
@ -31,6 +33,7 @@ class WNAffect(EmotionPlugin, ShelfMixin):
|
|||||||
'options': ['en',]
|
'options': ['en',]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
optional = True
|
optional = True
|
||||||
requirements = [
|
requirements = [
|
||||||
"nltk>=3.0.5",
|
"nltk>=3.0.5",
|
||||||
@ -42,42 +45,8 @@ class WNAffect(EmotionPlugin, ShelfMixin):
|
|||||||
onyx__usesEmotionModel = "emoml:big6"
|
onyx__usesEmotionModel = "emoml:big6"
|
||||||
nltk_resources = ['stopwords', 'averaged_perceptron_tagger_eng', 'wordnet']
|
nltk_resources = ['stopwords', 'averaged_perceptron_tagger_eng', 'wordnet']
|
||||||
|
|
||||||
def _load_synsets(self, synsets_path):
|
def __init__(self, **kwargs):
|
||||||
"""Returns a dictionary POS tag -> synset offset -> emotion (str -> int -> str)."""
|
super().__init__(**kwargs)
|
||||||
tree = ET.parse(synsets_path)
|
|
||||||
root = tree.getroot()
|
|
||||||
pos_map = {"noun": "NN", "adj": "JJ", "verb": "VB", "adv": "RB"}
|
|
||||||
|
|
||||||
synsets = {}
|
|
||||||
for pos in ["noun", "adj", "verb", "adv"]:
|
|
||||||
tag = pos_map[pos]
|
|
||||||
synsets[tag] = {}
|
|
||||||
for elem in root.findall(
|
|
||||||
".//{0}-syn-list//{0}-syn".format(pos, pos)):
|
|
||||||
offset = int(elem.get("id")[2:])
|
|
||||||
if not offset: continue
|
|
||||||
if elem.get("categ"):
|
|
||||||
synsets[tag][offset] = Emo.emotions[elem.get(
|
|
||||||
"categ")] if elem.get(
|
|
||||||
"categ") in Emo.emotions else None
|
|
||||||
elif elem.get("noun-id"):
|
|
||||||
synsets[tag][offset] = synsets[pos_map["noun"]][int(
|
|
||||||
elem.get("noun-id")[2:])]
|
|
||||||
return synsets
|
|
||||||
|
|
||||||
def _load_emotions(self, hierarchy_path):
|
|
||||||
"""Loads the hierarchy of emotions from the WordNet-Affect xml."""
|
|
||||||
|
|
||||||
tree = ET.parse(hierarchy_path)
|
|
||||||
root = tree.getroot()
|
|
||||||
for elem in root.findall("categ"):
|
|
||||||
name = elem.get("name")
|
|
||||||
if name == "root":
|
|
||||||
Emo.emotions["root"] = Emo("root")
|
|
||||||
else:
|
|
||||||
Emo.emotions[name] = Emo(name, elem.get("isa"))
|
|
||||||
|
|
||||||
def activate(self, *args, **kwargs):
|
|
||||||
|
|
||||||
self._stopwords = stopwords.words('english')
|
self._stopwords = stopwords.words('english')
|
||||||
self._wnlemma = wordnet.WordNetLemmatizer()
|
self._wnlemma = wordnet.WordNetLemmatizer()
|
||||||
@ -119,10 +88,42 @@ class WNAffect(EmotionPlugin, ShelfMixin):
|
|||||||
|
|
||||||
self._wn16_path = self.wn16_path
|
self._wn16_path = self.wn16_path
|
||||||
self._wn16 = WordNetCorpusReader(self.find_file(self._wn16_path), nltk.data.find(self.find_file(self._wn16_path)))
|
self._wn16 = WordNetCorpusReader(self.find_file(self._wn16_path), nltk.data.find(self.find_file(self._wn16_path)))
|
||||||
|
self.save()
|
||||||
|
|
||||||
|
def _load_synsets(self, synsets_path):
|
||||||
|
"""Returns a dictionary POS tag -> synset offset -> emotion (str -> int -> str)."""
|
||||||
|
tree = ET.parse(synsets_path)
|
||||||
|
root = tree.getroot()
|
||||||
|
pos_map = {"noun": "NN", "adj": "JJ", "verb": "VB", "adv": "RB"}
|
||||||
|
|
||||||
def deactivate(self, *args, **kwargs):
|
synsets = {}
|
||||||
self.save(ignore_errors=True)
|
for pos in ["noun", "adj", "verb", "adv"]:
|
||||||
|
tag = pos_map[pos]
|
||||||
|
synsets[tag] = {}
|
||||||
|
for elem in root.findall(
|
||||||
|
".//{0}-syn-list//{0}-syn".format(pos, pos)):
|
||||||
|
offset = int(elem.get("id")[2:])
|
||||||
|
if not offset: continue
|
||||||
|
if elem.get("categ"):
|
||||||
|
synsets[tag][offset] = Emo.emotions[elem.get(
|
||||||
|
"categ")] if elem.get(
|
||||||
|
"categ") in Emo.emotions else None
|
||||||
|
elif elem.get("noun-id"):
|
||||||
|
synsets[tag][offset] = synsets[pos_map["noun"]][int(
|
||||||
|
elem.get("noun-id")[2:])]
|
||||||
|
return synsets
|
||||||
|
|
||||||
|
def _load_emotions(self, hierarchy_path):
|
||||||
|
"""Loads the hierarchy of emotions from the WordNet-Affect xml."""
|
||||||
|
|
||||||
|
tree = ET.parse(hierarchy_path)
|
||||||
|
root = tree.getroot()
|
||||||
|
for elem in root.findall("categ"):
|
||||||
|
name = elem.get("name")
|
||||||
|
if name == "root":
|
||||||
|
Emo.emotions["root"] = Emo("root")
|
||||||
|
else:
|
||||||
|
Emo.emotions[name] = Emo(name, elem.get("isa"))
|
||||||
|
|
||||||
def _my_preprocessor(self, text):
|
def _my_preprocessor(self, text):
|
||||||
|
|
||||||
@ -268,7 +269,6 @@ class WNAffect(EmotionPlugin, ShelfMixin):
|
|||||||
'language': 'en',
|
'language': 'en',
|
||||||
'algorithm': 'emotion-wnaffect'}
|
'algorithm': 'emotion-wnaffect'}
|
||||||
|
|
||||||
self.activate()
|
|
||||||
texts = {'I hate you': 'anger',
|
texts = {'I hate you': 'anger',
|
||||||
'i am sad': 'sadness',
|
'i am sad': 'sadness',
|
||||||
'i am happy with my marks': 'joy',
|
'i am happy with my marks': 'joy',
|
||||||
|
@ -29,12 +29,12 @@ class Split(Transformation):
|
|||||||
(or paragraphs) is required.
|
(or paragraphs) is required.
|
||||||
'''
|
'''
|
||||||
|
|
||||||
author = ["@militarpancho", '@balkian']
|
author: str = ["@militarpancho", '@balkian']
|
||||||
version = '0.3'
|
version: str = '0.3'
|
||||||
url = "https://github.com/gsi-upm/senpy"
|
url = "https://github.com/gsi-upm/senpy"
|
||||||
nltk_resources = ['punkt']
|
nltk_resources = ['punkt']
|
||||||
|
|
||||||
extra_params = {
|
extra_params: dict = {
|
||||||
'delimiter': {
|
'delimiter': {
|
||||||
'description': 'Split text into paragraphs or sentences.',
|
'description': 'Split text into paragraphs or sentences.',
|
||||||
'aliases': ['type', 't'],
|
'aliases': ['type', 't'],
|
||||||
@ -47,7 +47,7 @@ class Split(Transformation):
|
|||||||
def analyse_entry(self, entry, activity):
|
def analyse_entry(self, entry, activity):
|
||||||
yield entry
|
yield entry
|
||||||
chunker_type = activity.params["delimiter"]
|
chunker_type = activity.params["delimiter"]
|
||||||
original_text = entry['nif:isString']
|
original_text = entry.text
|
||||||
if chunker_type == "sentence":
|
if chunker_type == "sentence":
|
||||||
tokenizer = PunktSentenceTokenizer()
|
tokenizer = PunktSentenceTokenizer()
|
||||||
if chunker_type == "paragraph":
|
if chunker_type == "paragraph":
|
||||||
@ -59,12 +59,12 @@ class Split(Transformation):
|
|||||||
for i, chunk in enumerate(chars):
|
for i, chunk in enumerate(chars):
|
||||||
start, end = chunk
|
start, end = chunk
|
||||||
e = Entry()
|
e = Entry()
|
||||||
e['nif:isString'] = original_text[start:end]
|
e.text = original_text[start:end]
|
||||||
if entry.id:
|
if entry.id:
|
||||||
e.id = entry.id + "#char={},{}".format(start, end)
|
e.id = entry.id + "#char={},{}".format(start, end)
|
||||||
yield e
|
yield e
|
||||||
|
|
||||||
test_cases = [
|
test_cases: list[dict] = [
|
||||||
{
|
{
|
||||||
'entry': {
|
'entry': {
|
||||||
'nif:isString': 'Hello. World.'
|
'nif:isString': 'Hello. World.'
|
||||||
|
@ -14,8 +14,11 @@
|
|||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
#
|
#
|
||||||
|
|
||||||
|
from typing import *
|
||||||
|
|
||||||
from senpy.plugins import EmotionConversionPlugin
|
from senpy.plugins import EmotionConversionPlugin
|
||||||
from senpy.models import EmotionSet, Emotion, Error
|
from senpy.models import EmotionSet, Emotion, EmotionConversion
|
||||||
|
from senpy.errors import Error
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@ -27,16 +30,16 @@ class CentroidConversion(EmotionConversionPlugin):
|
|||||||
categorical one, and vice versa. The centroids used in the conversion
|
categorical one, and vice versa. The centroids used in the conversion
|
||||||
are configurable and appear in the semantic description of the plugin.
|
are configurable and appear in the semantic description of the plugin.
|
||||||
'''
|
'''
|
||||||
def __init__(self, info, *args, **kwargs):
|
|
||||||
if 'centroids' not in info:
|
|
||||||
raise Error('Centroid conversion plugins should provide '
|
|
||||||
'the centroids in their senpy file')
|
|
||||||
if 'onyx:doesConversion' not in info:
|
|
||||||
if 'centroids_direction' not in info:
|
|
||||||
raise Error('Please, provide centroids direction')
|
|
||||||
|
|
||||||
cf, ct = info['centroids_direction']
|
centroids: Dict
|
||||||
info['onyx:doesConversion'] = [{
|
centroids_direction: tuple[str, str]
|
||||||
|
aliases: Dict[str, str] ={}
|
||||||
|
|
||||||
|
def __super__(self, centroids, centroids_direction, doesConversion=None, aliases={}, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
cf, ct = centroids_direction
|
||||||
|
if doesConversion is None:
|
||||||
|
doesConversion = [{
|
||||||
'onyx:conversionFrom': cf,
|
'onyx:conversionFrom': cf,
|
||||||
'onyx:conversionTo': ct
|
'onyx:conversionTo': ct
|
||||||
}, {
|
}, {
|
||||||
@ -44,17 +47,16 @@ class CentroidConversion(EmotionConversionPlugin):
|
|||||||
'onyx:conversionTo': cf
|
'onyx:conversionTo': cf
|
||||||
}]
|
}]
|
||||||
|
|
||||||
if 'aliases' in info:
|
|
||||||
aliases = info['aliases']
|
if aliases:
|
||||||
ncentroids = {}
|
ncentroids = {}
|
||||||
for k1, v1 in info['centroids'].items():
|
for k1, v1 in centroids.items():
|
||||||
nv1 = {}
|
nv1 = {}
|
||||||
for k2, v2 in v1.items():
|
for k2, v2 in v1.items():
|
||||||
nv1[aliases.get(k2, k2)] = v2
|
nv1[aliases.get(k2, k2)] = v2
|
||||||
ncentroids[aliases.get(k1, k1)] = nv1
|
ncentroids[aliases.get(k1, k1)] = nv1
|
||||||
info['centroids'] = ncentroids
|
centroids = centroids
|
||||||
|
self.centroids = centroids
|
||||||
super(CentroidConversion, self).__init__(info, *args, **kwargs)
|
|
||||||
|
|
||||||
self.dimensions = set()
|
self.dimensions = set()
|
||||||
for c in self.centroids.values():
|
for c in self.centroids.values():
|
||||||
@ -152,7 +154,7 @@ class CentroidConversion(EmotionConversionPlugin):
|
|||||||
"centroids_direction": ["emoml:big6", "emoml:fsre-dimensions"]
|
"centroids_direction": ["emoml:big6", "emoml:fsre-dimensions"]
|
||||||
}
|
}
|
||||||
|
|
||||||
c = CentroidConversion(info)
|
c = CentroidConversion.parse_obj(info)
|
||||||
|
|
||||||
es1 = EmotionSet()
|
es1 = EmotionSet()
|
||||||
e1 = Emotion()
|
e1 = Emotion()
|
||||||
|
@ -1,52 +0,0 @@
|
|||||||
---
|
|
||||||
name: Ekman2FSRE
|
|
||||||
module: senpy.plugins.postprocessing.emotion.centroids
|
|
||||||
description: Plugin to convert emotion sets from Ekman to VAD
|
|
||||||
version: 0.2
|
|
||||||
# No need to specify onyx:doesConversion because centroids.py adds it automatically from centroids_direction
|
|
||||||
neutralValue: 5.0
|
|
||||||
centroids:
|
|
||||||
anger:
|
|
||||||
A: 6.95
|
|
||||||
D: 5.1
|
|
||||||
V: 2.7
|
|
||||||
S: 5.0
|
|
||||||
disgust:
|
|
||||||
A: 5.3
|
|
||||||
D: 8.05
|
|
||||||
V: 2.7
|
|
||||||
S: 5.0
|
|
||||||
fear:
|
|
||||||
A: 6.5
|
|
||||||
D: 3.6
|
|
||||||
V: 3.2
|
|
||||||
S: 5.0
|
|
||||||
happiness:
|
|
||||||
A: 7.22
|
|
||||||
D: 6.28
|
|
||||||
V: 8.6
|
|
||||||
S: 5.0
|
|
||||||
sadness:
|
|
||||||
A: 5.21
|
|
||||||
D: 2.82
|
|
||||||
V: 2.21
|
|
||||||
S: 5.0
|
|
||||||
surprise:
|
|
||||||
A: 5.0
|
|
||||||
D: 5.0
|
|
||||||
V: 5.0
|
|
||||||
S: 10.0
|
|
||||||
centroids_direction:
|
|
||||||
- emoml:big6
|
|
||||||
- emoml:fsre-dimensions
|
|
||||||
aliases: # These are aliases for any key in the centroid, to avoid repeating a long name several times
|
|
||||||
A: emoml:fsre-dimensions_arousal
|
|
||||||
V: emoml:fsre-dimensions_valence
|
|
||||||
D: emoml:fsre-dimensions_potency
|
|
||||||
S: emoml:fsre-dimensions_unpredictability
|
|
||||||
anger: emoml:big6anger
|
|
||||||
disgust: emoml:big6disgust
|
|
||||||
fear: emoml:big6fear
|
|
||||||
happiness: emoml:big6happiness
|
|
||||||
sadness: emoml:big6sadness
|
|
||||||
surprise: emoml:big6surprise
|
|
61
senpy/plugins/postprocessing/emotion/ekman2fsre_plugin.py
Normal file
61
senpy/plugins/postprocessing/emotion/ekman2fsre_plugin.py
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
from typing import *
|
||||||
|
from senpy.ns import emoml
|
||||||
|
from senpy.plugins.postprocessing.emotion.centroids import CentroidConversion
|
||||||
|
|
||||||
|
|
||||||
|
class Ekman2FSRE(CentroidConversion):
|
||||||
|
''' Plugin to convert emotion sets from Ekman to VAD '''
|
||||||
|
version: str = '0.2'
|
||||||
|
# No need to specify doesConversion because centroids.py adds it automatically from centroids_direction
|
||||||
|
neutralValue: float = 5.0
|
||||||
|
centroids: Dict = {
|
||||||
|
'anger': {
|
||||||
|
'A': 6.95,
|
||||||
|
'D': 5.1,
|
||||||
|
'V': 2.7,
|
||||||
|
'S': 5.0,
|
||||||
|
},
|
||||||
|
'disgust': {
|
||||||
|
'A': 5.3,
|
||||||
|
'D': 8.05,
|
||||||
|
'V': 2.7,
|
||||||
|
'S': 5.0,
|
||||||
|
},
|
||||||
|
'fear': {
|
||||||
|
'A': 6.5,
|
||||||
|
'D': 3.6,
|
||||||
|
'V': 3.2,
|
||||||
|
'S': 5.0,
|
||||||
|
},
|
||||||
|
'happiness':{
|
||||||
|
'A': 7.22,
|
||||||
|
'D': 6.28,
|
||||||
|
'V': 8.6,
|
||||||
|
'S': 5.0,
|
||||||
|
},
|
||||||
|
'sadness': {
|
||||||
|
'A': 5.21,
|
||||||
|
'D': 2.82,
|
||||||
|
'V': 2.21,
|
||||||
|
'S': 5.0 ,
|
||||||
|
},
|
||||||
|
'surprise': {
|
||||||
|
'A': 5.0,
|
||||||
|
'D': 5.0,
|
||||||
|
'V': 5.0,
|
||||||
|
'S': 10.0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
centroids_direction: tuple[str, str] = ('emoml:big6', 'emoml:fsre-dimensions')
|
||||||
|
aliases: Dict[str, str] = { # These are aliases for any key in the centroid, to avoid repeating a long name several times
|
||||||
|
'A': emoml['emoml:fsre-dimensions_arousal'],
|
||||||
|
'V': emoml['emoml:fsre-dimensions_valence'],
|
||||||
|
'D': emoml['emoml:fsre-dimensions_potency'],
|
||||||
|
'S': emoml['emoml:fsre-dimensions_unpredictability'],
|
||||||
|
'anger': emoml['emoml:big6anger'],
|
||||||
|
'disgust': emoml['emoml:big6disgust'],
|
||||||
|
'fear': emoml['emoml:big6fear'],
|
||||||
|
'happiness': emoml['emoml:big6happiness'],
|
||||||
|
'sadness': emoml['emoml:big6sadness'],
|
||||||
|
'surprise': emoml['emoml:big6surprise'],
|
||||||
|
}
|
@ -1,40 +0,0 @@
|
|||||||
---
|
|
||||||
name: Ekman2PAD
|
|
||||||
module: senpy.plugins.postprocessing.emotion.centroids
|
|
||||||
description: Plugin to convert emotion sets from Ekman to VAD
|
|
||||||
version: 0.2
|
|
||||||
# No need to specify onyx:doesConversion because centroids.py adds it automatically from centroids_direction
|
|
||||||
neutralValue: 5.0
|
|
||||||
centroids:
|
|
||||||
anger:
|
|
||||||
A: 6.95
|
|
||||||
D: 5.1
|
|
||||||
P: 2.7
|
|
||||||
disgust:
|
|
||||||
A: 5.3
|
|
||||||
D: 8.05
|
|
||||||
P: 2.7
|
|
||||||
fear:
|
|
||||||
A: 6.5
|
|
||||||
D: 3.6
|
|
||||||
P: 3.2
|
|
||||||
happiness:
|
|
||||||
A: 7.22
|
|
||||||
D: 6.28
|
|
||||||
P: 8.6
|
|
||||||
sadness:
|
|
||||||
A: 5.21
|
|
||||||
D: 2.82
|
|
||||||
P: 2.21
|
|
||||||
centroids_direction:
|
|
||||||
- emoml:big6
|
|
||||||
- emoml:pad-dimensions
|
|
||||||
aliases: # These are aliases for any key in the centroid, to avoid repeating a long name several times
|
|
||||||
P: emoml:pad-dimensions_pleasure
|
|
||||||
A: emoml:pad-dimensions_arousal
|
|
||||||
D: emoml:pad-dimensions_dominance
|
|
||||||
anger: emoml:big6anger
|
|
||||||
disgust: emoml:big6disgust
|
|
||||||
fear: emoml:big6fear
|
|
||||||
happiness: emoml:big6happiness
|
|
||||||
sadness: emoml:big6sadness
|
|
54
senpy/plugins/postprocessing/emotion/ekman2vad_plugin.py
Normal file
54
senpy/plugins/postprocessing/emotion/ekman2vad_plugin.py
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
from typing import *
|
||||||
|
from senpy.ns import emoml
|
||||||
|
from senpy.plugins.postprocessing.emotion.centroids import CentroidConversion
|
||||||
|
|
||||||
|
|
||||||
|
class Ekman2PAD(CentroidConversion):
|
||||||
|
'''Plugin to convert emotion sets from Ekman to VAD'''
|
||||||
|
version: str = '0.2'
|
||||||
|
# No need to specify doesConversion because centroids.py adds it automatically from centroids_direction
|
||||||
|
neutralValue: float = 5.0
|
||||||
|
centroids: Dict = {
|
||||||
|
'anger': {
|
||||||
|
'A': 6.95,
|
||||||
|
'D': 5.1,
|
||||||
|
'V': 2.7,
|
||||||
|
},
|
||||||
|
'disgust': {
|
||||||
|
'A': 5.3,
|
||||||
|
'D': 8.05,
|
||||||
|
'V': 2.7,
|
||||||
|
},
|
||||||
|
'fear': {
|
||||||
|
'A': 6.5,
|
||||||
|
'D': 3.6,
|
||||||
|
'V': 3.2,
|
||||||
|
},
|
||||||
|
'happiness':{
|
||||||
|
'A': 7.22,
|
||||||
|
'D': 6.28,
|
||||||
|
'V': 8.6,
|
||||||
|
},
|
||||||
|
'sadness': {
|
||||||
|
'A': 5.21,
|
||||||
|
'D': 2.82,
|
||||||
|
'V': 2.21,
|
||||||
|
},
|
||||||
|
'surprise': {
|
||||||
|
'A': 5.0,
|
||||||
|
'D': 5.0,
|
||||||
|
'V': 5.0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
centroids_direction: tuple[str, str] = (emoml['big6'], emoml['pad-dimensions'])
|
||||||
|
aliases: Dict[str, str] = { # These are aliases for any key in the centroid, to avoid repeating a long name several times
|
||||||
|
'A': emoml['pad-dimensions_arousal'],
|
||||||
|
'V': emoml['pad-dimensions_potency'],
|
||||||
|
'D': emoml['pad-dimensions_dominance'],
|
||||||
|
'anger': emoml['emoml:big6anger'],
|
||||||
|
'disgust': emoml['emoml:big6disgust'],
|
||||||
|
'fear': emoml['emoml:big6fear'],
|
||||||
|
'happiness': emoml['emoml:big6happiness'],
|
||||||
|
'sadness': emoml['emoml:big6sadness'],
|
||||||
|
'surprise': emoml['emoml:big6surprise'],
|
||||||
|
}
|
@ -19,15 +19,15 @@ from senpy import PostProcessing, easy_test
|
|||||||
|
|
||||||
class MaxEmotion(PostProcessing):
|
class MaxEmotion(PostProcessing):
|
||||||
'''Plugin to extract the emotion with highest value from an EmotionSet'''
|
'''Plugin to extract the emotion with highest value from an EmotionSet'''
|
||||||
author = '@dsuarezsouto'
|
author: str = '@dsuarezsouto'
|
||||||
version = '0.1'
|
version: str = '0.1'
|
||||||
|
|
||||||
def process_entry(self, entry, activity):
|
def process_entry(self, entry, activity):
|
||||||
if len(entry.emotions) < 1:
|
if len(entry.emotions) < 1:
|
||||||
yield entry
|
yield entry
|
||||||
return
|
return
|
||||||
|
|
||||||
set_emotions = entry.emotions[0]['onyx:hasEmotion']
|
set_emotions = entry.emotions[0].hasEmotion
|
||||||
|
|
||||||
# If there is only one emotion, do not modify it
|
# If there is only one emotion, do not modify it
|
||||||
if len(set_emotions) < 2:
|
if len(set_emotions) < 2:
|
||||||
@ -38,17 +38,16 @@ class MaxEmotion(PostProcessing):
|
|||||||
|
|
||||||
# Extract max emotion from the set emotions (emotion with highest intensity)
|
# Extract max emotion from the set emotions (emotion with highest intensity)
|
||||||
for tmp_emotion in set_emotions:
|
for tmp_emotion in set_emotions:
|
||||||
if tmp_emotion['onyx:hasEmotionIntensity'] > max_emotion[
|
if tmp_emotion.hasEmotionIntensity > max_emotion.hasEmotionIntensity:
|
||||||
'onyx:hasEmotionIntensity']:
|
|
||||||
max_emotion = tmp_emotion
|
max_emotion = tmp_emotion
|
||||||
|
|
||||||
if max_emotion['onyx:hasEmotionIntensity'] == 0:
|
if max_emotion.hasEmotionIntensity == 0:
|
||||||
max_emotion['onyx:hasEmotionCategory'] = "neutral"
|
max_emotion.hasEmotionCategory = "neutral"
|
||||||
max_emotion['onyx:hasEmotionIntensity'] = 1.0
|
max_emotion.hasEmotionIntensity = 1.0
|
||||||
|
|
||||||
entry.emotions[0]['onyx:hasEmotion'] = [max_emotion]
|
entry.emotions[0].hasEmotion = [max_emotion]
|
||||||
|
|
||||||
entry.emotions[0]['prov:wasGeneratedBy'] = activity.id
|
entry.emotions[0].wasGeneratedBy = activity.id
|
||||||
yield entry
|
yield entry
|
||||||
|
|
||||||
def check(self, request, plugins):
|
def check(self, request, plugins):
|
||||||
@ -57,7 +56,7 @@ class MaxEmotion(PostProcessing):
|
|||||||
# Test Cases:
|
# Test Cases:
|
||||||
# 1 Normal Situation.
|
# 1 Normal Situation.
|
||||||
# 2 Case to return a Neutral Emotion.
|
# 2 Case to return a Neutral Emotion.
|
||||||
test_cases = [
|
test_cases: list[dict] = [
|
||||||
{
|
{
|
||||||
"name": "If there are several emotions within an emotion set, reduce it to one.",
|
"name": "If there are several emotions within an emotion set, reduce it to one.",
|
||||||
"entry": {
|
"entry": {
|
||||||
|
@ -13,7 +13,8 @@ from scipy.interpolate import interp1d
|
|||||||
from os import path
|
from os import path
|
||||||
|
|
||||||
from senpy.plugins import SentimentBox, SenpyPlugin
|
from senpy.plugins import SentimentBox, SenpyPlugin
|
||||||
from senpy.models import Results, Entry, Sentiment, Error
|
from senpy.models import Results, Entry, Sentiment
|
||||||
|
from senpy.errors import Error
|
||||||
|
|
||||||
if sys.version_info[0] >= 3:
|
if sys.version_info[0] >= 3:
|
||||||
unicode = str
|
unicode = str
|
||||||
@ -24,9 +25,9 @@ class SentimentBasic(SentimentBox):
|
|||||||
Sentiment classifier using rule-based classification for Spanish. Based on english to spanish translation and SentiWordNet sentiment knowledge. This is a demo plugin that uses only some features from the TASS 2015 classifier. To use the entirely functional classifier you can use the service in: http://senpy.cluster.gsi.dit.upm.es.
|
Sentiment classifier using rule-based classification for Spanish. Based on english to spanish translation and SentiWordNet sentiment knowledge. This is a demo plugin that uses only some features from the TASS 2015 classifier. To use the entirely functional classifier you can use the service in: http://senpy.cluster.gsi.dit.upm.es.
|
||||||
'''
|
'''
|
||||||
name = "sentiment-basic"
|
name = "sentiment-basic"
|
||||||
author = "github.com/nachtkatze"
|
author: str = "github.com/nachtkatze"
|
||||||
version = "0.1.1"
|
version: str = "0.1.1"
|
||||||
extra_params = {
|
extra_params: dict = {
|
||||||
"language": {
|
"language": {
|
||||||
"description": "language of the text",
|
"description": "language of the text",
|
||||||
"aliases": ["language", "l"],
|
"aliases": ["language", "l"],
|
||||||
@ -36,29 +37,30 @@ class SentimentBasic(SentimentBox):
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
sentiword_path = "SentiWordNet_3.0.txt"
|
sentiword_path = "SentiWordNet_3.0.txt"
|
||||||
pos_path = "unigram_spanish.pickle"
|
|
||||||
maxPolarityValue = 1
|
maxPolarityValue = 1
|
||||||
minPolarityValue = -1
|
minPolarityValue = -1
|
||||||
nltk_resources = ['punkt_tab','wordnet', 'omw', 'omw-1.4']
|
nltk_resources = ['punkt_tab','wordnet', 'omw', 'omw-1.4']
|
||||||
optional = True
|
optional = True
|
||||||
requirements = ['nltk>=3.0.5', 'scipy>=0.14.0', 'textblob==0.17']
|
requirements = ['nltk>=3.0.5', 'scipy>=0.14.0', 'textblob==0.17']
|
||||||
with_polarity = False
|
with_polarity = False
|
||||||
|
_pos_path = "unigram_spanish.pickle"
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
self._swn = self._load_swn()
|
||||||
|
self._pos_tagger = self._load_pos_tagger()
|
||||||
|
|
||||||
def _load_swn(self):
|
def _load_swn(self):
|
||||||
self.swn_path = self.find_file(self.sentiword_path)
|
self._swn_path = self.find_file(self.sentiword_path)
|
||||||
swn = SentiWordNet(self.swn_path)
|
swn = SentiWordNet(self._swn_path)
|
||||||
return swn
|
return swn
|
||||||
|
|
||||||
def _load_pos_tagger(self):
|
def _load_pos_tagger(self):
|
||||||
self.pos_path = self.find_file(self.pos_path)
|
self._pos_path = self.find_file(self._pos_path)
|
||||||
with open(self.pos_path, 'rb') as f:
|
with open(self._pos_path, 'rb') as f:
|
||||||
tagger = pickle.load(f)
|
tagger = pickle.load(f)
|
||||||
return tagger
|
return tagger
|
||||||
|
|
||||||
def activate(self, *args, **kwargs):
|
|
||||||
self._swn = self._load_swn()
|
|
||||||
self._pos_tagger = self._load_pos_tagger()
|
|
||||||
|
|
||||||
def _remove_punctuation(self, tokens):
|
def _remove_punctuation(self, tokens):
|
||||||
return [t for t in tokens if t not in string.punctuation]
|
return [t for t in tokens if t not in string.punctuation]
|
||||||
|
|
||||||
@ -156,7 +158,7 @@ class SentimentBasic(SentimentBox):
|
|||||||
return [0, 1, 0]
|
return [0, 1, 0]
|
||||||
|
|
||||||
|
|
||||||
test_cases = [
|
test_cases: list[dict] = [
|
||||||
{
|
{
|
||||||
'input': 'Odio ir al cine',
|
'input': 'Odio ir al cine',
|
||||||
'params': {'language': 'es'},
|
'params': {'language': 'es'},
|
||||||
|
@ -34,7 +34,8 @@ import os
|
|||||||
from os import path
|
from os import path
|
||||||
import time
|
import time
|
||||||
from senpy.plugins import SentimentPlugin
|
from senpy.plugins import SentimentPlugin
|
||||||
from senpy.models import Results, Entry, Entity, Topic, Sentiment, Error
|
from senpy.models import Results, Entry, Entity, Topic, Sentiment
|
||||||
|
from senpy.errors import Error
|
||||||
from senpy.utils import check_template
|
from senpy.utils import check_template
|
||||||
|
|
||||||
|
|
||||||
@ -49,13 +50,13 @@ class MeaningCloudPlugin(SentimentPlugin):
|
|||||||
|
|
||||||
http://senpy.cluster.gsi.dit.upm.es/api/?algo=meaningCloud&language=en&apiKey=YOUR_API_KEY&input=I%20love%20Madrid.
|
http://senpy.cluster.gsi.dit.upm.es/api/?algo=meaningCloud&language=en&apiKey=YOUR_API_KEY&input=I%20love%20Madrid.
|
||||||
'''
|
'''
|
||||||
name = 'sentiment-meaningcloud'
|
name: str = 'sentiment-meaningcloud'
|
||||||
author = 'GSI UPM'
|
author: str = 'GSI UPM'
|
||||||
version = "1.1"
|
version: str = "1.1"
|
||||||
maxPolarityValue = 1
|
maxPolarityValue: float = 1
|
||||||
minPolarityValue = -1
|
minPolarityValue: float = -1
|
||||||
|
|
||||||
extra_params = {
|
extra_params: dict = {
|
||||||
"language": {
|
"language": {
|
||||||
"description": "language of the input",
|
"description": "language of the input",
|
||||||
"aliases": ["language", "l"],
|
"aliases": ["language", "l"],
|
||||||
@ -86,7 +87,7 @@ class MeaningCloudPlugin(SentimentPlugin):
|
|||||||
def analyse_entry(self, entry, activity):
|
def analyse_entry(self, entry, activity):
|
||||||
params = activity.params
|
params = activity.params
|
||||||
|
|
||||||
txt = entry['nif:isString']
|
txt = entry.text
|
||||||
api = 'http://api.meaningcloud.com/'
|
api = 'http://api.meaningcloud.com/'
|
||||||
lang = params.get("language")
|
lang = params.get("language")
|
||||||
model = "general"
|
model = "general"
|
||||||
@ -175,7 +176,7 @@ class MeaningCloudPlugin(SentimentPlugin):
|
|||||||
entry['senpy:hasTopic'].append(concept)
|
entry['senpy:hasTopic'].append(concept)
|
||||||
yield entry
|
yield entry
|
||||||
|
|
||||||
test_cases = [
|
test_cases: list[dict] = [
|
||||||
{
|
{
|
||||||
'params': {
|
'params': {
|
||||||
'algo': 'sentiment-meaningCloud',
|
'algo': 'sentiment-meaningCloud',
|
||||||
|
@ -25,10 +25,10 @@ ENDPOINT = 'http://www.sentiment140.com/api/bulkClassifyJson'
|
|||||||
class Sentiment140(SentimentBox):
|
class Sentiment140(SentimentBox):
|
||||||
'''Connects to the sentiment140 free API: http://sentiment140.com'''
|
'''Connects to the sentiment140 free API: http://sentiment140.com'''
|
||||||
|
|
||||||
author = "@balkian"
|
author: str = "@balkian"
|
||||||
version = '0.2'
|
version: str = '0.2'
|
||||||
url = "https://github.com/gsi-upm/senpy-plugins-community"
|
url = "https://github.com/gsi-upm/senpy-plugins-community"
|
||||||
extra_params = {
|
extra_params: dict = {
|
||||||
'language': {
|
'language': {
|
||||||
"@id": 'lang_sentiment140',
|
"@id": 'lang_sentiment140',
|
||||||
'description': 'language of the text',
|
'description': 'language of the text',
|
||||||
@ -66,7 +66,7 @@ class Sentiment140(SentimentBox):
|
|||||||
continue
|
continue
|
||||||
yield [0, 1, 0]
|
yield [0, 1, 0]
|
||||||
|
|
||||||
test_cases = [
|
test_cases: list[dict] = [
|
||||||
{
|
{
|
||||||
'entry': {
|
'entry': {
|
||||||
'nif:isString': 'I love Titanic'
|
'nif:isString': 'I love Titanic'
|
||||||
|
@ -12,9 +12,9 @@ class VaderSentimentPlugin(SentimentBox):
|
|||||||
'''
|
'''
|
||||||
name = "sentiment-vader"
|
name = "sentiment-vader"
|
||||||
module = "sentiment-vader"
|
module = "sentiment-vader"
|
||||||
author = "@icorcuera"
|
author: str = "@icorcuera"
|
||||||
version = "0.1.1"
|
version: str = "0.1.1"
|
||||||
extra_params = {
|
extra_params: dict = {
|
||||||
"language": {
|
"language": {
|
||||||
"description": "language of the input",
|
"description": "language of the input",
|
||||||
"@id": "lang_rand",
|
"@id": "lang_rand",
|
||||||
@ -51,9 +51,9 @@ class VaderSentimentPlugin(SentimentBox):
|
|||||||
|
|
||||||
return sentiments
|
return sentiments
|
||||||
|
|
||||||
test_cases = []
|
test_cases: list[dict] = []
|
||||||
|
|
||||||
test_cases = [
|
test_cases: list[dict] = [
|
||||||
{
|
{
|
||||||
'input': 'I am tired :(',
|
'input': 'I am tired :(',
|
||||||
'polarity': 'marl:Negative'
|
'polarity': 'marl:Negative'
|
||||||
|
@ -1,16 +1,26 @@
|
|||||||
{
|
{
|
||||||
"@context": {
|
"@context": {
|
||||||
"@vocab": "http://www.gsi.upm.es/onto/senpy/ns#",
|
"@vocab": "http://www.gsi.upm.es/ontologies/senpy/ns#",
|
||||||
|
"amor": "http://www.gsi.upm.es/ontologies/amor/ns#",
|
||||||
|
"amor-bhv": "http://www.gsi.upm.es/ontologies/amor/models/bhv/ns#",
|
||||||
|
"amor-mft": "http://www.gsi.upm.es/ontologies/amor/models/mft/ns#",
|
||||||
|
"bhv": "http://www.gsi.upm.es/ontologies/bhv#",
|
||||||
"dc": "http://dublincore.org/2012/06/14/dcelements#",
|
"dc": "http://dublincore.org/2012/06/14/dcelements#",
|
||||||
"senpy": "http://www.gsi.upm.es/onto/senpy/ns#",
|
|
||||||
"prov": "http://www.w3.org/ns/prov#",
|
|
||||||
"nif": "http://persistence.uni-leipzig.org/nlp2rdf/ontologies/nif-core#",
|
|
||||||
"marl": "http://www.gsi.upm.es/ontologies/marl/ns#",
|
|
||||||
"onyx": "http://www.gsi.upm.es/ontologies/onyx/ns#",
|
|
||||||
"wna": "http://www.gsi.upm.es/ontologies/wnaffect/ns#",
|
|
||||||
"emoml": "http://www.gsi.upm.es/ontologies/onyx/vocabularies/emotionml/ns#",
|
"emoml": "http://www.gsi.upm.es/ontologies/onyx/vocabularies/emotionml/ns#",
|
||||||
"xsd": "http://www.w3.org/2001/XMLSchema#",
|
|
||||||
"fam": "http://vocab.fusepool.info/fam#",
|
"fam": "http://vocab.fusepool.info/fam#",
|
||||||
|
"marl": "http://www.gsi.upm.es/ontologies/marl/ns#",
|
||||||
|
"mft": "http://www.gsi.upm.es/ontologies/mft/ns#",
|
||||||
|
"mls": "http://www.w3.org/ns/mls#",
|
||||||
|
"nif": "http://persistence.uni-leipzig.org/nlp2rdf/ontologies/nif-core#",
|
||||||
|
"onyx": "http://www.gsi.upm.es/ontologies/onyx/ns#",
|
||||||
|
"owl": "http://www.w3.org/2002/07/owl#",
|
||||||
|
"prov": "http://www.w3.org/ns/prov#",
|
||||||
|
"prov": "http://www.w3.org/ns/prov#",
|
||||||
|
"rdfs": "http://www.w3.org/2000/01/rdf-schema#",
|
||||||
|
"schema": "http://schema.org/",
|
||||||
|
"senpy": "http://www.gsi.upm.es/onto/senpy/ns#",
|
||||||
|
"wna": "http://www.gsi.upm.es/ontologies/wnaffect/ns#",
|
||||||
|
"xsd": "http://www.w3.org/2001/XMLSchema#",
|
||||||
"topics": {
|
"topics": {
|
||||||
"@id": "nif:topic",
|
"@id": "nif:topic",
|
||||||
"@container": "@set"
|
"@container": "@set"
|
||||||
@ -71,7 +81,6 @@
|
|||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"@type": "ParameterError"
|
"@type": "ParameterError"
|
||||||
},
|
}
|
||||||
"prefix": "http://senpy.invalid/"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -83,7 +83,6 @@ def easy_load(app=None, plugin_list=None, plugin_folder=None, **kwargs):
|
|||||||
for plugin in plugin_list:
|
for plugin in plugin_list:
|
||||||
sp.add_plugin(plugin)
|
sp.add_plugin(plugin)
|
||||||
sp.install_deps()
|
sp.install_deps()
|
||||||
sp.activate_all()
|
|
||||||
return sp, app
|
return sp, app
|
||||||
|
|
||||||
|
|
||||||
|
@ -21,7 +21,8 @@ logger = logging.getLogger(__name__)
|
|||||||
from unittest import TestCase
|
from unittest import TestCase
|
||||||
from senpy.api import (boolean, parse_params, get_extra_params, parse_analyses,
|
from senpy.api import (boolean, parse_params, get_extra_params, parse_analyses,
|
||||||
API_PARAMS, NIF_PARAMS, WEB_PARAMS)
|
API_PARAMS, NIF_PARAMS, WEB_PARAMS)
|
||||||
from senpy.models import Error, Plugin
|
from senpy.errors import Error
|
||||||
|
from senpy.models import Plugin
|
||||||
|
|
||||||
|
|
||||||
class APITest(TestCase):
|
class APITest(TestCase):
|
||||||
@ -72,7 +73,7 @@ class APITest(TestCase):
|
|||||||
in2 = {
|
in2 = {
|
||||||
'apikey': 25
|
'apikey': 25
|
||||||
}
|
}
|
||||||
extra_params = {
|
extra_params: dict = {
|
||||||
"apikey": {
|
"apikey": {
|
||||||
"aliases": [
|
"aliases": [
|
||||||
"apikey",
|
"apikey",
|
||||||
@ -110,7 +111,7 @@ class APITest(TestCase):
|
|||||||
def test_parse_analyses(self):
|
def test_parse_analyses(self):
|
||||||
'''The API should parse user parameters and return them in a format that plugins can use'''
|
'''The API should parse user parameters and return them in a format that plugins can use'''
|
||||||
plugins = [
|
plugins = [
|
||||||
Plugin({
|
Plugin.parse_obj({
|
||||||
'name': 'plugin1',
|
'name': 'plugin1',
|
||||||
'extra_params': {
|
'extra_params': {
|
||||||
# Incompatible parameter
|
# Incompatible parameter
|
||||||
@ -133,7 +134,7 @@ class APITest(TestCase):
|
|||||||
'options': ['value2_1', 'value2_2', 'value3_3']
|
'options': ['value2_1', 'value2_2', 'value3_3']
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}), Plugin({
|
}), Plugin.parse_obj({
|
||||||
'name': 'plugin2',
|
'name': 'plugin2',
|
||||||
'extra_params': {
|
'extra_params': {
|
||||||
'param0': {
|
'param0': {
|
||||||
@ -186,7 +187,7 @@ class APITest(TestCase):
|
|||||||
def test_get_extra_params(self):
|
def test_get_extra_params(self):
|
||||||
'''The API should return the list of valid parameters for a set of plugins'''
|
'''The API should return the list of valid parameters for a set of plugins'''
|
||||||
plugins = [
|
plugins = [
|
||||||
Plugin({
|
Plugin.parse_obj({
|
||||||
'name': 'plugin1',
|
'name': 'plugin1',
|
||||||
'extra_params': {
|
'extra_params': {
|
||||||
# Incompatible parameter
|
# Incompatible parameter
|
||||||
@ -208,7 +209,7 @@ class APITest(TestCase):
|
|||||||
'options': ['value2_1', 'value2_2', 'value3_3']
|
'options': ['value2_1', 'value2_2', 'value3_3']
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}), Plugin({
|
}), Plugin.parse_obj({
|
||||||
'name': 'plugin2',
|
'name': 'plugin2',
|
||||||
'extra_params': {
|
'extra_params': {
|
||||||
'param0': {
|
'param0': {
|
||||||
@ -234,14 +235,14 @@ class APITest(TestCase):
|
|||||||
|
|
||||||
expected = {
|
expected = {
|
||||||
# Overlapping parameters
|
# Overlapping parameters
|
||||||
'plugin1.param0': plugins[0]['extra_params']['param0'],
|
'plugin1.param0': plugins[0].extra_params['param0'],
|
||||||
'plugin1.param1': plugins[0]['extra_params']['param1'],
|
'plugin1.param1': plugins[0].extra_params['param1'],
|
||||||
'plugin2.param0': plugins[1]['extra_params']['param0'],
|
'plugin2.param0': plugins[1].extra_params['param0'],
|
||||||
'plugin2.param1': plugins[1]['extra_params']['param1'],
|
'plugin2.param1': plugins[1].extra_params['param1'],
|
||||||
|
|
||||||
# Non-overlapping parameters
|
# Non-overlapping parameters
|
||||||
'param2': plugins[0]['extra_params']['param2'],
|
'param2': plugins[0].extra_params['param2'],
|
||||||
'param3': plugins[1]['extra_params']['param3'],
|
'param3': plugins[1].extra_params['param3'],
|
||||||
|
|
||||||
# Intersection of overlapping parameters
|
# Intersection of overlapping parameters
|
||||||
'param1': {
|
'param1': {
|
||||||
|
@ -38,11 +38,8 @@ class BlueprintsTest(TestCase):
|
|||||||
"""Set up only once, and re-use in every individual test"""
|
"""Set up only once, and re-use in every individual test"""
|
||||||
cls.app = Flask("test_extensions")
|
cls.app = Flask("test_extensions")
|
||||||
cls.client = cls.app.test_client()
|
cls.client = cls.app.test_client()
|
||||||
cls.senpy = Senpy(default_plugins=True, strict=False) # Ignore any optional plugins
|
|
||||||
cls.senpy.init_app(cls.app)
|
|
||||||
cls.dir = os.path.join(os.path.dirname(__file__), "..")
|
cls.dir = os.path.join(os.path.dirname(__file__), "..")
|
||||||
cls.senpy.add_folder(cls.dir)
|
cls.senpy = Senpy(default_plugins=True, app=cls.app, plugin_folders=[cls.dir, "."], strict=False) # Ignore any optional plugins
|
||||||
cls.senpy.activate_all()
|
|
||||||
cls.senpy.default_plugin = 'Dummy'
|
cls.senpy.default_plugin = 'Dummy'
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
@ -21,7 +21,7 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
from unittest import TestCase
|
from unittest import TestCase
|
||||||
from senpy.cli import main_function
|
from senpy.cli import main_function
|
||||||
from senpy.models import Error
|
from senpy.errors import Error
|
||||||
|
|
||||||
|
|
||||||
class CLITest(TestCase):
|
class CLITest(TestCase):
|
||||||
|
@ -18,7 +18,8 @@ from unittest import TestCase
|
|||||||
|
|
||||||
from senpy.testing import patch_requests
|
from senpy.testing import patch_requests
|
||||||
from senpy.client import Client
|
from senpy.client import Client
|
||||||
from senpy.models import Results, Plugins, Error
|
from senpy.models import Results, Plugins
|
||||||
|
from senpy.errors import Error
|
||||||
from senpy.plugins import AnalysisPlugin
|
from senpy.plugins import AnalysisPlugin
|
||||||
|
|
||||||
|
|
||||||
|
@ -23,7 +23,8 @@ import logging
|
|||||||
from functools import partial
|
from functools import partial
|
||||||
from senpy.extensions import Senpy
|
from senpy.extensions import Senpy
|
||||||
from senpy import plugins, config, api
|
from senpy import plugins, config, api
|
||||||
from senpy.models import Error, Results, Entry, EmotionSet, Emotion, Plugin
|
from senpy.models import Results, Entry, EmotionSet, Emotion, Plugin
|
||||||
|
from senpy.errors import Error
|
||||||
from flask import Flask
|
from flask import Flask
|
||||||
from unittest import TestCase
|
from unittest import TestCase
|
||||||
|
|
||||||
@ -41,8 +42,7 @@ class ExtensionsTest(TestCase):
|
|||||||
self.senpy = Senpy(plugin_folder=self.examples_dir,
|
self.senpy = Senpy(plugin_folder=self.examples_dir,
|
||||||
app=self.app,
|
app=self.app,
|
||||||
default_plugins=False)
|
default_plugins=False)
|
||||||
self.senpy.deactivate_all()
|
self.senpy.default_plugin = "Dummy"
|
||||||
self.senpy.activate_plugin("Dummy", sync=True)
|
|
||||||
self.app.config['TESTING'] = True # Tell Flask not to catch Exceptions
|
self.app.config['TESTING'] = True # Tell Flask not to catch Exceptions
|
||||||
|
|
||||||
def test_init(self):
|
def test_init(self):
|
||||||
@ -62,9 +62,9 @@ class ExtensionsTest(TestCase):
|
|||||||
'''Should be able to add and delete new plugins. '''
|
'''Should be able to add and delete new plugins. '''
|
||||||
new = plugins.Analyser(name='new', description='new', version=0)
|
new = plugins.Analyser(name='new', description='new', version=0)
|
||||||
self.senpy.add_plugin(new)
|
self.senpy.add_plugin(new)
|
||||||
assert new in self.senpy.plugins(is_activated=False)
|
assert new in self.senpy.plugins()
|
||||||
self.senpy.delete_plugin(new)
|
self.senpy.delete_plugin(new)
|
||||||
assert new not in self.senpy.plugins(is_activated=False)
|
assert new not in self.senpy.plugins()
|
||||||
|
|
||||||
def test_adding_folder(self):
|
def test_adding_folder(self):
|
||||||
""" It should be possible for senpy to look for plugins in more folders. """
|
""" It should be possible for senpy to look for plugins in more folders. """
|
||||||
@ -74,7 +74,7 @@ class ExtensionsTest(TestCase):
|
|||||||
default_plugins=False)
|
default_plugins=False)
|
||||||
assert not senpy.analysis_plugins()
|
assert not senpy.analysis_plugins()
|
||||||
senpy.add_folder(self.examples_dir)
|
senpy.add_folder(self.examples_dir)
|
||||||
assert senpy.plugins(plugin_type=plugins.Analyser, is_activated=False)
|
assert senpy.plugins(plugin_type=plugins.Analyser)
|
||||||
self.assertRaises(AttributeError, senpy.add_folder, 'DOES NOT EXIST')
|
self.assertRaises(AttributeError, senpy.add_folder, 'DOES NOT EXIST')
|
||||||
|
|
||||||
def test_installing(self):
|
def test_installing(self):
|
||||||
@ -94,9 +94,8 @@ class ExtensionsTest(TestCase):
|
|||||||
|
|
||||||
def test_enabling(self):
|
def test_enabling(self):
|
||||||
""" Enabling a plugin """
|
""" Enabling a plugin """
|
||||||
self.senpy.activate_all(sync=True)
|
|
||||||
assert len(self.senpy.plugins()) >= 3
|
assert len(self.senpy.plugins()) >= 3
|
||||||
assert self.senpy.get_plugin("Sleep").is_activated
|
assert self.senpy.get_plugin("Sleep")
|
||||||
|
|
||||||
def test_installing_nonexistent(self):
|
def test_installing_nonexistent(self):
|
||||||
""" Fail if the dependencies cannot be met """
|
""" Fail if the dependencies cannot be met """
|
||||||
@ -110,23 +109,14 @@ class ExtensionsTest(TestCase):
|
|||||||
with self.assertRaises(Error):
|
with self.assertRaises(Error):
|
||||||
plugins.install_deps(info)
|
plugins.install_deps(info)
|
||||||
|
|
||||||
def test_disabling(self):
|
|
||||||
""" Disabling a plugin """
|
|
||||||
self.senpy.deactivate_all(sync=True)
|
|
||||||
assert not self.senpy.get_plugin("dummy").is_activated
|
|
||||||
assert not self.senpy.get_plugin("sleep").is_activated
|
|
||||||
|
|
||||||
def test_default(self):
|
def test_default(self):
|
||||||
""" Default plugin should be set """
|
""" Default plugin should be set """
|
||||||
assert self.senpy.default_plugin
|
assert self.senpy.default_plugin
|
||||||
assert self.senpy.default_plugin.name == "dummy"
|
assert self.senpy.default_plugin.name == "dummy"
|
||||||
self.senpy.deactivate_all(sync=True)
|
|
||||||
logging.debug("Default: {}".format(self.senpy.default_plugin))
|
|
||||||
assert self.senpy.default_plugin is None
|
|
||||||
|
|
||||||
def test_noplugin(self):
|
def test_noplugin(self):
|
||||||
""" Don't analyse if there isn't any plugin installed """
|
""" Don't analyse if there isn't any plugin installed """
|
||||||
self.senpy.deactivate_all(sync=True)
|
nosenpy = Senpy(default_plugins=False, plugin_folders=[])
|
||||||
self.assertRaises(Error, partial(analyse, self.senpy, input="tupni"))
|
self.assertRaises(Error, partial(analyse, self.senpy, input="tupni"))
|
||||||
|
|
||||||
def test_analyse(self):
|
def test_analyse(self):
|
||||||
@ -177,7 +167,7 @@ class ExtensionsTest(TestCase):
|
|||||||
|
|
||||||
def test_analyse_error(self):
|
def test_analyse_error(self):
|
||||||
class ErrorPlugin(plugins.Analyser):
|
class ErrorPlugin(plugins.Analyser):
|
||||||
author = 'nobody'
|
author: str = 'nobody'
|
||||||
version = 0
|
version = 0
|
||||||
ex = Error()
|
ex = Error()
|
||||||
|
|
||||||
@ -205,17 +195,12 @@ class ExtensionsTest(TestCase):
|
|||||||
""" Filtering plugins """
|
""" Filtering plugins """
|
||||||
assert len(self.senpy.plugins(name="Dummy")) > 0
|
assert len(self.senpy.plugins(name="Dummy")) > 0
|
||||||
assert not len(self.senpy.plugins(name="NotDummy"))
|
assert not len(self.senpy.plugins(name="NotDummy"))
|
||||||
assert self.senpy.plugins(name="Dummy", is_activated=True)
|
|
||||||
self.senpy.deactivate_plugin("Dummy", sync=True)
|
|
||||||
assert not len(self.senpy.plugins(name="Dummy",
|
|
||||||
is_activated=True))
|
|
||||||
|
|
||||||
def test_load_default_plugins(self):
|
def test_load_default_plugins(self):
|
||||||
senpy = Senpy(plugin_folder=self.examples_dir, default_plugins=True)
|
senpy = Senpy(plugin_folder=self.examples_dir, default_plugins=True)
|
||||||
assert len(senpy.plugins(is_activated=False)) > 1
|
assert len(senpy.plugins()) > 1
|
||||||
|
|
||||||
def test_convert_emotions(self):
|
def test_convert_emotions(self):
|
||||||
self.senpy.activate_all(sync=True)
|
|
||||||
plugin = Plugin({
|
plugin = Plugin({
|
||||||
'id': 'imaginary',
|
'id': 'imaginary',
|
||||||
'onyx:usesEmotionModel': 'emoml:fsre-dimensions'
|
'onyx:usesEmotionModel': 'emoml:fsre-dimensions'
|
||||||
|
@ -26,14 +26,14 @@ from senpy.models import (Analysis,
|
|||||||
EmotionAnalysis,
|
EmotionAnalysis,
|
||||||
EmotionSet,
|
EmotionSet,
|
||||||
Entry,
|
Entry,
|
||||||
Error,
|
ErrorResponse,
|
||||||
Results,
|
Results,
|
||||||
Sentiment,
|
Sentiment,
|
||||||
SentimentPlugin,
|
SentimentPlugin,
|
||||||
Plugins,
|
Plugins,
|
||||||
from_string,
|
from_string,
|
||||||
from_dict,
|
from_dict)
|
||||||
subtypes)
|
from senpy.errors import Error
|
||||||
from senpy import plugins
|
from senpy import plugins
|
||||||
from pprint import pprint
|
from pprint import pprint
|
||||||
|
|
||||||
@ -117,7 +117,7 @@ class ModelsTest(TestCase):
|
|||||||
|
|
||||||
def test_plugins(self):
|
def test_plugins(self):
|
||||||
self.assertRaises(Error, plugins.Plugin)
|
self.assertRaises(Error, plugins.Plugin)
|
||||||
p = plugins.SentimentPlugin({"name": "dummy",
|
p = plugins.SentimentPlugin.parse_obj({"name": "dummy",
|
||||||
"description": "I do nothing",
|
"description": "I do nothing",
|
||||||
"version": 0,
|
"version": 0,
|
||||||
"extra_params": {
|
"extra_params": {
|
||||||
@ -152,11 +152,6 @@ 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()
|
||||||
|
@ -35,24 +35,23 @@ ROOT = os.path.join(os.path.dirname(__file__), '..')
|
|||||||
|
|
||||||
class ShelfDummyPlugin(plugins.SentimentPlugin, plugins.ShelfMixin):
|
class ShelfDummyPlugin(plugins.SentimentPlugin, plugins.ShelfMixin):
|
||||||
'''Dummy plugin for tests.'''
|
'''Dummy plugin for tests.'''
|
||||||
name = 'Shelf'
|
name: str = 'Shelf'
|
||||||
version = 0
|
version: str = '0'
|
||||||
author = 'the senpy community'
|
author: str = 'the senpy community'
|
||||||
|
|
||||||
def activate(self, *args, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
|
super().__init__(**kwargs)
|
||||||
if 'counter' not in self.sh:
|
if 'counter' not in self.sh:
|
||||||
self.sh['counter'] = 0
|
self.sh['counter'] = 0
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
def deactivate(self, *args, **kwargs):
|
|
||||||
self.save()
|
|
||||||
|
|
||||||
def analyse(self, *args, **kwargs):
|
def analyse(self, *args, **kwargs):
|
||||||
self.sh['counter'] = self.sh['counter'] + 1
|
self.sh['counter'] = self.sh['counter'] + 1
|
||||||
e = Entry()
|
e = Entry()
|
||||||
e.nif__isString = self.sh['counter']
|
e.nif__isString = self.sh['counter']
|
||||||
r = Results()
|
r = Results()
|
||||||
r.entries.append(e)
|
r.entries.append(e)
|
||||||
|
self.save()
|
||||||
return r
|
return r
|
||||||
|
|
||||||
|
|
||||||
@ -82,7 +81,6 @@ class PluginsTest(TestCase):
|
|||||||
info={'name': 'default_shelve_file',
|
info={'name': 'default_shelve_file',
|
||||||
'description': 'Dummy plugin for tests',
|
'description': 'Dummy plugin for tests',
|
||||||
'version': 'test'})
|
'version': 'test'})
|
||||||
a.activate()
|
|
||||||
assert os.path.isfile(a.shelf_file)
|
assert os.path.isfile(a.shelf_file)
|
||||||
os.remove(a.shelf_file)
|
os.remove(a.shelf_file)
|
||||||
|
|
||||||
@ -114,8 +112,6 @@ class PluginsTest(TestCase):
|
|||||||
'version': 'test',
|
'version': 'test',
|
||||||
'shelf_file': newfile
|
'shelf_file': newfile
|
||||||
})
|
})
|
||||||
assert a.sh == {}
|
|
||||||
a.activate()
|
|
||||||
assert a.sh == {'counter': 0}
|
assert a.sh == {'counter': 0}
|
||||||
assert a.shelf_file == newfile
|
assert a.shelf_file == newfile
|
||||||
|
|
||||||
@ -137,12 +133,10 @@ class PluginsTest(TestCase):
|
|||||||
'shelf_file': self.shelf_file,
|
'shelf_file': self.shelf_file,
|
||||||
'version': 'test'
|
'version': 'test'
|
||||||
})
|
})
|
||||||
a.activate()
|
|
||||||
|
|
||||||
assert a.shelf_file == self.shelf_file
|
assert a.shelf_file == self.shelf_file
|
||||||
res1 = a.analyse(input=1)
|
res1 = a.analyse(input=1)
|
||||||
assert res1.entries[0].nif__isString == 100
|
assert res1.entries[0].nif__isString == 100
|
||||||
a.deactivate()
|
|
||||||
del a
|
del a
|
||||||
|
|
||||||
with open(self.shelf_file, 'rb') as f:
|
with open(self.shelf_file, 'rb') as f:
|
||||||
@ -190,7 +184,6 @@ class PluginsTest(TestCase):
|
|||||||
'version': 'test',
|
'version': 'test',
|
||||||
'shelf_file': self.shelf_file
|
'shelf_file': self.shelf_file
|
||||||
})
|
})
|
||||||
a.activate()
|
|
||||||
print('Shelf file: %s' % a.shelf_file)
|
print('Shelf file: %s' % a.shelf_file)
|
||||||
a.sh['a'] = 'fromA'
|
a.sh['a'] = 'fromA'
|
||||||
a.save()
|
a.save()
|
||||||
@ -201,7 +194,6 @@ class PluginsTest(TestCase):
|
|||||||
'version': 'test',
|
'version': 'test',
|
||||||
'shelf_file': self.shelf_file
|
'shelf_file': self.shelf_file
|
||||||
})
|
})
|
||||||
b.activate()
|
|
||||||
assert b.sh['a'] == 'fromA'
|
assert b.sh['a'] == 'fromA'
|
||||||
b.sh['a'] = 'fromB'
|
b.sh['a'] = 'fromB'
|
||||||
assert b.sh['a'] == 'fromB'
|
assert b.sh['a'] == 'fromB'
|
||||||
@ -228,8 +220,8 @@ class PluginsTest(TestCase):
|
|||||||
class MyBox(plugins.Box):
|
class MyBox(plugins.Box):
|
||||||
''' Vague description'''
|
''' Vague description'''
|
||||||
|
|
||||||
author = 'me'
|
author: str = 'me'
|
||||||
version = 0
|
version: str = 0
|
||||||
|
|
||||||
def to_features(self, entry, **kwargs):
|
def to_features(self, entry, **kwargs):
|
||||||
return entry.text.split()
|
return entry.text.split()
|
||||||
@ -243,7 +235,7 @@ class PluginsTest(TestCase):
|
|||||||
entry.myAnnotation = 'DETECTED'
|
entry.myAnnotation = 'DETECTED'
|
||||||
return entry
|
return entry
|
||||||
|
|
||||||
test_cases = [
|
test_cases: list[dict] = [
|
||||||
{
|
{
|
||||||
'input': "nothing here",
|
'input': "nothing here",
|
||||||
'expected': {'myAnnotation': 'DETECTED'},
|
'expected': {'myAnnotation': 'DETECTED'},
|
||||||
@ -260,8 +252,8 @@ class PluginsTest(TestCase):
|
|||||||
class SentimentBox(plugins.SentimentBox):
|
class SentimentBox(plugins.SentimentBox):
|
||||||
''' Vague description'''
|
''' Vague description'''
|
||||||
|
|
||||||
author = 'me'
|
author: str = 'me'
|
||||||
version = 0
|
version: str = 0
|
||||||
|
|
||||||
def predict_one(self, features, **kwargs):
|
def predict_one(self, features, **kwargs):
|
||||||
text = ' '.join(features)
|
text = ' '.join(features)
|
||||||
@ -269,7 +261,7 @@ class PluginsTest(TestCase):
|
|||||||
return [1, 0, 0]
|
return [1, 0, 0]
|
||||||
return [0, 0, 1]
|
return [0, 0, 1]
|
||||||
|
|
||||||
test_cases = [
|
test_cases: list[dict] = [
|
||||||
{
|
{
|
||||||
'input': 'a happy face :)',
|
'input': 'a happy face :)',
|
||||||
'polarity': 'marl:Positive'
|
'polarity': 'marl:Positive'
|
||||||
@ -355,7 +347,7 @@ class PluginsTest(TestCase):
|
|||||||
|
|
||||||
class DummyPlugin(plugins.SentimentBox):
|
class DummyPlugin(plugins.SentimentBox):
|
||||||
description = 'Plugin to test evaluation'
|
description = 'Plugin to test evaluation'
|
||||||
version = 0
|
version: str = 0
|
||||||
|
|
||||||
classes = ['marl:Positive', 'marl:Negative']
|
classes = ['marl:Positive', 'marl:Negative']
|
||||||
|
|
||||||
@ -365,7 +357,7 @@ class PluginsTest(TestCase):
|
|||||||
|
|
||||||
class SmartPlugin(plugins.SentimentBox):
|
class SmartPlugin(plugins.SentimentBox):
|
||||||
description = 'Plugin to test evaluation'
|
description = 'Plugin to test evaluation'
|
||||||
version = 0
|
version: str = 0
|
||||||
|
|
||||||
classes = ['marl:Positive', 'marl:Negative']
|
classes = ['marl:Positive', 'marl:Negative']
|
||||||
|
|
||||||
|
@ -1,80 +0,0 @@
|
|||||||
#
|
|
||||||
# Copyright 2014 Grupo de Sistemas Inteligentes (GSI) DIT, UPM
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
# you may not use this file except in compliance with the License.
|
|
||||||
# You may obtain a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
# See the License for the specific language governing permissions and
|
|
||||||
# limitations under the License.
|
|
||||||
#
|
|
||||||
|
|
||||||
from __future__ import print_function
|
|
||||||
|
|
||||||
import json
|
|
||||||
import unittest
|
|
||||||
import os
|
|
||||||
from os import path
|
|
||||||
from fnmatch import fnmatch
|
|
||||||
|
|
||||||
from jsonschema import RefResolver, Draft4Validator, ValidationError
|
|
||||||
|
|
||||||
from senpy.models import read_schema
|
|
||||||
|
|
||||||
root_path = path.join(path.dirname(path.realpath(__file__)), '..')
|
|
||||||
schema_folder = path.join(root_path, 'senpy', 'schemas')
|
|
||||||
examples_path = path.join(root_path, 'docs', 'examples')
|
|
||||||
bad_examples_path = path.join(root_path, 'docs', 'bad-examples')
|
|
||||||
|
|
||||||
|
|
||||||
class JSONSchemaTests(unittest.TestCase):
|
|
||||||
def test_definitions(self):
|
|
||||||
read_schema('definitions.json')
|
|
||||||
|
|
||||||
|
|
||||||
def do_create_(jsfile, success):
|
|
||||||
def do_expected(self):
|
|
||||||
with open(jsfile) as f:
|
|
||||||
js = json.load(f)
|
|
||||||
try:
|
|
||||||
assert '@type' in js
|
|
||||||
schema_name = js['@type']
|
|
||||||
with open(os.path.join(schema_folder, schema_name +
|
|
||||||
".json")) as file_object:
|
|
||||||
schema = json.load(file_object)
|
|
||||||
resolver = RefResolver('file://' + schema_folder + '/', schema)
|
|
||||||
validator = Draft4Validator(schema, resolver=resolver)
|
|
||||||
validator.validate(js)
|
|
||||||
except (AssertionError, ValidationError, KeyError) as ex:
|
|
||||||
if success:
|
|
||||||
raise
|
|
||||||
return
|
|
||||||
assert success
|
|
||||||
return do_expected
|
|
||||||
|
|
||||||
|
|
||||||
def add_examples(dirname, success):
|
|
||||||
for dirpath, dirnames, filenames in os.walk(dirname):
|
|
||||||
for i in filenames:
|
|
||||||
if fnmatch(i, '*.json'):
|
|
||||||
filename = path.join(dirpath, i)
|
|
||||||
test_method = do_create_(filename, success)
|
|
||||||
test_method.__name__ = 'test_file_%s_success_%s' % (filename,
|
|
||||||
success)
|
|
||||||
test_method.__doc__ = '%s should %svalidate' % (filename, ''
|
|
||||||
if success else
|
|
||||||
'not')
|
|
||||||
setattr(JSONSchemaTests, test_method.__name__, test_method)
|
|
||||||
del test_method
|
|
||||||
|
|
||||||
|
|
||||||
add_examples(examples_path, True)
|
|
||||||
add_examples(bad_examples_path, False)
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
unittest.main()
|
|
@ -42,12 +42,10 @@ class SemanticsTest(TestCase):
|
|||||||
"""Set up only once, and re-use in every individual test"""
|
"""Set up only once, and re-use in every individual test"""
|
||||||
cls.app = Flask("test_extensions")
|
cls.app = Flask("test_extensions")
|
||||||
cls.client = cls.app.test_client()
|
cls.client = cls.app.test_client()
|
||||||
cls.senpy = Senpy(default_plugins=True)
|
cls.senpy = Senpy(plugin_folder=None, default_plugins=True)
|
||||||
cls.senpy.init_app(cls.app)
|
cls.senpy.init_app(cls.app)
|
||||||
cls.dir = os.path.join(os.path.dirname(__file__), "..")
|
#cls.dir = os.path.join(os.path.dirname(__file__), "..")
|
||||||
cls.senpy.add_folder(cls.dir)
|
#cls.senpy.add_folder(cls.dir)
|
||||||
cls.senpy.activate_all()
|
|
||||||
cls.senpy.default_plugin = 'Dummy'
|
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.app.config['TESTING'] = True # Tell Flask not to catch Exceptions
|
self.app.config['TESTING'] = True # Tell Flask not to catch Exceptions
|
||||||
@ -57,10 +55,10 @@ class SemanticsTest(TestCase):
|
|||||||
|
|
||||||
def test_sentiment(self):
|
def test_sentiment(self):
|
||||||
"""
|
"""
|
||||||
A sentiment analysis call in JSON-LD
|
a sentiment analysis call in json-ld
|
||||||
"""
|
"""
|
||||||
# We use expanded JSON-LD and ignore the context, because in general
|
# we use expanded json-ld and ignore the context, because in general
|
||||||
# the context is a URIS to the service and that URI is not
|
# the context is a uris to the service and that uri is not
|
||||||
# available outside of self.client
|
# available outside of self.client
|
||||||
params = {
|
params = {
|
||||||
'input': 'hello',
|
'input': 'hello',
|
||||||
@ -69,28 +67,28 @@ class SemanticsTest(TestCase):
|
|||||||
'expanded': True,
|
'expanded': True,
|
||||||
'prefix': 'http://default.example/#'
|
'prefix': 'http://default.example/#'
|
||||||
}
|
}
|
||||||
resp = self.client.get("/api/basic?{}".format(urlencode(params)))
|
resp = self.client.get("/api/sentiment-basic?{}".format(urlencode(params)))
|
||||||
self.assertCode(resp, 200)
|
self.assertCode(resp, 200)
|
||||||
g = parse_resp(resp, fmt='json-ld')
|
g = parse_resp(resp, fmt='json-ld')
|
||||||
|
print('Got this graph: ', g.serialize(format='ttl'))
|
||||||
assert g
|
assert g
|
||||||
qres = g.query("""
|
qres = g.query("""
|
||||||
PREFIX prov: <http://www.w3.org/ns/prov#>
|
prefix prov: <http://www.w3.org/ns/prov#>
|
||||||
PREFIX marl: <http://www.gsi.upm.es/ontologies/marl/ns#>
|
prefix marl: <http://www.gsi.upm.es/ontologies/marl/ns#>
|
||||||
PREFIX nif: <http://persistence.uni-leipzig.org/nlp2rdf/ontologies/nif-core#>
|
prefix nif: <http://persistence.uni-leipzig.org/nlp2rdf/ontologies/nif-core#>
|
||||||
PREFIX onyx: <http://www.gsi.upm.es/ontologies/onyx/ns#>
|
prefix onyx: <http://www.gsi.upm.es/ontologies/onyx/ns#>
|
||||||
PREFIX senpy: <http://www.gsi.upm.es/onto/senpy/ns#>
|
prefix senpy: <http://www.gsi.upm.es/ontologies/senpy/ns#>
|
||||||
|
|
||||||
SELECT DISTINCT ?entry ?text ?sentiment
|
SELECT distinct ?entry ?text ?sentiment
|
||||||
WHERE {
|
WHERE {
|
||||||
?entry a senpy:Entry .
|
?entry a senpy:Entry .
|
||||||
?entry marl:hasOpinion ?o .
|
?entry marl:hasOpinion ?o .
|
||||||
?entry nif:isString ?text .
|
?entry nif:isString ?text .
|
||||||
?o marl:hasPolarity ?sentiment .
|
?o marl:hasPolarity ?sentiment .
|
||||||
}""")
|
}""")
|
||||||
assert len(qres) == 1
|
assert len(qres) == 1, "There should only be one result"
|
||||||
entry, text, sentiment = list(qres)[0]
|
entry, text, sentiment = list(qres)[0]
|
||||||
assert entry
|
assert str(text) == 'hello', "The returned text does not match the input text."
|
||||||
assert str(text) == 'hello'
|
|
||||||
assert str(sentiment) in ['marl:Positive', 'marl:Neutral', 'marl:Negative']
|
assert str(sentiment) in ['marl:Positive', 'marl:Neutral', 'marl:Negative']
|
||||||
|
|
||||||
def test_sentiment_turtle(self):
|
def test_sentiment_turtle(self):
|
||||||
@ -104,25 +102,75 @@ class SemanticsTest(TestCase):
|
|||||||
'expanded': True,
|
'expanded': True,
|
||||||
'prefix': 'http://default.example/#'
|
'prefix': 'http://default.example/#'
|
||||||
}
|
}
|
||||||
resp = self.client.get("/api/basic?{}".format(urlencode(params)))
|
resp = self.client.get("/api/sentiment-basic?{}".format(urlencode(params)))
|
||||||
self.assertCode(resp, 200)
|
self.assertCode(resp, 200)
|
||||||
g = parse_resp(resp, 'ttl')
|
g = parse_resp(resp, 'ttl')
|
||||||
|
print('Got this graph: ', g.serialize(format='ttl'))
|
||||||
qres = g.query("""
|
qres = g.query("""
|
||||||
PREFIX prov: <http://www.w3.org/ns/prov#>
|
PREFIX prov: <http://www.w3.org/ns/prov#>
|
||||||
PREFIX marl: <http://www.gsi.upm.es/ontologies/marl/ns#>
|
PREFIX marl: <http://www.gsi.upm.es/ontologies/marl/ns#>
|
||||||
PREFIX nif: <http://persistence.uni-leipzig.org/nlp2rdf/ontologies/nif-core#>
|
PREFIX nif: <http://persistence.uni-leipzig.org/nlp2rdf/ontologies/nif-core#>
|
||||||
PREFIX onyx: <http://www.gsi.upm.es/ontologies/onyx/ns#>
|
PREFIX onyx: <http://www.gsi.upm.es/ontologies/onyx/ns#>
|
||||||
PREFIX senpy: <http://www.gsi.upm.es/onto/senpy/ns#>
|
PREFIX senpy: <http://www.gsi.upm.es/ontologies/senpy/ns#>
|
||||||
|
|
||||||
SELECT DISTINCT ?entry ?text ?sentiment
|
SELECT DISTINCT ?entry ?text ?sentiment
|
||||||
WHERE {
|
WHERE {
|
||||||
?entry a senpy:Entry .
|
?entry a senpy:Entry ;
|
||||||
?entry marl:hasOpinion ?o .
|
nif:isString ?text ;
|
||||||
?entry nif:isString ?text .
|
marl:hasOpinion [
|
||||||
?o marl:hasPolarity ?sentiment .
|
marl:hasPolarity ?sentiment
|
||||||
|
] .
|
||||||
}""")
|
}""")
|
||||||
|
assert len(qres) == 1, "There should only be one row in the result"
|
||||||
|
entry, text, sentiment = list(qres)[0]
|
||||||
|
assert str(text) == 'hello', "Returned text does not match input text"
|
||||||
|
assert str(sentiment) in ['marl:Positive', 'marl:Neutral', 'marl:Negative']
|
||||||
|
|
||||||
|
def test_moral(self):
|
||||||
|
"""
|
||||||
|
An example of a moral analysis, adapted from the examples for the AMOR project:
|
||||||
|
http://www.gsi.upm.es/ontologies/amor/examples
|
||||||
|
"""
|
||||||
|
# we use expanded json-ld and ignore the context, because in general
|
||||||
|
# the context is a uris to the service and that uri is not
|
||||||
|
# available outside of self.client
|
||||||
|
params = {
|
||||||
|
'input': 'hello',
|
||||||
|
'in-headers': True,
|
||||||
|
'outformat': 'json-ld',
|
||||||
|
'expanded': True,
|
||||||
|
'prefix': 'http://default.example/#'
|
||||||
|
}
|
||||||
|
resp = self.client.get("/api/sentiment-basic?{}".format(urlencode(params)))
|
||||||
|
self.assertCode(resp, 200)
|
||||||
|
g = parse_resp(resp, fmt='json-ld')
|
||||||
|
print('Got this graph: ', g.serialize(format='ttl'))
|
||||||
|
assert g
|
||||||
|
qres = g.query("""
|
||||||
|
prefix : <http://www.gsi.upm.es/ontologies/amor/examples#>
|
||||||
|
prefix amor: <http://www.gsi.upm.es/ontologies/amor/ns#>
|
||||||
|
prefix amor-bhv: <http://www.gsi.upm.es/ontologies/amor/models/bhv/ns#>
|
||||||
|
prefix amor-mft: <http://www.gsi.upm.es/ontologies/amor/models/mft/ns#>
|
||||||
|
prefix bhv: <http://www.gsi.upm.es/ontologies/bhv#>
|
||||||
|
prefix mft: <http://www.gsi.upm.es/ontologies/mft/ns#>
|
||||||
|
prefix mls: <http://www.w3.org/ns/mls#>
|
||||||
|
prefix owl: <http://www.w3.org/2002/07/owl#>
|
||||||
|
prefix prov: <http://www.w3.org/ns/prov#>
|
||||||
|
prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#>
|
||||||
|
prefix schema: <http://schema.org/>
|
||||||
|
|
||||||
|
SELECT ?analysis ?agent ?model ?annotation ?origin ?category
|
||||||
|
WHERE {
|
||||||
|
?analysis a amor:MoralValueAnalysis ;
|
||||||
|
prov:wasAssociatedWith ?agent ;
|
||||||
|
amor:usedMoralValueModel ?model ;
|
||||||
|
amor:analysed ?origin ;
|
||||||
|
prov:generated ?annotation .
|
||||||
|
?annotation a amor:MoralValueAnnotation ;
|
||||||
|
amor:hasMoralValueCategory ?category .
|
||||||
|
}""")
|
||||||
assert len(qres) == 1
|
assert len(qres) == 1
|
||||||
entry, text, sentiment = list(qres)[0]
|
entry, text, sentiment = list(qres)[0]
|
||||||
assert entry
|
assert entry
|
||||||
assert str(text) == 'hello'
|
assert str(text) == 'hello'
|
||||||
assert str(sentiment) in ['marl:Positive', 'marl:Neutral', 'marl:Negative']
|
assert str(sentiment) in ['marl:positive', 'marl:neutral', 'marl:negative']
|
||||||
|
Loading…
Reference in New Issue
Block a user