Source code for kim.field

# kim/field.py
# Copyright (C) 2014-2015 the Kim authors and contributors
# <see AUTHORS file>
#
# This module is part of Kim and is released under
# the MIT License: http://www.opensource.org/licenses/mit-license.php

from collections import defaultdict

from .exception import FieldError, FieldInvalid, FieldOptsError
from .utils import set_creation_order
from .pipelines import (
    StringMarshalPipeline, StringSerializePipeline,
    StaticSerializePipeline,
    IntegerMarshalPipeline, IntegerSerializePipeline,
    NestedMarshalPipeline, NestedSerializePipeline,
    CollectionMarshalPipeline, CollectionSerializePipeline,
    BooleanMarshalPipeline, BooleanSerializePipeline,
    DateTimeSerializePipeline, DateTimeMarshalPipeline,
    DateMarshalPipeline, DateSerializePipeline,
    DecimalSerializePipeline, DecimalMarshalPipeline,
    FloatSerializePipeline, FloatMarshalPipeline)
from .pipelines.base import run_pipeline, Session
from .pipelines.marshaling import MarshalPipeline
from .pipelines.serialization import SerializePipeline

DEFAULT_ERROR_MSGS = {
    'required': 'This is a required field',
    'type_error': 'Invalid type',
    'not_found': '{name} not found',
    'none_not_allowed': 'This field cannot be null',
    'invalid_choice': 'invalid choice',
    'duplicates': 'duplicates found',
    'out_of_bounds': 'value out of allowed range',
}


[docs]class FieldOpts(object): """FieldOpts are used to provide configuration options to :class:`.Field`. They are designed to allow users to easily provide custom configuration options to :class:`.Field` classes. Custom :class:`.FieldOpts` classes are set on :class:`.Field` using the ``opts_class`` property. .. code-block:: python class MyFieldOpts(FieldOpts): def __init__(self, **opts): self.some_property = opts.get('some_property', None) super(MyFieldOpts, self).__init__(**opts) .. seealso:: :class:`.Field` """ extra_error_msgs = {} def __init__(self, **opts): """ Construct a new instance of :class:`FieldOpts` and set config options :param name: Specify the name of the field for data output :param required: This field must be present when marshaling :param attribute_name: Specify internal name for this field, set on mapper.fields dict :param source: Specify the name of the attribute on the object to use when getting/setting data. May be ``__self__`` to use entire mapper object as data :param default: Specify a default value for this field to apply when serializing or marshaling :param allow_none: This option only takes affect if required=False. If allow_none=False and required=False, then Kim will accept either the field being missing completely from the data, or the field being passed with a non-None value. That is, either ``{}`` or ``{'field': 'value'}`` but never ``{'field': None}``. Default True. :param read_only: Specify if this field should be ignored when marshaling :param error_msgs: A dict of error_type: error messages. :param null_default: Specify the default type to return when a field is null IE None or {} or '' :param choices: Specify a list of valid values :param extra_serialize_pipes: dict of lists containing extra Pipe functions to be run at the end of each stage when serializing. eg ``{'output': [my_pipe, my_other_pipe]}``` :param extra_marshal_pipes: dict of lists containing extra Pipe functions to be run at the end of each stage when marshaling. eg ``{'validate': [my_pipe, my_other_pipe]}``` :raises: :class:`.FieldOptsError` :returns: None """ self._opts = opts.copy() # internal attrs self._is_wrapped = opts.pop('_is_wrapped', False) # set attribute_name, name and source options. name = opts.pop('name', None) attribute_name = opts.pop('attribute_name', None) source = opts.pop('source', None) self.name, self.attribute_name, self.source = None, None, None self.set_name(name=name, attribute_name=attribute_name, source=source) self.error_msgs = DEFAULT_ERROR_MSGS.copy() self.error_msgs.update(opts.pop('error_msgs', self.extra_error_msgs)) self.required = opts.pop('required', True) self.default = opts.pop('default', None) self.null_default = opts.pop('null_default', None) self.allow_none = opts.pop('allow_none', True) self.read_only = opts.pop('read_only', False) self.choices = opts.pop('choices', None) self.extra_marshal_pipes = \ opts.pop('extra_marshal_pipes', defaultdict(list)) self.extra_serialize_pipes = \ opts.pop('extra_serialize_pipes', defaultdict(list)) self.validate()
[docs] def validate(self): """Allow users to perform checks for required config options. Concrete classes should raise :class:`.FieldError` when invalid configuration is encountered. A slightly contrived example is requiring all fields to be ``read_only=True`` Usage:: from kim.field import FieldOpts class MyOpts(FieldOpts): def validate(self): if self.read_only is True: raise FieldOptsError('Field cannot be read only') :raises: `.FieldOptsError` :returns: None """ pass
[docs] def set_name(self, name=None, attribute_name=None, source=None): """Programmatically set the name properties for a field. Names cascade from each other unless they are explicitly overridden. Example 1: class MyMapper(Mapper): foo = field.String() attribute_name = foo name = foo source = foo Example 2: class MyMapper(Mapper): foo = field.String(name='bar', source='baz') attribute_name = foo name = bar source = baz :param name: value of name property :param attribute_name: value of attribute_name property :param source: value of source property :returns: None """ self.attribute_name = self.attribute_name or attribute_name self.name = self.name or name or self.attribute_name self.source = self.source or source or self.name
[docs] def get_name(self): """Return the name property set by :meth:`set_name` :rtype: str :returns: the name of the field to be used in input/output """ return self.name
[docs]class Field(object): """Field, as it's name suggests, represents a single key or 'field' inside of your mappings. Much like columns in a database or a csv, they provide a way to represent different data types when pushing data into and out of your Mappers. A core concept of Kims architecture is that of Pipelines. Every Field makes use of both an Input and Output pipeline which affords users a great level of flexibility when it comes to handling data. Kim provides a collection of default Field implementations, for more complex cases extending Field to create new field types couldn't be easier. Usage:: from kim import Mapper from kim import field class UserMapper(Mapper): id = field.Integer(required=True, read_only=True) name = field.String(required=True) """ #: The :class:`FieldOpts` field config class to use for the Field. opts_class = FieldOpts #: The Fields marshaling pipeline marshal_pipeline = MarshalPipeline #: The Fields serialization pipeline serialize_pipeline = SerializePipeline def __init__(self, *args, **field_opts): """Constructs a new instance of Field. Each Field accepts a set of kwargs that will be passed directly to the fields defined :class:`FieldOpts`. :param args: list of arguments passed to the field :param kwargs: keyword arguments typically passed to the FieldOpts class attached to this Field. :raises: :class:`FieldOptsError` :returns: None .. seealso:: :class:`FieldOpts` """ try: self.opts = self.opts_class(*args, **field_opts) except FieldOptsError as e: msg = '{0} field has invalid options: {1}' \ .format(self.__class__.__name__, e.message) raise FieldError(msg) set_creation_order(self) self.marshal_pipes = self.marshal_pipeline.get_pipeline( **self.opts.extra_marshal_pipes ) self.serialize_pipes = self.serialize_pipeline.get_pipeline( **self.opts.extra_serialize_pipes )
[docs] def get_error(self, error_type): """Return the error message for ``error_type`` from the error messages defined on the fields opts class. :param error_type: the key of the error found in self.error_msgs :returns: Error message :rtype: string """ parse_opts = { 'name': self.name } return self.opts.error_msgs[error_type].format(**parse_opts)
[docs] def invalid(self, error_type): """Raise an Exception using the provided error_type for the error message. This method is typically used by pipes to allow :class:`Field` to control how its errors are handled. Usage:: @pipe() def validate_name(session): if session.data and session.data != 'Mike Waites': raise session.fied.invalid('not_mike') :param error_type: The key of the error being raised. :raises: :class:`FieldInvalid` .. seealso:: :class:`FieldOpts` for an explanation on defining error messags """ raise FieldInvalid(self.get_error(error_type), field=self)
@property def name(self): """Proxy access to the :class:`FieldOpts` defined for this field. :rtype: str :returns: The value of get_name from FieldOpts :raises: :class:`FieldError` .. seealso:: :meth:`kim.field.FieldOpts.get_name` """ field_name = self.opts.get_name() if not field_name: cn = self.__class__.__name__ raise FieldError('{0} requires {0}.name or ' '{0}.attribute_name. Please provide a `name` ' 'or `attribute_name` param to {0}'.format(cn)) return field_name @name.setter def name(self, name): """Proxy setting the name property via :py:meth:`kim.field.FieldOpts.set_name` :param name: the value to set against FieldOpts name property :returns: None .. seealso:: :meth:`kim.field.FieldOpts.set_name` """ self.opts.set_name(name)
[docs] def marshal(self, mapper_session, **opts): """Run the marshal :class:`Pipeline` for this field for the given ``data`` and update the output for this field inside of the mapper_session. :param mapper_session: The Mappers marshaling session this field is being run inside of. :opts: kwargs passed to the marshal pipelines run method. :returns: None .. seealso:: :meth:`kim.mapper.Mapper.marshal` """ parent = opts.get('parent_session', None) session = Session( self, mapper_session.data, mapper_session.output, mapper_session=mapper_session, parent=parent) run_pipeline(self.marshal_pipes, session, self, **opts)
[docs] def serialize(self, mapper_session, **opts): """Run the serialize :class:`Pipeline` for this field for the given `data` and update `output` in for this field inside of the mapper_session. :param mapper_session: The Mappers marshaling session this field is being run inside of. :opts: kwargs passed to the marshal pipelines run method. :returns: None .. seealso:: :meth:`kim.mapper.Mapper.serialize` """ parent = opts.get('parent_session', None) session = Session( self, mapper_session.data, mapper_session.output, mapper_session=mapper_session, parent=parent) run_pipeline(self.serialize_pipes, session, self, **opts)
class StringFieldOpts(FieldOpts): """Custom FieldOpts class that provides additional config options for :class:`String`. """ def __init__(self, **kwargs): """ Construct a new instance of :class:`StringFieldOpts` and set config options :param max: Specify the maximum permitted length :param min: Specify the minimum permitted length :param blank: If False, raise error if empty string passed. Default True :raises: :class:`FieldOptsError` :returns: None """ self.max = kwargs.pop('max', None) self.min = kwargs.pop('min', None) self.blank = kwargs.pop('blank', True) super(StringFieldOpts, self).__init__(**kwargs)
[docs]class String(Field): """:class:`String` represents a value that must be valid when passed to str() Usage:: from kim import Mapper from kim import field class UserMapper(Mapper): __type__ = User name = field.String(required=True) """ opts_class = StringFieldOpts marshal_pipeline = StringMarshalPipeline serialize_pipeline = StringSerializePipeline
[docs]class IntegerFieldOpts(FieldOpts): """Custom FieldOpts class that provides additional config options for :class:`Integer`. """ def __init__(self, **kwargs): """ Construct a new instance of :class:`IntegerFieldOpts` and set config options :param max: Specify the maximum permitted value :param min: Specify the minimum permitted value :raises: :class:`FieldOptsError` :returns: None """ self.max = kwargs.pop('max', None) self.min = kwargs.pop('min', None) super(IntegerFieldOpts, self).__init__(**kwargs)
[docs]class Integer(Field): """:class:`Integer` represents a value that must be valid when passed to int() Usage:: from kim import Mapper from kim import field class UserMapper(Mapper): __type__ = User id = field.Integer(required=True, min=1, max=10) """ opts_class = IntegerFieldOpts marshal_pipeline = IntegerMarshalPipeline serialize_pipeline = IntegerSerializePipeline
class FloatFieldOpts(FieldOpts): """Custom FieldOpts class that provides additional config options for :class:`Float`. """ def __init__(self, **kwargs): """ Construct a new instance of :class:`FloatFieldOpts` and set config options :param precision: Specify the precision of the float :param max: Specify the maximum permitted value :param min: Specify the minimum permitted value :raises: :class:`FieldOptsError` :returns: None """ self.precision = kwargs.pop('precision', 5) self.max = kwargs.pop('max', None) self.min = kwargs.pop('min', None) super(FloatFieldOpts, self).__init__(**kwargs) class Float(Field): """:class:`Float` represents a value that must be valid Float type. Usage:: from kim import Mapper from kim import field class UserMapper(Mapper): __type__ = User score = field.Float(precision=4) """ opts_class = FloatFieldOpts marshal_pipeline = FloatMarshalPipeline serialize_pipeline = FloatSerializePipeline
[docs]class Decimal(Float): """:class:`Decimal` represents a value that must be valid when passed to decimal.Decimal() Usage:: from kim import Mapper from kim import field class UserMapper(Mapper): __type__ = User score = field.Decimal(precision=4, min=0, max=1.5) """ opts_class = FloatFieldOpts marshal_pipeline = DecimalMarshalPipeline serialize_pipeline = DecimalSerializePipeline
[docs]class BooleanFieldOpts(FieldOpts): """Custom FieldOpts class that provides additional config options for :class:`Boolean`. """ def __init__(self, **kwargs): """ Construct a new instance of :class:`BooleanFieldOpts` and set config options :param true_boolean_values: Specify an array of values that will validate as being 'true' when the field is marshaled. :param false_boolean_values: Specify an array of values that will validate as being 'false' when the field is marshaled. :raises: :class:`FieldOptsError` :returns: None """ self.true_boolean_values = \ kwargs.pop('true_boolean_values', [True, 'true', '1', 1, 'True']) self.false_boolean_values = \ kwargs.pop('false_boolean_values', [False, 'false', '0', 0, 'False']) super(BooleanFieldOpts, self).__init__(**kwargs) self.choices = set(self.true_boolean_values + self.false_boolean_values)
[docs]class Boolean(Field): """:class:`Boolean` represents a value that must be valid boolean type. Usage:: from kim import Mapper from kim import field class UserMapper(Mapper): __type__ = User active = field.Boolean( required=True, true_boolean_values=[True, 'true', 1], false_boolean_values=[False, 'false', 0]) """ opts_class = BooleanFieldOpts marshal_pipeline = BooleanMarshalPipeline serialize_pipeline = BooleanSerializePipeline
[docs]class NestedFieldOpts(FieldOpts): """Custom FieldOpts class that provides additional config options for :class:`Nested`. """ def __init__(self, mapper_or_mapper_name, **kwargs): """Construct a new instance of :class:`NestedFieldOpts` :param mapper_or_mapper_name: a required instance of a :class:`Mapper` or a valid mapper name :param role: specify the name of a role to use on the Nested mapper :param collection_class: provide a custom type to be used when mapping many nested objects :param getter: provide a function taking a pipeline session which returns the object to be set on this field, or None if it can't find one. This is useful where your API accepts simply `{'id': 2}` but you want a full object to be set :param allow_updates: Allow existing objects returned by the ``getter`` function to be updated. :param allow_updates_in_place: Whereas allow_updates requires the getter to return an existing object which it will then update, allow_updates_in_place will make updates to any existing object it finds at the specified key. :param allow_create: If the ``getter`` returns None, allow the Nested field to create a new instance. :param allow_partial_updates: Allow existing object to be updated using a subset of the fields defined on the Nested field. """ self.mapper = mapper_or_mapper_name self.role = kwargs.pop('role', '__default__') self.collection_class = kwargs.pop('collection_class', list) self.getter = kwargs.pop('getter', None) self.allow_updates = kwargs.pop('allow_updates', False) self.allow_updates_in_place = kwargs.pop( 'allow_updates_in_place', False) self.allow_partial_updates = kwargs.pop( 'allow_partial_updates', False) self.allow_create = kwargs.pop('allow_create', False) super(NestedFieldOpts, self).__init__(**kwargs)
[docs]class Nested(Field): """:class:`Nested` represents an object that is represented by another mapper. Usage:: from kim import Mapper from kim import field class PostMapper(Mapper): __type__ = User id = field.String() name= field.String() content = field.String() user = field.Nested( 'UserMapper', role='public', getter=user_getter, allow_upadtes=False, allow_partial_updates=False, allow_updates_in_place=False, allow_create=False, required=True) .. seealso:: :class:`NestedFieldOpts` """ opts_class = NestedFieldOpts marshal_pipeline = NestedMarshalPipeline serialize_pipeline = NestedSerializePipeline def __init__(self, *args, **kwargs): super(Nested, self).__init__(*args, **kwargs) self._mapper_class = None
[docs] def get_mapper(self, as_class=False, **mapper_params): """Retrieve the specified mapper from the Mapper registry. :param as_class: Return the Mapper class object without calling the constructor. This is typically used when nested is mapping many objects. :param mapper_params: A dict of kwarg's to pass to the specified mappers constructor :rtype: :class:`Mapper` :returns: a new instance of the specified mapper """ from .mapper import get_mapper_from_registry if self._mapper_class is None: mapper = get_mapper_from_registry(self.opts.mapper) self._mapper_class = mapper if as_class: return self._mapper_class else: return self._mapper_class(**mapper_params)
[docs]class CollectionFieldOpts(FieldOpts): """Custom FieldOpts class that provides additional config options for :class:`Collection`. """ def __init__(self, field, **kwargs): """Construct a new instance of :class:`.CollectionFieldOpts` :param field: Specify the field type mpapped inside of this collection. This may be any :class:`Field` type. :param unique_on: Specify a key that is used to check the collection for duplicates. """ self.field = field try: self.field.name except FieldError: pass else: raise FieldError('name/attribute_name/source should ' 'not be passed to a wrapped field.') self.field.opts._is_wrapped = True self.unique_on = kwargs.pop('unique_on', None) super(CollectionFieldOpts, self).__init__(**kwargs)
[docs] def set_name(self, *args, **kwargs): """proxy access to the :class:`FieldOpts` defined for this collections field. :returns: None """ self.field.opts.set_name(*args, **kwargs) super(CollectionFieldOpts, self).set_name(*args, **kwargs)
[docs] def get_name(self): """Proxy access to the :class:`FieldOpts` defined for this collections field. :rtype: str :returns: The value of get_name from the collections Field. """ return self.field.name
[docs] def validate(self): """Exra validation for Collection Field. :raises: FieldOptsError """ if not isinstance(self.field, Field): raise FieldOptsError('Collection requires a valid Field ' 'instance as its first argument')
[docs]class Collection(Field): """:class:`Collection` represents collection of other field types, typically stored in a list. Usage:: from kim import Mapper from kim import field class UserMapper(Mapper): __type__ = User id = field.String() friends = field.Collection(field.Nested('UserMapper', required=True)) user_ids = field.Collection(field.String()) .. seealso:: :class:`CollectionFieldOpts` """ marshal_pipeline = CollectionMarshalPipeline serialize_pipeline = CollectionSerializePipeline opts_class = CollectionFieldOpts
[docs]class StaticFieldOpts(FieldOpts): """Custom FieldOpts class that provides additional config options for :class:`Static`. """ def __init__(self, value, **kwargs): """Construct a new instance of :class:`StaticFieldOpts` :param value: specify the static value to return when this field is serialized. """ self.value = value super(StaticFieldOpts, self).__init__(**kwargs) self.read_only = True
[docs]class Static(Field): """:class:`Static` represents a field that outputs a constant value. This field is implicitly read_only and therefore is typically only used during serialization flows. Usage:: from kim import Mapper from kim import field class UserMapper(Mapper): __type__ = User id = field.String() object_type = field.Static(value='user') """ opts_class = StaticFieldOpts serialize_pipeline = StaticSerializePipeline
class DateTimeFieldOpts(FieldOpts): """Custom FieldOpts class that provides additional config options for :class:`DateTime`. """ def __init__(self, **kwargs): """Construct a new instance of :class:`DateTimeFieldOpts` :param format_str: Specify a format string used to validate an incoming date string. If not value is passed then the format defaults to iso8601. """ self.date_format = kwargs.pop('format_str', 'iso8601') super(DateTimeFieldOpts, self).__init__(**kwargs) self.error_msgs['invalid'] = 'Not a valid datetime.' class DateFieldOpts(FieldOpts): """Custom FieldOpts class that provides additional config options for :class:`Date`. """ def __init__(self, **kwargs): """Construct a new instance of :class:`Date` :param format_str: Specify a format string used to validate an incoming date string. If not value is passed then the format defaults to iso8601. """ self.date_format = kwargs.pop('format_str', '%Y-%m-%d') super(DateFieldOpts, self).__init__(**kwargs) self.error_msgs['invalid'] = 'Not a valid date.'
[docs]class DateTime(Field): """:class:`DateTime` represents an iso8601 encoded date time .. code-block:: python from kim import Mapper from kim import field class UserMapper(Mapper): __type__ = User created_at = field.DateTime(required=True) """ opts_class = DateTimeFieldOpts marshal_pipeline = DateTimeMarshalPipeline serialize_pipeline = DateTimeSerializePipeline
[docs]class Date(DateTime): """:class:`Date` represents a date object .. code-block:: python from kim import Mapper from kim import field class UserMapper(Mapper): __type__ = User signup_date = field.Date(required=True) """ opts_class = DateFieldOpts marshal_pipeline = DateMarshalPipeline