From b543a4614ee7186a4067b1a4361d15a7f0f7a28f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=2E=20Fernando=20S=C3=A1nchez?= Date: Tue, 10 Jan 2017 10:02:14 +0100 Subject: [PATCH] Improved schema validation * Added debug Dockerfile/Makefile * Validation of examples in docs --- Dockerfile-debug-3.4 | 5 ++ Makefile | 10 ++- docs/bad-examples/plugins/noplugins.json | 4 ++ .../results/example-basic-FAIL.json | 18 ++++++ docs/examples/plugins/noplugins.json | 5 ++ .../examples/{ => results}/example-basic.json | 1 + .../{ => results}/example-complete.json | 1 + .../{ => results}/example-emotion.json | 1 + docs/examples/{ => results}/example-ner.json | 1 + docs/examples/results/example-pad.json | 46 ++++++++++++++ .../{ => results}/example-sentiment.json | 1 + .../{ => results}/example-suggestion.json | 1 + senpy/VERSION | 2 +- senpy/models.py | 22 ++++--- senpy/schemas/dimensions.json | 9 +++ senpy/schemas/emotionAnalysis.json | 18 ++++++ senpy/schemas/emotionModel.json | 15 +++-- senpy/schemas/plugins.json | 19 ++++-- senpy/schemas/response.json | 7 ++- senpy/schemas/results.json | 62 +++++++++++-------- tests/test_schemas.py | 52 ++++++++++++++++ 21 files changed, 249 insertions(+), 51 deletions(-) create mode 100644 Dockerfile-debug-3.4 create mode 100644 docs/bad-examples/plugins/noplugins.json create mode 100644 docs/bad-examples/results/example-basic-FAIL.json create mode 100644 docs/examples/plugins/noplugins.json rename docs/examples/{ => results}/example-basic.json (94%) rename docs/examples/{ => results}/example-complete.json (99%) rename docs/examples/{ => results}/example-emotion.json (98%) rename docs/examples/{ => results}/example-ner.json (98%) create mode 100644 docs/examples/results/example-pad.json rename docs/examples/{ => results}/example-sentiment.json (97%) rename docs/examples/{ => results}/example-suggestion.json (97%) create mode 100644 senpy/schemas/dimensions.json create mode 100644 senpy/schemas/emotionAnalysis.json create mode 100644 tests/test_schemas.py diff --git a/Dockerfile-debug-3.4 b/Dockerfile-debug-3.4 new file mode 100644 index 0000000..86ab349 --- /dev/null +++ b/Dockerfile-debug-3.4 @@ -0,0 +1,5 @@ +from python:3.4 + +RUN pip install pytest +ADD requirements.txt /usr/src/app/ +RUN pip install -r /usr/src/app/requirements.txt diff --git a/Makefile b/Makefile index 33ab24a..cf8c4fb 100644 --- a/Makefile +++ b/Makefile @@ -20,12 +20,17 @@ buildall: $(addprefix build-, $(PYVERSIONS)) build-%: Dockerfile-% docker build -t '$(REPO)/$(NAME):$(VERSION)-python$*' -f Dockerfile-$* .; +build-debug-%: + docker build -t '$(NAME)-debug' -f Dockerfile-debug-$* .; + test: $(addprefix test-,$(PYMAIN)) testall: $(addprefix test-,$(PYVERSIONS)) -debug-%: build-% - docker run --rm -w /usr/src/app/ -v $$PWD:/usr/src/app --entrypoint=/bin/bash -ti '$(REPO)/$(NAME):$(VERSION)-python$*' ; +debug-%: build-debug-% + docker run --rm -w /usr/src/app/ -v $$PWD:/usr/src/app --entrypoint=/bin/bash -ti $(NAME)-debug ; + +debug: debug-$(PYMAIN) test-%: build-% docker run --rm -w /usr/src/app/ --entrypoint=/usr/local/bin/python -ti '$(REPO)/$(NAME):$(VERSION)-python$*' setup.py test --addopts "-vvv -s" ; @@ -51,6 +56,7 @@ upload: testall $(addprefix upload-,$(PYVERSIONS)) clean: @docker ps -a | awk '/$(REPO)\/$(NAME)/{ split($$2, vers, "-"); if(vers[1] != "${VERSION}"){ print $$1;}}' | xargs docker rm 2>/dev/null|| true @docker images | awk '/$(REPO)\/$(NAME)/{ split($$2, vers, "-"); if(vers[1] != "${VERSION}"){ print $$1":"$$2;}}' | xargs docker rmi 2>/dev/null|| true + @docker rmi $(NAME)-debug 2>/dev/null || true upload_git: git commit -a diff --git a/docs/bad-examples/plugins/noplugins.json b/docs/bad-examples/plugins/noplugins.json new file mode 100644 index 0000000..9b50fb4 --- /dev/null +++ b/docs/bad-examples/plugins/noplugins.json @@ -0,0 +1,4 @@ +{ + "plugins": [ + ] +} diff --git a/docs/bad-examples/results/example-basic-FAIL.json b/docs/bad-examples/results/example-basic-FAIL.json new file mode 100644 index 0000000..288843c --- /dev/null +++ b/docs/bad-examples/results/example-basic-FAIL.json @@ -0,0 +1,18 @@ +{ + "@context": "http://mixedemotions-project.eu/ns/context.jsonld", + "@id": "http://example.com#NIFExample", + "@type": "results", + "analysis": [ + ], + "entries": [ + { + "@type": [ + "nif:RFC5147String", + "nif:Context" + ], + "nif:beginIndex": 0, + "nif:endIndex": 40, + "nif:isString": "My favourite actress is Natalie Portman" + } + ] +} diff --git a/docs/examples/plugins/noplugins.json b/docs/examples/plugins/noplugins.json new file mode 100644 index 0000000..3cc12e3 --- /dev/null +++ b/docs/examples/plugins/noplugins.json @@ -0,0 +1,5 @@ +{ + "@type": "plugins", + "plugins": [ + ] +} diff --git a/docs/examples/example-basic.json b/docs/examples/results/example-basic.json similarity index 94% rename from docs/examples/example-basic.json rename to docs/examples/results/example-basic.json index c9f427b..c308f0f 100644 --- a/docs/examples/example-basic.json +++ b/docs/examples/results/example-basic.json @@ -1,6 +1,7 @@ { "@context": "http://mixedemotions-project.eu/ns/context.jsonld", "@id": "http://example.com#NIFExample", + "@type": "results", "analysis": [ ], "entries": [ diff --git a/docs/examples/example-complete.json b/docs/examples/results/example-complete.json similarity index 99% rename from docs/examples/example-complete.json rename to docs/examples/results/example-complete.json index 420e7ab..53d105b 100644 --- a/docs/examples/example-complete.json +++ b/docs/examples/results/example-complete.json @@ -1,6 +1,7 @@ { "@context": "http://mixedemotions-project.eu/ns/context.jsonld", "@id": "me:Result1", + "@type": "results", "analysis": [ { "@id": "me:SAnalysis1", diff --git a/docs/examples/example-emotion.json b/docs/examples/results/example-emotion.json similarity index 98% rename from docs/examples/example-emotion.json rename to docs/examples/results/example-emotion.json index 965d91a..452606a 100644 --- a/docs/examples/example-emotion.json +++ b/docs/examples/results/example-emotion.json @@ -1,6 +1,7 @@ { "@context": "http://mixedemotions-project.eu/ns/context.jsonld", "@id": "me:Result1", + "@type": "results", "analysis": [ { "@id": "me:EmotionAnalysis1", diff --git a/docs/examples/example-ner.json b/docs/examples/results/example-ner.json similarity index 98% rename from docs/examples/example-ner.json rename to docs/examples/results/example-ner.json index 27535a5..d177bf8 100644 --- a/docs/examples/example-ner.json +++ b/docs/examples/results/example-ner.json @@ -1,6 +1,7 @@ { "@context": "http://mixedemotions-project.eu/ns/context.jsonld", "@id": "me:Result1", + "@type": "results", "analysis": [ { "@id": "me:NER1", diff --git a/docs/examples/results/example-pad.json b/docs/examples/results/example-pad.json new file mode 100644 index 0000000..bf90bbc --- /dev/null +++ b/docs/examples/results/example-pad.json @@ -0,0 +1,46 @@ +{ +"@context": [ + "http://mixedemotions-project.eu/ns/context.jsonld", + { + "emovoc": "http://www.gsi.dit.upm.es/ontologies/onyx/vocabularies/emotionml/ns#" + } + ], + "@id": "me:Result1", + "@type": "results", + "analysis": [ + { + "@id": "me:HesamsAnalysis", + "@type": "onyx:EmotionAnalysis", + "onyx:usesEmotionModel": "emovoc:pad-dimensions" + } + ], + "entries": [ + { + "@id": "Entry1", + "@type": [ + "nif:RFC5147String", + "nif:Context" + ], + "nif:isString": "This is a test string", + "entities": [ + ], + "suggestions": [ + ], + "sentiments": [ + ], + "emotions": [ + { + "@id": "Entry1#char=0,21", + "nif:anchorOf": "This is a test string", + "prov:wasGeneratedBy": "me:HesamAnalysis", + "onyx:hasEmotion": [ + { + "emovoc:pleasure": 0.5, + "emovoc:arousal": 0.7 + } + ] + } + ] + } + ] +} diff --git a/docs/examples/example-sentiment.json b/docs/examples/results/example-sentiment.json similarity index 97% rename from docs/examples/example-sentiment.json rename to docs/examples/results/example-sentiment.json index ffdcc60..175cb56 100644 --- a/docs/examples/example-sentiment.json +++ b/docs/examples/results/example-sentiment.json @@ -1,6 +1,7 @@ { "@context": "http://mixedemotions-project.eu/ns/context.jsonld", "@id": "me:Result1", + "@type": "results", "analysis": [ { "@id": "me:SAnalysis1", diff --git a/docs/examples/example-suggestion.json b/docs/examples/results/example-suggestion.json similarity index 97% rename from docs/examples/example-suggestion.json rename to docs/examples/results/example-suggestion.json index 339be64..2ba943a 100644 --- a/docs/examples/example-suggestion.json +++ b/docs/examples/results/example-suggestion.json @@ -1,6 +1,7 @@ { "@context": "http://mixedemotions-project.eu/ns/context.jsonld", "@id": "me:Result1", + "@type": "results", "analysis": [ { "@id": "me:SgAnalysis1", diff --git a/senpy/VERSION b/senpy/VERSION index b616048..2337036 100644 --- a/senpy/VERSION +++ b/senpy/VERSION @@ -1 +1 @@ -0.6.2 +pre-0.7.0 diff --git a/senpy/models.py b/senpy/models.py index 457b881..26e1f10 100644 --- a/senpy/models.py +++ b/senpy/models.py @@ -36,7 +36,6 @@ def read_schema(schema_file, absolute=False): return jsonref.load(f, base_uri=schema_uri) - base_schema = read_schema(DEFINITIONS_FILE) logging.debug(base_schema) @@ -157,18 +156,16 @@ class SenpyModel(SenpyMixin, dict): temp = dict(*args, **kwargs) + for obj in [self.schema,]+self.schema.get('allOf', []): + for k, v in obj.get('properties', {}).items(): + if 'default' in v: + temp[k] = copy.deepcopy(v['default']) + for i in temp: nk = self._get_key(i) if nk != i: temp[nk] = temp[i] del temp[i] - - reqs = self.schema.get('required', []) - for i in reqs: - if i not in temp: - prop = self.schema['properties'][i] - if 'default' in prop: - temp[i] = copy.deepcopy(prop['default']) if 'context' in temp: context = temp['context'] del temp['context'] @@ -226,12 +223,21 @@ class EmotionSet(SenpyModel): class Emotion(SenpyModel): schema = read_schema('emotion.json') +class EmotionModel(SenpyModel): + schema = read_schema('emotionModel.json') + class Suggestion(SenpyModel): schema = read_schema('suggestion.json') class PluginModel(SenpyModel): schema = read_schema('plugin.json') +class EmotionPluginModel(SenpyModel): + schema = read_schema('plugin.json') + +class SentimentPluginModel(SenpyModel): + schema = read_schema('plugin.json') + class Plugins(SenpyModel): schema = read_schema('plugins.json') diff --git a/senpy/schemas/dimensions.json b/senpy/schemas/dimensions.json new file mode 100644 index 0000000..f2d1468 --- /dev/null +++ b/senpy/schemas/dimensions.json @@ -0,0 +1,9 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "properties": { + "name": {"type": "string"}, + "maxValue": {"type": "number"}, + "minValue": {"type": "number"} + }, + "required": ["name", "maxValue", "minValue"] +} diff --git a/senpy/schemas/emotionAnalysis.json b/senpy/schemas/emotionAnalysis.json new file mode 100644 index 0000000..41a9f60 --- /dev/null +++ b/senpy/schemas/emotionAnalysis.json @@ -0,0 +1,18 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Senpy Emotion analysis", + "type": "object", + "allOf": [ + {"$ref": "analysis.json"}, + {"properties": + { + "onyx:hasEmotionModel": { + "anyOf": [ + {"type": "string"}, + {"$ref": "emotionModel.json"} + ] + } + }, + "required": ["onyx:hasEmotionModel"] + }] +} diff --git a/senpy/schemas/emotionModel.json b/senpy/schemas/emotionModel.json index b2ae5cc..4ce5392 100644 --- a/senpy/schemas/emotionModel.json +++ b/senpy/schemas/emotionModel.json @@ -8,17 +8,20 @@ "description": "Piece of context that contains the Sentiment", "type": "string" }, - "onyx:hasEmotion": { + "onyx:hasDimension": { + "type": "array", + "items": { + "$ref": "dimensions.json" + }, + "uniqueItems": true + }, + "onyx:hasEmotionCategory": { "type": "array", "items": { "$ref": "emotion.json" }, "default": [] - }, - "prov:wasGeneratedBy": { - "type": "string", - "description": "The ID of the analysis that generated this Emotion. The full object should be included in the \"analysis\" property of the root object" } }, - "required": ["@id", "prov:wasGeneratedBy", "onyx:hasEmotion"] + "required": ["@id", "onyx:hasEmotion"] } diff --git a/senpy/schemas/plugins.json b/senpy/schemas/plugins.json index 33ee664..5866b49 100644 --- a/senpy/schemas/plugins.json +++ b/senpy/schemas/plugins.json @@ -1,11 +1,18 @@ { "$schema": "http://json-schema.org/draft-04/schema#", - "properties": { - "plugins": { - "type": "array", - "items": { - "$ref": "plugin.json" + "allOf": [ + {"$ref": "response.json"}, + { + "properties": { + "plugins": { + "type": "array", + "items": { + "$ref": "plugin.json" + } + }, + "@type": { + } } } - } + ] } diff --git a/senpy/schemas/response.json b/senpy/schemas/response.json index 70090b1..13b3ee9 100644 --- a/senpy/schemas/response.json +++ b/senpy/schemas/response.json @@ -1,4 +1,9 @@ { "$schema": "http://json-schema.org/draft-04/schema#", - "type": "object" + "type": "object", + "properties": { + "@type": {"type": "string"} + }, + "required": ["@type"] + } diff --git a/senpy/schemas/results.json b/senpy/schemas/results.json index 73fdc0f..d699b56 100644 --- a/senpy/schemas/results.json +++ b/senpy/schemas/results.json @@ -1,31 +1,39 @@ { "$schema": "http://json-schema.org/draft-04/schema#", - "title": "Results", - "description": "The results of an analysis", - "type": "object", - "properties": { - "@context": { - "$ref": "context.json" - }, - "@id": { - "description": "ID of the analysis", - "type": "string" - }, - "analysis": { - "type": "array", - "default": [], - "items": { - "$ref": "analysis.json" - } - }, - "entries": { - "type": "array", - "default": [], - "items": { - "$ref": "entry.json" - } + "allOf": [ + {"$ref": "response.json"}, + { + "title": "Results", + "description": "The results of an analysis", + "type": "object", + "properties": { + "@context": { + "$ref": "context.json" + }, + "@type": { + "default": "results" + }, + "@id": { + "description": "ID of the analysis", + "type": "string" + }, + "analysis": { + "type": "array", + "default": [], + "items": { + "$ref": "analysis.json" + } + }, + "entries": { + "type": "array", + "default": [], + "items": { + "$ref": "entry.json" + } + } + + }, + "required": ["@id", "analysis", "entries"] } - - }, - "required": ["@id", "analysis", "entries"] + ] } diff --git a/tests/test_schemas.py b/tests/test_schemas.py new file mode 100644 index 0000000..b448d27 --- /dev/null +++ b/tests/test_schemas.py @@ -0,0 +1,52 @@ +from __future__ import print_function + +import json +import unittest +import os +from os import path +from fnmatch import fnmatch + +import pyld +from jsonschema import validate, RefResolver, Draft4Validator, ValidationError + +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): + pass + +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 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()