1
0
mirror of https://github.com/gsi-upm/senpy synced 2025-12-21 10:58:16 +00:00

Several fixes

* Refactored BaseModel for efficiency
* Added plugin metaclass to keep track of plugin types
* Moved plugins to examples dir (in a previous commit)
* Simplified validation in parse_params
* Added convenience methods to mock requests in tests
* Changed help schema to use `.valid_parameters` instead of `.parameters`,
which was used in results to show parameters provided by the user.
* Improved UI
    * Added basic parameters
    * Fixed bugs in parameter handling
    * Refactored and cleaned code
This commit is contained in:
J. Fernando Sánchez
2018-01-01 13:13:17 +01:00
parent f93eed2cf5
commit bfc588a915
35 changed files with 845 additions and 445 deletions

View File

@@ -6,7 +6,11 @@ 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 six import string_types
from future import standard_library
standard_library.install_aliases()
from future.utils import with_metaclass
from past.builtins import basestring
import time
import copy
@@ -15,6 +19,8 @@ import os
import jsonref
import jsonschema
import inspect
from collections import UserDict
from abc import ABCMeta
from flask import Response as FlaskResponse
from pyld import jsonld
@@ -62,7 +68,7 @@ class Context(dict):
return contexts
elif isinstance(context, dict):
return Context(context)
elif isinstance(context, string_types):
elif isinstance(context, basestring):
try:
with open(context) as f:
return Context(json.loads(f.read()))
@@ -75,9 +81,154 @@ class Context(dict):
base_context = Context.load(CONTEXT_PATH)
class SenpyMixin(object):
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.
'''
def __new__(mcs, name, bases, attrs, **kwargs):
defaults = {}
if 'schema' in attrs:
defaults = mcs.get_defaults(attrs['schema'])
for b in bases:
if hasattr(b, 'defaults'):
defaults.update(b.defaults)
info = mcs.attrs_to_dict(attrs)
defaults.update(info)
attrs['defaults'] = defaults
return super(BaseMeta, mcs).__new__(mcs, name, bases, attrs)
@staticmethod
def attrs_to_dict(attrs):
'''
Extract the attributes of the class.
This allows adding default values in the class definition.
e.g.:
'''
def is_attr(k, v):
return (not(inspect.isroutine(v) or
inspect.ismethod(v) or
inspect.ismodule(v) or
isinstance(v, property)) and
k[0] != '_' and
k != 'schema' and
k != 'data')
return {key: copy.deepcopy(value) for key, value in attrs.items() if is_attr(key, value)}
@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] = copy.deepcopy(v['default'])
return temp
class CustomDict(UserDict, object):
'''
A dictionary whose elements can also be accessed as attributes. Since some
characters are not valid in the dot-notation, the attribute names also
converted. e.g.:
> d = CustomDict()
> d.key = d['ns:name'] = 1
> d.key == d['key']
True
> d.ns__name == d['ns:name']
'''
defaults = []
def __init__(self, *args, **kwargs):
temp = copy.deepcopy(self.defaults)
for arg in args:
temp.update(copy.deepcopy(arg))
for k, v in kwargs.items():
temp[self._get_key(k)] = v
super(CustomDict, self).__init__(temp)
@staticmethod
def _get_key(key):
if key is 'id':
key = '@id'
key = key.replace("__", ":", 1)
return key
@staticmethod
def _internal_key(key):
return key[0] == '_' or key == 'data'
def __getattr__(self, key):
'''
__getattr__ only gets called when the attribute could not be found
in the __dict__. So we only need to look for the the element in the
dictionary, or raise an Exception.
'''
mkey = self._get_key(key)
if not self._internal_key(key) and mkey in self:
return self[mkey]
raise AttributeError(key)
def __setattr__(self, key, value):
# Work as usual for internal properties or already existing
# properties
if self._internal_key(key) or key in self.__dict__:
return super(CustomDict, self).__setattr__(key, value)
key = self._get_key(key)
return self.__setitem__(self._get_key(key), value)
def __delattr__(self, key):
if self._internal_key(key):
return object.__delattr__(self, key)
key = self._get_key(key)
self.__delitem__(self._get_key(key))
class BaseModel(with_metaclass(BaseMeta, CustomDict)):
'''
Entities of the base model are a special kind of dictionary that emulates
a JSON-LD object. The structure of the dictionary is checked via JSON-schema.
For convenience, the values can also be accessed as attributes
(a la Javascript). e.g.:
> myobject.key == myobject['key']
True
> myobject.ns__name == myobject['ns:name']
True
'''
schema = base_schema
_context = base_context["@context"]
def __init__(self, *args, **kwargs):
auto_id = kwargs.pop('_auto_id', True)
super(BaseModel, self).__init__(*args, **kwargs)
if '@id' not in self and auto_id:
self.id = ':{}_{}'.format(type(self).__name__, time.time())
if '@type' not in self:
logger.warn('Created an instance of an unknown model')
def flask(self,
in_headers=True,
headers=None,
@@ -146,7 +297,7 @@ class SenpyMixin(object):
else:
return item
return ser_or_down(self._plain_dict())
return ser_or_down(self.data)
def jsonld(self,
with_context=True,
@@ -188,150 +339,21 @@ class SenpyMixin(object):
return str(self.serialize())
class BaseModel(SenpyMixin, dict):
'''
Entities of the base model are a special kind of dictionary that emulates
a JSON-LD object. For convenience, the values can also be accessed as attributes
(a la Javascript). e.g.:
> myobject.key == myobject['key']
True
> myobject.ns__name == myobject['ns:name']
True
'''
schema = base_schema
def __init__(self, *args, **kwargs):
self.attrs_to_dict()
if 'id' in kwargs:
self.id = kwargs.pop('id')
elif kwargs.pop('_auto_id', True):
self.id = '_:{}_{}'.format(type(self).__name__, time.time())
temp = self.get_defaults()
temp.update(dict(*args))
for k, v in kwargs.items():
temp[self._get_key(k)] = v
super(BaseModel, self).__init__(temp)
if '@type' not in self:
logger.warn('Created an instance of an unknown model')
def get_defaults(self):
temp = {}
for obj in [
self.schema,
] + self.schema.get('allOf', []):
for k, v in obj.get('properties', {}).items():
if 'default' in v and k not in temp:
temp[k] = copy.deepcopy(v['default'])
return temp
def attrs_to_dict(self):
'''
Copy the attributes of the class to the instance.
This allows adding default values in the class definition.
e.g.:
class MyPlugin(Plugin):
version=0.3
description='A dull plugin'
'''
def is_attr(x):
return not(inspect.isroutine(x) or inspect.ismethod(x) or isinstance(x, property))
for key, value in inspect.getmembers(self.__class__, is_attr):
if key[0] != '_' and key != 'schema':
self[key] = value
def _get_key(self, key):
if key is 'id':
key = '@id'
key = key.replace("__", ":", 1)
return key
def __delitem__(self, key):
key = self._get_key(key)
dict.__delitem__(self, key)
def _internal_key(self, key):
return key[0] == '_' or key in self.__dict__
def _plain_dict(self):
d = {k: v for (k, v) in self.items() if k[0] != "_"}
return d
def __getattr__(self, key):
'''
__getattr__ only gets called when the attribute could not
be found in the __dict__. So we only need to look for the
the element in the dictionary, or raise an Exception.
'''
if self._internal_key(key):
raise AttributeError(key)
return self.__getitem__(self._get_key(key))
def __setattr__(self, key, value):
if self._internal_key(key):
return super(BaseModel, self).__setattr__(key, value)
key = self._get_key(key)
return self.__setitem__(self._get_key(key), value)
def __delattr__(self, key):
if self._internal_key(key):
return object.__delattr__(self, key)
key = self._get_key(key)
self.__delitem__(self._get_key(key))
_subtypes = {}
def register(rsubclass, rtype=None):
_subtypes[rtype or rsubclass.__name__] = rsubclass
_subtypes = {}
def from_dict(indict, cls=None):
if not cls:
target = indict.get('@type', None)
try:
if target and target in _subtypes:
cls = _subtypes[target]
else:
cls = BaseModel
except Exception:
cls = BaseModel
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):
for ix, v2 in enumerate(v):
if isinstance(v2, dict):
v[ix] = from_dict(v2)
outdict[k] = v
return cls(**outdict)
def from_string(string, **kwargs):
return from_dict(json.loads(string), **kwargs)
def from_json(injson):
indict = json.loads(injson)
return from_dict(indict)
def from_schema(name, schema=None, schema_file=None, base_classes=None):
base_classes = base_classes or []
base_classes.append(BaseModel)
schema_file = schema_file or '{}.json'.format(name)
class_name = '{}{}'.format(name[0].upper(), name[1:])
if '/' not in 'schema_file':
schema_file = os.path.join(os.path.dirname(os.path.realpath(__file__)),
thisdir = os.path.dirname(os.path.realpath(__file__))
schema_file = os.path.join(thisdir,
'schemas',
schema_file)
@@ -354,6 +376,40 @@ def from_schema(name, schema=None, schema_file=None, base_classes=None):
return newclass
def from_dict(indict, cls=None):
if not cls:
target = indict.get('@type', None)
try:
if target and target in _subtypes:
cls = _subtypes[target]
else:
cls = BaseModel
except Exception:
cls = BaseModel
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.deepcopy(v)
return cls(**outdict)
def from_string(string, **kwargs):
return from_dict(json.loads(string), **kwargs)
def from_json(injson):
indict = json.loads(injson)
return from_dict(indict)
def _add_from_schema(*args, **kwargs):
generatedClass = from_schema(*args, **kwargs)
globals()[generatedClass.__name__] = generatedClass
@@ -384,40 +440,14 @@ for i in [
_ErrorModel = from_schema('error')
class Error(SenpyMixin, Exception):
class Error(_ErrorModel, Exception):
def __init__(self, message, *args, **kwargs):
super(Error, self).__init__(self, message, message)
self._error = _ErrorModel(message=message, *args, **kwargs)
Exception.__init__(self, message)
super(Error, self).__init__(*args, **kwargs)
self.message = message
def validate(self, obj=None):
self._error.validate()
def __getitem__(self, key):
return self._error[key]
def __setitem__(self, key, value):
self._error[key] = value
def __delitem__(self, key):
del self._error[key]
def __getattr__(self, key):
if key != '_error' and hasattr(self._error, key):
return getattr(self._error, key)
raise AttributeError(key)
def __setattr__(self, key, value):
if key != '_error':
return setattr(self._error, key, value)
else:
super(Error, self).__setattr__(key, value)
def __delattr__(self, key):
delattr(self._error, key)
def __str__(self):
return str(self.to_JSON(with_context=False))
def __hash__(self):
return Exception.__hash__(self)
register(Error, 'error')