diff --git a/senpy/api.py b/senpy/api.py index dc0785d..3a45cbc 100644 --- a/senpy/api.py +++ b/senpy/api.py @@ -3,6 +3,10 @@ from .models import Error, Results, Entry, from_string import logging logger = logging.getLogger(__name__) + +boolean = (True, False) + + API_PARAMS = { "algorithm": { "aliases": ["algorithms", "a", "algo"], @@ -13,14 +17,14 @@ API_PARAMS = { "expanded-jsonld": { "@id": "expanded-jsonld", "aliases": ["expanded"], - "options": "boolean", + "options": boolean, "required": True, "default": False }, "with_parameters": { "aliases": ['withparameters', 'with-parameters'], - "options": "boolean", + "options": boolean, "default": False, "required": True }, @@ -36,7 +40,7 @@ API_PARAMS = { "description": "Show additional help to know more about the possible parameters", "aliases": ["h"], "required": True, - "options": "boolean", + "options": boolean, "default": False }, "emotionModel": { @@ -83,7 +87,7 @@ WEB_PARAMS = { "aliases": ["headers"], "required": True, "default": False, - "options": "boolean" + "options": boolean }, } @@ -132,7 +136,7 @@ NIF_PARAMS = { "aliases": ["u"], "required": False, "default": "RFC5147String", - "options": "RFC5147String" + "options": ["RFC5147String", ] } } @@ -159,7 +163,7 @@ def parse_params(indict, *specs): wrong_params[param] = spec[param] continue if "options" in options: - if options["options"] == "boolean": + if options["options"] == boolean: outdict[param] = outdict[param] in [None, True, 'true', '1'] elif outdict[param] not in options["options"]: wrong_params[param] = spec[param] @@ -171,8 +175,8 @@ def parse_params(indict, *specs): parameters=outdict, errors=wrong_params) raise message - if 'algorithm' in outdict and not isinstance(outdict['algorithm'], list): - outdict['algorithm'] = outdict['algorithm'].split(',') + if 'algorithm' in outdict and not isinstance(outdict['algorithm'], tuple): + outdict['algorithm'] = tuple(outdict['algorithm'].split(',')) return outdict diff --git a/senpy/blueprints.py b/senpy/blueprints.py index 26fd330..4389c84 100644 --- a/senpy/blueprints.py +++ b/senpy/blueprints.py @@ -18,7 +18,7 @@ Blueprints for Senpy """ from flask import (Blueprint, request, current_app, render_template, url_for, - jsonify) + jsonify, redirect) from .models import Error, Response, Help, Plugins, read_schema, dump_schema, Datasets from . import api from .version import __version__ @@ -27,6 +27,7 @@ from functools import wraps import logging import traceback import json +import base64 logger = logging.getLogger(__name__) @@ -34,6 +35,19 @@ api_blueprint = Blueprint("api", __name__) demo_blueprint = Blueprint("demo", __name__, template_folder='templates') ns_blueprint = Blueprint("ns", __name__) +_mimetypes_r = {'json-ld': ['application/ld+json'], + 'turtle': ['text/turtle'], + 'text': ['text/plain']} + +MIMETYPES = {} + +for k, vs in _mimetypes_r.items(): + for v in vs: + MIMETYPES[v] = k + +DEFAULT_MIMETYPE = 'application/ld+json' +DEFAULT_FORMAT = 'json-ld' + def get_params(req): if req.method == 'POST': @@ -45,6 +59,30 @@ def get_params(req): return indict +def encoded_url(url=None, base=None): + code = '' + if not url: + if request.method == 'GET': + url = request.full_path[1:] # Remove the first slash + else: + hash(frozenset(request.form.params().items())) + code = 'hash:{}'.format(hash) + + code = code or base64.urlsafe_b64encode(url.encode()).decode() + + if base: + return base + code + return url_for('api.decode', code=code, _external=True) + + +def decoded_url(code, base=None): + if code.startswith('hash:'): + raise Exception('Can not decode a URL for a POST request') + base = base or request.url_root + path = base64.urlsafe_b64decode(code.encode()).decode() + return base + path + + @demo_blueprint.route('/') def index(): ev = str(get_params(request).get('evaluation', False)) @@ -59,13 +97,22 @@ def index(): def context(entity="context"): context = Response._context context['@vocab'] = url_for('ns.index', _external=True) + context['endpoint'] = url_for('api.api_root', _external=True) return jsonify({"@context": context}) +@api_blueprint.route('/d/') +def decode(code): + try: + return redirect(decoded_url(code)) + except Exception: + return Error('invalid URL').flask() + + @ns_blueprint.route('/') # noqa: F811 def index(): - context = Response._context - context['@vocab'] = url_for('.ns', _external=True) + context = Response._context.copy() + context['endpoint'] = url_for('api.api_root', _external=True) return jsonify({"@context": context}) @@ -81,7 +128,7 @@ def basic_api(f): default_params = { 'inHeaders': False, 'expanded-jsonld': False, - 'outformat': 'json-ld', + 'outformat': None, 'with_parameters': True, } @@ -115,14 +162,21 @@ def basic_api(f): del response.parameters logger.info('Response: {}'.format(response)) + mime = request.accept_mimetypes\ + .best_match(MIMETYPES.keys(), + DEFAULT_MIMETYPE) + + mimeformat = MIMETYPES.get(mime, DEFAULT_FORMAT) + outformat = params['outformat'] or mimeformat + return response.flask( in_headers=params['inHeaders'], headers=headers, - prefix=url_for('.api_root', _external=True), + prefix=params.get('prefix', encoded_url()), context_uri=url_for('api.context', entity=type(response).__name__, _external=True), - outformat=params['outformat'], + outformat=outformat, expanded=params['expanded-jsonld']) return decorated_function diff --git a/senpy/extensions.py b/senpy/extensions.py index 8c97d0f..b6e8f6b 100644 --- a/senpy/extensions.py +++ b/senpy/extensions.py @@ -95,7 +95,7 @@ class Senpy(object): if plugin in self._plugins: return self._plugins[plugin] - results = self.plugins(id='plugins/{}'.format(name)) + results = self.plugins(id='endpoint:plugins/{}'.format(name)) if not results: return Error(message="Plugin not found", status=404) diff --git a/senpy/models.py b/senpy/models.py index 7bb8bbe..8006810 100644 --- a/senpy/models.py +++ b/senpy/models.py @@ -138,7 +138,7 @@ class BaseModel(with_metaclass(BaseMeta, CustomDict)): @property def id(self): if '@id' not in self: - self['@id'] = ':{}_{}'.format(type(self).__name__, time.time()) + self['@id'] = '_:{}_{}'.format(type(self).__name__, time.time()) return self['@id'] @id.setter @@ -146,7 +146,7 @@ class BaseModel(with_metaclass(BaseMeta, CustomDict)): self['@id'] = value def flask(self, - in_headers=True, + in_headers=False, headers=None, outformat='json-ld', **kwargs): @@ -176,20 +176,21 @@ class BaseModel(with_metaclass(BaseMeta, CustomDict)): def serialize(self, format='json-ld', with_mime=False, **kwargs): js = self.jsonld(**kwargs) + content = json.dumps(js, indent=2, sort_keys=True) if format == 'json-ld': - content = json.dumps(js, indent=2, sort_keys=True) mimetype = "application/json" elif format in ['turtle', ]: logger.debug(js) - content = json.dumps(js, indent=2, sort_keys=True) + base = kwargs.get('prefix') g = Graph().parse( data=content, format='json-ld', - base=kwargs.get('prefix'), + base=base, context=self._context) logger.debug( 'Parsing with prefix: {}'.format(kwargs.get('prefix'))) - content = g.serialize(format='turtle').decode('utf-8') + content = g.serialize(format='turtle', + base=base).decode('utf-8') mimetype = 'text/{}'.format(format) else: raise Error('Unknown outformat: {}'.format(format)) @@ -205,20 +206,22 @@ class BaseModel(with_metaclass(BaseMeta, CustomDict)): expanded=False): result = self.serializable() - if context_uri or with_context: - result['@context'] = context_uri or self._context + ctx = context_uri or self._context + + result['@context'] = ctx # result = jsonld.compact(result, - # self._context, + # ctx, # options={ # 'base': prefix, # 'expandContext': self._context, # 'senpy': prefix # }) + if expanded: result = jsonld.expand( result, options={'base': prefix, - 'expandContext': self._context}) + 'expandContext': ctx}) if not with_context: try: del result['@context'] diff --git a/senpy/plugins/__init__.py b/senpy/plugins/__init__.py index 314f2fb..d7e0ead 100644 --- a/senpy/plugins/__init__.py +++ b/senpy/plugins/__init__.py @@ -3,6 +3,7 @@ standard_library.install_aliases() from future.utils import with_metaclass +from functools import partial import os.path import os @@ -92,7 +93,7 @@ class Plugin(with_metaclass(PluginMeta, models.Plugin)): if info: self.update(info) self.validate() - self.id = 'plugins/{}_{}'.format(self['name'], self['version']) + self.id = 'endpoint:plugins/{}_{}'.format(self['name'], self['version']) self.is_activated = False self._lock = threading.Lock() self._directory = os.path.abspath(os.path.dirname(inspect.getfile(self.__class__))) @@ -530,7 +531,7 @@ def find_plugins(folders): yield fpath -def from_path(fpath, **kwargs): +def from_path(fpath, install_on_fail=False, **kwargs): logger.debug("Loading plugin from {}".format(fpath)) if fpath.endswith('.py'): # We asume root is the dir of the file, and module is the name of the file @@ -540,7 +541,7 @@ def from_path(fpath, **kwargs): yield instance else: info = parse_plugin_info(fpath) - yield from_info(info, **kwargs) + yield from_info(info, install_on_fail=install_on_fail, **kwargs) def from_folder(folders, loader=from_path, **kwargs): @@ -551,7 +552,7 @@ def from_folder(folders, loader=from_path, **kwargs): return plugins -def from_info(info, root=None, **kwargs): +def from_info(info, root=None, install_on_fail=True, **kwargs): if any(x not in info for x in ('module',)): raise ValueError('Plugin info is not valid: {}'.format(info)) module = info["module"] @@ -559,7 +560,12 @@ def from_info(info, root=None, **kwargs): if not root and '_path' in info: root = os.path.dirname(info['_path']) - return one_from_module(module, root=root, info=info, **kwargs) + fun = partial(one_from_module, module, root=root, info=info, **kwargs) + try: + return fun() + except (ImportError, LookupError): + install_deps(info) + return fun() def parse_plugin_info(fpath): @@ -606,17 +612,9 @@ def _instances_in_module(module): yield obj -def _from_module_name(module, root, info=None, install=True, **kwargs): - try: - module = load_module(module, root) - except (ImportError, LookupError): - if not install or not info: - raise - install_deps(info) - module = load_module(module, root) +def _from_module_name(module, root, info=None, **kwargs): + module = load_module(module, root) for plugin in _from_loaded_module(module=module, root=root, info=info, **kwargs): - if install: - install_deps(plugin) yield plugin diff --git a/tests/test_blueprints.py b/tests/test_blueprints.py index ec8b28f..68fbf87 100644 --- a/tests/test_blueprints.py +++ b/tests/test_blueprints.py @@ -139,7 +139,7 @@ class BlueprintsTest(TestCase): js = parse_resp(resp) logging.debug(js) assert "@id" in js - assert js["@id"] == "plugins/Dummy_0.1" + assert js["@id"] == "endpoint:plugins/Dummy_0.1" def test_default(self): """ Show only one plugin""" @@ -148,7 +148,7 @@ class BlueprintsTest(TestCase): js = parse_resp(resp) logging.debug(js) assert "@id" in js - assert js["@id"] == "plugins/Dummy_0.1" + assert js["@id"] == "endpoint:plugins/Dummy_0.1" def test_context(self): resp = self.client.get("/api/contexts/context.jsonld") diff --git a/tests/test_extensions.py b/tests/test_extensions.py index b2e8e3b..c2c3446 100644 --- a/tests/test_extensions.py +++ b/tests/test_extensions.py @@ -121,8 +121,8 @@ class ExtensionsTest(TestCase): # Leaf (defaultdict with __setattr__ and __getattr__. r1 = analyse(self.senpy, algorithm="Dummy", input="tupni", output="tuptuo") r2 = analyse(self.senpy, input="tupni", output="tuptuo") - assert r1.analysis[0] == "plugins/Dummy_0.1" - assert r2.analysis[0] == "plugins/Dummy_0.1" + assert r1.analysis[0] == "endpoint:plugins/Dummy_0.1" + assert r2.analysis[0] == "endpoint:plugins/Dummy_0.1" assert r1.entries[0]['nif:isString'] == 'input' def test_analyse_empty(self): @@ -156,8 +156,8 @@ class ExtensionsTest(TestCase): r2 = analyse(self.senpy, input="tupni", output="tuptuo") - assert r1.analysis[0] == "plugins/Dummy_0.1" - assert r2.analysis[0] == "plugins/Dummy_0.1" + assert r1.analysis[0] == "endpoint:plugins/Dummy_0.1" + assert r2.analysis[0] == "endpoint:plugins/Dummy_0.1" assert r1.entries[0]['nif:isString'] == 'input' def test_analyse_error(self):