from marshmallow.fields import * from marshmallow import missing import re from flask import url_for _tpl_pattern = re.compile(r"\s*<\s*(\S*)\s*>\s*") def _tpl(val): """Return value within ``< >`` if possible, else return ``None``.""" match = _tpl_pattern.match(val) if match: return match.groups()[0] return None def _get_value(obj, key, default=missing): """Slightly-modified version of marshmallow.utils.get_value. If a dot-delimited ``key`` is passed and any attribute in the path is `None`, return `None`. """ if "." in key: return _get_value_for_keys(obj, key.split("."), default) else: return _get_value_for_key(obj, key, default) def _get_value_for_keys(obj, keys, default): if len(keys) == 1: return _get_value_for_key(obj, keys[0], default) else: value = _get_value_for_key(obj, keys[0], default) # XXX This differs from the marshmallow implementation if value is None: return None return _get_value_for_keys(value, keys[1:], default) def _get_value_for_key(obj, key, default): if not hasattr(obj, "__getitem__"): return getattr(obj, key, default) try: return obj[key] except (KeyError, IndexError, TypeError, AttributeError): return getattr(obj, key, default) class URLFor(Field): """Field that outputs the URL for an endpoint. Acts identically to Flask's ``url_for`` function, except that arguments can be pulled from the object to be serialized. Usage: :: url = URLFor('author_get', id='') https_url = URLFor('author_get', id='', _scheme='https', _external=True) :param str endpoint: Flask endpoint name. :param kwargs: Same keyword arguments as Flask's url_for, except string arguments enclosed in `< >` will be interpreted as attributes to pull from the object. """ _CHECK_ATTRIBUTE = False def __init__(self, endpoint, **kwargs): self.endpoint = endpoint self.params = kwargs Field.__init__(self, **kwargs) def _serialize(self, value, key, obj): """Output the URL for the endpoint, given the kwargs passed to ``__init__``. """ param_values = {} for name, attr_tpl in self.params.items(): attr_name = _tpl(str(attr_tpl)) if attr_name: attribute_value = _get_value(obj, attr_name, default=missing) if attribute_value is None: return None if attribute_value is not missing: param_values[name] = attribute_value else: raise AttributeError( "{attr_name!r} is not a valid " "attribute of {obj!r}".format(attr_name=attr_name, obj=obj) ) else: param_values[name] = attr_tpl return url_for(self.endpoint, **param_values) UrlFor = URLFor class AbsoluteURLFor(URLFor): """Field that outputs the absolute URL for an endpoint.""" def __init__(self, endpoint, **kwargs): kwargs["_external"] = True URLFor.__init__(self, endpoint=endpoint, **kwargs) AbsoluteUrlFor = AbsoluteURLFor def _rapply(d, func, *args, **kwargs): """Apply a function to all values in a dictionary or list of dictionaries, recursively.""" if isinstance(d, (tuple, list)): return [_rapply(each, func, *args, **kwargs) for each in d] if isinstance(d, dict): return {key: _rapply(value, func, *args, **kwargs) for key, value in d.items()} else: return func(d, *args, **kwargs) def _url_val(val, key, obj, **kwargs): """Function applied by `HyperlinksField` to get the correct value in the schema. """ if isinstance(val, URLFor): return val.serialize(key, obj, **kwargs) else: return val class Hyperlinks(Field): """Field that outputs a dictionary of hyperlinks, given a dictionary schema with :class:`~flask_marshmallow.fields.URLFor` objects as values. Example: :: _links = Hyperlinks({ 'self': URLFor('author', id=''), 'collection': URLFor('author_list'), }) `URLFor` objects can be nested within the dictionary. :: _links = Hyperlinks({ 'self': { 'href': URLFor('book', id=''), 'title': 'book detail' } }) :param dict schema: A dict that maps names to :class:`~fields.URLFor` fields. """ _CHECK_ATTRIBUTE = False def __init__(self, schema, **kwargs): self.schema = schema Field.__init__(self, **kwargs) def _serialize(self, value, attr, obj): return _rapply(self.schema, _url_val, key=attr, obj=obj)