"""
restfulchemy.fields
~~~~~~~~~~~~~~~~~~~
Marshmallow fields used in model resource schemas.
:copyright: (c) 2016 by Nicholas Repole and contributors.
See AUTHORS for more details.
:license: MIT - See LICENSE for more details.
"""
import copy
from marshmallow.compat import basestring
from marshmallow.fields import Field, Nested, missing_
from marshmallow.utils import is_collection, get_value
from marshmallow.validate import ValidationError
from marshmallow_sqlalchemy.fields import Related, ensure_list
from restfulchemy import resource_class_registry
from sqlalchemy.inspection import inspect
[docs]class EmbeddedField(Field):
"""Contains a default field and an embeddable field.
Member or method access will use the default field provided if the
current state isn't embedded, or the embedded field provided if the
current state is embedded.
A common use case is to have a default field be a
:class:`RelationshipUrl`, and the embedded field be a
:class:`NestedRelated` field.
"""
def __init__(self, default_field, embedded_field, embedded=False, *args,
**kwargs):
"""Construct an embeddable field.
:param default_field: Default field to use when not embedded.
:type default_field: :class:`~marshmallow.fields.Field`
:param embedded_field: Field to use when embedded.
:type embedded_field: :class:`~marshmallow.fields.Field`
"""
self._embedded = embedded
self._default_field = default_field
self._embedded_field = embedded_field
super(EmbeddedField, self).__init__(*args, **kwargs)
def _rebind_field(self):
"""After embedding or unembedding, bind the new active field."""
inactive_field = self.embedded_field
if inactive_field == self.active_field:
inactive_field = self.default_field
schema = inactive_field.parent
if schema is not None:
if schema.load_only:
self.active_field.load_only = True
if schema.dump_only:
self.active_field.dump_only = True
schema.on_bind_field(inactive_field.name, self.active_field)
self._add_to_schema(inactive_field.name, schema)
[docs] def embed(self):
"""Embed the non default field."""
if not self.embedded:
self._embedded = True
self._rebind_field()
[docs] def unembed(self):
"""Restore the default field."""
if self.embedded:
self._embedded = False
self._rebind_field()
@property
def embedded(self):
"""Return ``True`` if the embedded field is currently active."""
return self._embedded
@property
def embedded_field(self):
"""Get the embedded field, regardless of if it is embedded."""
return self._embedded_field
@embedded_field.setter
def embedded_field(self, value):
"""Replace the embedded field.
:param value: A new field to use for when the current state is
embedded.
:type value: :class:`~marshmallow.fields.Field`
"""
self._embedded_field = value
@property
def default_field(self):
"""Return the default, non embedded field."""
return self._default_field
@default_field.setter
def default_field(self, value):
"""Replace the default field.
:param value: A new field to use for when the current state is
not embedded.
:type value: :class:`~marshmallow.fields.Field`
"""
self._default_field = value
@property
def active_field(self):
"""Return the embedded field if embedded, else the default."""
if self.embedded:
return self._embedded_field
else:
return self._default_field
@property
def name(self):
"""Get the ``active_field.name`` property."""
return self.active_field.name
@name.setter
def name(self, value):
"""Set the ``active_field.name`` property.
:param str value: The new ``name`` value to use for the field.
"""
setattr(self.active_field, "name", value)
@property
def default(self):
"""Get the ``active_field.default`` property."""
return self.active_field.default
@default.setter
def default(self, value):
"""Set the ``active_field.default`` property.
:param str value: The new ``default`` value to use for the
field.
"""
setattr(self.active_field, "default", value)
@property
def attribute(self):
"""Get the ``active_field.attribute`` property."""
return self.active_field.attribute
@attribute.setter
def attribute(self, value):
"""Set the ``active_field.attribute`` property.
:param str value: The new ``attribute`` value to use for the
field.
"""
setattr(self.active_field, "attribute", value)
@property
def load_from(self):
"""Get the ``active_field.load_from`` property."""
return self.active_field.load_from
@load_from.setter
def load_from(self, value):
"""Set the ``active_field.load_from`` property.
:param str value: The new ``load_from`` value to use for the
field.
"""
setattr(self.active_field, "load_from", value)
@property
def dump_to(self):
"""Get the ``active_field.dump_to`` property."""
return self.active_field.dump_to
@dump_to.setter
def dump_to(self, value):
"""Set the ``active_field.dump_to`` property.
:param str value: The new ``dump_to`` value to use for the
field.
"""
setattr(self.active_field, "dump_to", value)
@property
def validate(self):
"""Get the ``active_field.validate`` property."""
return self.active_field.validate
@validate.setter
def validate(self, value):
"""Set the ``active_field.validate`` property.
:param str value: The new ``validate`` value to use for the
field.
"""
setattr(self.active_field, "validate", value)
@property
def validators(self):
"""Get the ``active_field.validators`` property."""
return self.active_field.validators
@validators.setter
def validators(self, value):
"""Set the ``active_field.validators`` property.
:param str value: The new ``validators`` value to use for the
field.
"""
setattr(self.active_field, "validators", value)
@property
def required(self):
"""Get the ``active_field.required`` property."""
return self.active_field.required
@required.setter
def required(self, value):
"""Set the ``active_field.required`` property.
:param str value: The new ``required`` value to use for the
field.
"""
setattr(self.active_field, "required", value)
@property
def allow_none(self):
"""Get the ``active_field.allow_none`` property."""
return self.active_field.allow_none
@allow_none.setter
def allow_none(self, value):
"""Set the ``active_field.allow_none`` property.
:param bool value: The new ``allow_none`` value to use for the
field.
"""
setattr(self.active_field, "allow_none", value)
@property
def load_only(self):
"""Get the ``active_field.load_only`` property."""
return self.active_field.load_only
@load_only.setter
def load_only(self, value):
"""Set the ``active_field.load_only`` property.
:param bool value: The new ``load_only`` value to use for the
field.
"""
setattr(self.active_field, "load_only", value)
@property
def dump_only(self):
"""Get the ``active_field.dump_only`` property."""
return self.active_field.dump_only
@dump_only.setter
def dump_only(self, value):
"""Set the ``active_field.dump_only`` property.
:param bool value: The new ``dump_only`` value to use for the
field.
"""
setattr(self.active_field, "dump_only", value)
@property
def missing(self):
"""Get the ``active_field.missing`` property."""
return self.active_field.missing
@missing.setter
def missing(self, value):
"""Set the ``active_field.missing`` property.
:param value: The new ``missing`` value to use for the field.
"""
setattr(self.active_field, "missing", value)
@property
def metadata(self):
"""Get the ``active_field.metadata`` property."""
return self.active_field.metadata
@metadata.setter
def metadata(self, value):
"""Set the ``active_field.metadata`` property.
:param dict value: The new ``metadata`` value to use for the
field.
"""
setattr(self.active_field, "metadata", value)
@property
def parent(self):
"""Get the ``active_field.parent`` property."""
return self.active_field.parent
@parent.setter
def parent(self, value):
"""Set the ``active_field.parent`` property.
:param value: The new ``parent`` value to use for the field.
"""
setattr(self.active_field, "parent", value)
@property
def error_messages(self):
"""Get the ``active_field.error_messages`` property."""
return self.active_field.error_messages
@error_messages.setter
def error_messages(self, value):
"""Set the ``active_field.error_messages`` property.
:param dict value: The new ``error_messages`` value to use for
the field.
"""
setattr(self.active_field, "error_messages", value)
[docs] def get_value(self, attr, obj, accessor=None, default=missing_):
"""Call ``active_field.get_value``.
:param str attr: Name of the attribute containing the desired
value.
:param obj: A generic object that contains the desired value.
:param accessor: Function used to pull values from ``obj``.
Defaults to :func:`~marshmallow.utils.get_value`.
:type accessor: callable or None
:param default: The default value to return if the attr can not
be accessed in the obj.
:return: The value of the object's attr.
"""
return self.active_field.get_value(attr, obj, accessor, default)
def _validate(self, value):
"""Call ``active_field._validate``.
:param value: Value to be validated for this field.
:raise ValidationError: If validation does not succeed.
"""
return self.active_field._validate(value)
[docs] def fail(self, key, **kwargs):
"""Call ``active_field.fail``.
:param str key: The type of failure, used to choose which error
message to include in the raised exception.
:param kwargs: Key word args used for string replacement fields
in the error message.
:raise ValidationError: In all cases, raises an exception.
"""
return self.active_field.fail(key, **kwargs)
def _validate_missing(self, value):
"""Call ``active_field._validate_missing``.
:param value: The value provided for this field.
:raise ValidationError: If ``value`` is considered missing.
"""
return self.active_field._validate_missing(value)
[docs] def serialize(self, attr, obj, accessor=None):
"""Call ``active_field.serialize``.
:param str attr: The attribute or key to get from the object.
:param str obj: The object to pull the key from.
:param accessor: Function used to pull values from ``obj``.
Defaults to :func:`~marshmallow.utils.get_value`.
:type accessor: callable or None
:raise ValidationError: In case of formatting problem.
:return: The serialized value.
"""
return self.active_field.serialize(attr, obj, accessor)
[docs] def deserialize(self, value, attr=None, data=None):
"""Call ``active_field.deserialize``.
:param value: The value to be deserialized.
:param str attr: The attribute/key in ``data`` to be
deserialized.
:param dict data: The raw input data passed to ``Schema.load``.
:raise ValidationError: If an invalid value is passed or if a
required value is missing.
:return: The deserialized value.
"""
return self.active_field.deserialize(value, attr, data)
def _add_to_schema(self, field_name, schema):
"""Call ``active_field._add_to_schema``.
:param str field_name: Field name set in schema.
:param schema: This field's parent schema.
:type schema: :class:`~marshmallow.schema.Schema`
"""
return self.active_field._add_to_schema(field_name, schema)
def _serialize(self, value, attr, obj):
"""Call ``active_field._serialize``.
:param str attr: The attribute or key to get from the object.
:param str obj: The object to pull the key from.
:raise ValidationError: In case of formatting problem.
:return: The serialized value.
"""
return self.active_field._serialize(value, attr, obj)
def _deserialize(self, value, attr, data):
"""Call ``active_field._deserialize``.
:param value: The value to be deserialized.
:param str attr: The attribute/key in ``data`` to be
deserialized.
:param dict data: The raw input data passed to ``Schema.load``.
:raise ValidationError: If an invalid value is passed or if a
required value is missing.
:return: The deserialized value.
"""
return self.active_field._deserialize(value, attr, data)
@property
def context(self):
"""Use the active_field implementation of context."""
return self.active_field.context
@property
def root(self):
"""Use the active_field implementation of root."""
return self.active_field.root
def __deepcopy__(self, memo):
"""Takes care of deepcopying the embedded and default fields.
:param dict memo: Standard deepcopy memo dict containing id to
object mapping.
"""
ret = super(EmbeddedField, self).__deepcopy__(memo)
ret._default_field = copy.deepcopy(self._default_field)
ret._embedded_field = copy.deepcopy(self._embedded_field)
return ret
[docs]class RelationshipUrl(Field):
"""Text field, displays the sub resource url of a relationship."""
def __init__(self, default=missing_, attribute=None, load_from=None,
dump_to=None, error=None, validate=None, required=False,
allow_none=None, load_only=False, dump_only=False,
missing=missing_, error_messages=None, resource=None,
**metadata):
"""Initialize a relationship url field.
:param default: If set, this value will be used during
serialization if the input value is missing. If not set, the
field will be excluded from the serialized output if the
input value is missing. May be a value or a callable.
:param str attribute: The name of the attribute to get the value
from. If `None`, assumes the attribute has the same name as
the field.
:type attribute: str or None
:param load_from: Additional key to look for when deserializing.
Will only be checked if the field's name is not found on the
input dictionary. If checked, it will return this parameter
on error.
:type load_from: str or None
:param dump_to: Field name to use as a key when serializing.
:type dump_to: str or None
:param validate: Validator or collection of validators that are
called during deserialization. Validator takes a field's
input value as its only parameter and returns a boolean.
If it returns `False`, an :exc:`ValidationError` is raised.
:type validate: callable or None
:param bool required: Raise a
:exc:`~marshmallow.exceptions.ValidationError` if the field
value is not supplied during deserialization.
:param bool allow_none: Set this to `True` if `None` should be
considered a valid value during validation/deserialization.
If ``missing=None`` and ``allow_none`` is unset, will
default to `True`. Otherwise, the default is `False`.
:param bool load_only: If `True` skip this field during
serialization, otherwise its value will be present in the
serialized data.
:param bool dump_only: If `True` skip this field during
deserialization, otherwise its value will be present in the
deserialized object. In the context of an HTTP API, this
effectively marks the field as "read-only".
:param missing: Default deserialization value for the field if
the field is not found in the input data. May be a value
or a callable.
:param error_messages: Overrides for field error messages.
:type error_messages: dict or None
:param metadata: Extra arguments to be stored as metadata.
"""
self._resource_arg = resource
super(RelationshipUrl, self).__init__(
default=default,
attribute=attribute,
load_from=load_from,
dump_to=dump_to,
error=error,
validate=validate,
required=required,
allow_none=allow_none,
load_only=load_only,
dump_only=dump_only,
missing=missing,
error_messages=error_messages,
**metadata)
@property
def resource_class(self):
"""Get the nested resource class."""
# Ensure that only parameter is a tuple
if isinstance(self._resource_arg, basestring):
return resource_class_registry.get_class(self._resource_arg)
else:
return self._resource_arg
[docs] def serialize(self, attr, obj, accessor=None):
"""Serialize a relationship sub-resource url.
:param str attr: The attribute name of this field. Unused.
:param str obj: The object to pull any needed info from.
:param accessor: Function used to pull values from ``obj``.
Unused.
:type accessor: callable or None
:return: The serialized relationship url value.
:rtype: str
"""
url = ""
if self.parent and "self" in self.parent.fields:
url += self.parent.fields["self"].serialize("self", obj)
relationship_name = self.dump_to or self.name
url += "/" + relationship_name
return url
[docs]class APIUrl(Field):
"""Text field, displays the url of the resource it's attached to."""
[docs] def serialize(self, attr, obj, accessor=None):
"""Serialize an API url.
:param str attr: The attribute name of this field. Unused.
:param str obj: The object to pull any needed info from.
:param accessor: Function used to pull values from ``obj``.
Defaults to :func:`~marshmallow.utils.get_value`.
:type accessor: callable or None
:raise ValidationError: In case of formatting problem.
:return: The serialized API url value.
"""
# TODO - Better safety checking
accessor_func = accessor or get_value
id_keys = self.parent.id_keys
result = "/" + self.parent.api_endpoint_base
for column in id_keys:
if hasattr(obj, column):
val = accessor_func(column, obj, missing_)
result += "/" + str(val)
return result