Create a one to many relation to a given target :class:`Resource`.

    Args:

        dest_class(Resource): The *target* class for the relationship

    Keyword Args:

        type(RelationType): The relationship approach to use.
        writable(bool): Whether the relationship is mutable.
        reverse(to_may or to_one): An *optional* reverse relationship.
        reverse_type(RelationType): The reverse relationship approach.
        resource_classes(Resource): The kinds of Resources to expect
            in the relationship


    Returns:

        A builder function which, given a source class creates a
        one-to-many relationship with the previously supplied target.

    A to-many relationship means that the there are many *dest_class*
    resources associated with the given source class. The returned
    method builder will automatically create methods for fetching the
    associated objects. If the *reverse* function is supplied the
    builder will create the correponding reverse relationship methods
    on the target class.

    .. code-block:: python

        @to_many(Sensor, writable=True)
        class Label:
            pass

        # find a label, then fetch sensors
        sensor = label.sensors()

    Since the example above also declares that the relationship is
    *writable* you can also add, remove and update all target
    resources from the source object:

    .. code-block:: python

        # fetch a couple of sensors then add them to the label
        label.add_sensors([sensor1, sensor2])

        # remove a sensor from the label
        label.remove_sensors([sensor1])

        # remove all sensors from the label
        label.update_sensors([])

def to_many(dest_class, type=RelationType.DIRECT,
            reverse=None, reverse_type=RelationType.DIRECT,
            resource_classes=None, writable=False):
    """Create a one to many relation to a given target :class:`Resource`.

    Args:

        dest_class(Resource): The *target* class for the relationship

    Keyword Args:

        type(RelationType): The relationship approach to use.
        writable(bool): Whether the relationship is mutable.
        reverse(to_may or to_one): An *optional* reverse relationship.
        reverse_type(RelationType): The reverse relationship approach.
        resource_classes(Resource): The kinds of Resources to expect
            in the relationship


    Returns:

        A builder function which, given a source class creates a
        one-to-many relationship with the previously supplied target.

    A to-many relationship means that the there are many *dest_class*
    resources associated with the given source class. The returned
    method builder will automatically create methods for fetching the
    associated objects. If the *reverse* function is supplied the
    builder will create the correponding reverse relationship methods
    on the target class.

    .. code-block:: python

        @to_many(Sensor, writable=True)
        class Label:
            pass

        # find a label, then fetch sensors
        sensor = label.sensors()

    Since the example above also declares that the relationship is
    *writable* you can also add, remove and update all target
    resources from the source object:

    .. code-block:: python

        # fetch a couple of sensors then add them to the label
        label.add_sensors([sensor1, sensor2])

        # remove a sensor from the label
        label.remove_sensors([sensor1])

        # remove all sensors from the label
        label.update_sensors([])

    """
    def method_builder(cls):
        dest_resource_type = dest_class._resource_type()
        dest_method_name = pluralize(dest_resource_type).replace('-', '_')
        doc_variables = {
            'from_class': cls.__name__,
            'to_class': dest_class.__name__,
            'to_name': dest_method_name
        }

        fetch_method_doc = """Fetch the {to_name} associated with this :class:`{from_class}`.

        Returns:

              iterable({to_class}): The {to_name} of :class:`{from_class}`
        """

        def _fetch_relationship_included(self, filter=None):
            session = self._session
            include = self._include
            if include is None or dest_class not in include:
                # You requested an included relationship that was
                # not originally included
                error = "{} was not included".format(dest_class.__name__)
                raise AttributeError(error)
            included = self._included.get(dest_resource_type)
            mk_one = dest_class._mk_one(session,
                                        resource_classes=resource_classes)
            result = [mk_one({'data': entry}) for entry in included]
            return result if filter is None else list(_filter(filter, result))

        def fetch_relationship_include(self, use_included=False, filter=None):
            if use_included:
                return _fetch_relationship_included(self, filter=filter)
            session = self._session
            id = None if self.is_singleton() else self.id
            url = session._build_url(self._resource_path(), id)
            params = build_request_include([dest_class], None)

            def _process(json):
                included = json.get('included')
                mk_one = dest_class._mk_one(session,
                                            resource_classes=resource_classes)
                result = [mk_one({'data': entry}) for entry in included]
                return result if filter is None else list(_filter(filter, result))
            return session.get(url, CB.json(200, _process), params=params)

        def fetch_relationship_direct(self, use_included=False, filter=None):
            if use_included:
                return _fetch_relationship_included(self)
            session = self._session
            id = None if self.is_singleton() else self.id
            url = session._build_url(self._resource_path(), id,
                                     dest_resource_type)
            process = dest_class._mk_many(session,
                                          resource_classes=resource_classes,
                                          filter=filter)
            return session.get(url, CB.json(200, process))

        if type == RelationType.DIRECT:
            fetch_relationship = fetch_relationship_direct
        elif type == RelationType.INCLUDE:
            fetch_relationship = fetch_relationship_include
        else:  # pragma: no cover
            raise ValueError("Invalid RelationType: {}".format(type))

        fetch_relationship.__doc__ = fetch_method_doc

        def add_many(self, resources):
            """Add {to_name} to this :class:`{from_class}`.

            Args:

              resources: A list of :class:`{to_class}` to add

            Returns:

                True if the relationship was mutated, False otherwise
            """
            session, url, json = _build_relatonship(self, dest_resource_type,
                                                    resources)
            return session.post(url, CB.boolean(200, false_code=204),
                                json=json)

        def remove_many(self, resources):
            """Remove {to_name} from this :class:`{from_class}`.

            Args:

              resources: A list of :class:`{to_class}` to remove

            Returns:

                True if the relationship was mutated, False otherwise
            """
            session, url, json = _build_relatonship(self, dest_resource_type,
                                                    resources)
            return session.delete(url, CB.boolean(200, false_code=204),
                                  json=json)

        def update_method(self, resources):
            """Set the {to_name} for this :class:`{from_class}`.

            To remove all {to_name} pass in an empty list.

            Args:

              resources: A list of :class:`{to_class}` to set

            Returns:

                True if successful
            """
            session, url, json = _build_relatonship(self, dest_resource_type,
                                                    resources)
            return session.patch(url, CB.boolean(200), json=json)

        methods = [(dest_method_name, fetch_relationship)]
        if writable:
            methods.extend([
                ('add_{}'.format(dest_method_name), add_many),
                ('remove_{}'.format(dest_method_name), remove_many),
                ('update_{}'.format(dest_method_name), update_method)
            ])
        for name, method in methods:
            method.__doc__ = method.__doc__.format(**doc_variables)
            setattr(cls, name, method)

        if reverse is not None:
            reverse(cls, type=reverse_type)(dest_class)

        return cls

    return method_builder