# -*- coding: utf-8 -*-

"""

  model
  ~~~~~

  :author: Sam Gammon <sg@samgammon.com>
  :copyright: (c) Sam Gammon, 2014
  :license: This software makes use of the MIT Open Source License.
            A copy of this license is included as ``LICENSE.md`` in
            the root of the project.

"""

__version__ = 'v5'

# stdlib
import abc
import string
import inspect
import operator
import datetime

# model components
from . import query
from . import exceptions

# dateutil
from dateutil import parser as dtparser

# model adapters
from .adapter import abstract, concrete
from .adapter import KeyMixin, ModelMixin
from .adapter import VertexMixin, EdgeMixin

# datastructures
from canteen.util.struct import EMPTY
from canteen.util.struct import BidirectionalEnum


# Globals / Sentinels
_NDB = False  # `canteen.model` no longer supports NDB
_MULTITENANCY = False  # toggle multi-tenant key namespaces
Edge = Vertex = Model = AbstractModel = None  # must initially be `None`
_DEFAULT_KEY_SCHEMA = ('id', 'kind', 'parent')  # default key schema
_MULTITENANT_KEY_SCHEMA = ('id', 'kind', 'parent', 'namespace', 'app')
_BASE_MODEL_CLS = frozenset(('Model', 'AbstractModel', 'Vertex', 'Edge'))
_BASE_GRAPH_CLS = frozenset(('Vertex', 'Edge'))
is_adapter = lambda _t: issubclass(_t.__class__, abstract.ModelAdapter)
_PROPERTY_SLOTS = (
  'name',  # property string name (used as attribute/item key)
  '_options',  # arbitrary userland property options
  '_indexed',  # should this property be indexed?
  '_required',  # should property should be required?
  '_repeated',  # should property be allowed to keep multiple values?
  '_basetype',  # property base type, for validation and storage
  '_default')  # default value for property, defaults to ``None``


def is_there_a_global(name):

  """ Simple utility to interrogate the global context
      and see if something is defined yet.

      :param name: Name to check for global definition
        in this module.

      :returns: Whether the target ``Name`` is defined
        in module globals and is not falsy. """

  gl = globals()
  return name in gl and (not gl[name])


# == Metaclasses == #

class MetaFactory(type):

  """ Abstract parent for Model API primitive metaclasses,
      such as :py:class:`AbstractKey.__metaclass__` and
      :py:class:`AbstractModel.__metaclass__`.

      Enforces the metaclass chain and proper :py:mod:`abc`
      compliance.

      .. note :: Metaclass implementors of this class **must**
             implement :py:meth:`cls.initialize()`, or
             :py:class:`Model` construction will yield a
             :py:exc:`NotImplementedError`. """

  class __metaclass__(abc.ABCMeta):

    """ Embedded metaclass - enforces ABC compliance and
      properly formats :py:attr:`cls.__name__`. """

    __owner__ = 'MetaFactory'

    def __new__(mcs, name=None, bases=tuple(), properties=None):

      """ Factory for metaclasses classes. Regular
          metaclass factory function.

          If the target class definition has the attribute
          :py:attr:`cls.__owner__`, it will be taken as the
          target class' internal ``__name__``. ``basestring``
          and classes are accepted (in which case the bound
          class' name is taken instead).

          :param name: String name for the metaclass class to factory.
          :param bases: Metaclass class inheritance path.
          :param properties: Property dictionary, as defined inline.
          :returns: Constructed :py:class:`MetaFactory.__metaclass__`
            descendant.

          .. note:: This class is *two* levels up in the meta chain.
                Please note this is an *embedded* metaclass used for
                *metaclass classes*. """

      # alias embedded metaclasses to their `__owner__` (for __repr__)
      name = mcs.__name__ = mcs.__owner__ if hasattr(mcs, '__owner__') else name

      # enforces metaclass
      return super(mcs, mcs).__new__(mcs, name, bases, properties or {})

  # = Internal Methods = #
  def __new__(mcs, name=None, bases=tuple(), properties=None):

    """ Factory for concrete metaclasses. Enforces
        abstract-ness (prevents direct construction) and
        dispatches :py:meth:`mcs.initialize()`.

        :param name: String name for the metaclass to factory.
        :param bases: Inheritance path for the new concrete metaclass.
        :param properties: Property dictionary, as defined inline.
        :returns: Constructed :py:class:`MetaFactory` descendant.
        :raises: :py:exc:`model.exceptions.AbstractConstructionFailure`
          upon concrete construction. """

    # fail on construction - embedded metaclasses cannot be instantiated
    if not name: raise exceptions.AbstractConstructionFailure(mcs.__name__)

    # pass up to `type`, which properly enforces metaclasses
    impl = mcs.initialize(name, bases, properties or {})

    # if we're passed a tuple, we're being asked to super-instantiate
    if isinstance(impl, tuple):
      return super(MetaFactory, mcs).__new__(mcs, *impl)
    return impl

  ## = Exported Methods = ##
  @classmethod
  def resolve(mcs, name, bases, properties, default=True):

    """ Resolve a suitable model adapter for a given model Key or Model.

        :param name: Class name, as provided to :py:meth:`__new__`.
        :param bases: Inheritance path for the target :py:class:`Model`.
        :param properties: Class definition, as provided to :py:meth:`__new__`.
        :keyword default: Allow use of the default adapter. Defaults to `True`.
        :returns: A suitable :py:class:`model.adapter.ModelAdapter` subclass.

        :raises NoSupportedAdapters: in the case that no supported (or valid)
          adapters could be found.

        :raises InvalidExplicitAdapter: in the case of an unavailable,
          explicitly-requested adapter. """

    from canteen.model.adapter import abstract

    if '__adapter__' not in properties:

      for i in bases:
        if hasattr(i, '__adapter__'):
          # parent specifies adapter and no explicit, so use that
          return None, i.__adapter__.acquire(name, bases, properties)

      # grab each supported adapter
      _adapters = [option for option in concrete if option.is_supported()]

      # fail with no adapters...
      if not _adapters: raise exceptions.NoSupportedAdapters()

      # if we only have one adapter, the choice is easy...
      if not default:
        return _adapters[0].acquire(name, bases, properties), tuple()
      return _adapters[0].acquire(name, bases, properties)

    # an explicit adapter was requested via an `__adapter__` class property
    _spec = properties['__adapter__']

    if not isinstance(_spec, (list, tuple)):
      _spec = [_spec]

    for _spec_item in _spec:

      if isinstance(_spec_item, (type, basestring)):
        for _a in concrete:
          if _a is _spec_item or _a.__name__ == _spec_item:
            return _a.acquire(name, bases, properties)

      elif is_adapter(_spec_item):  # pragma: no cover
        # it's a valid model adapter instance already
        return _spec_item  # pragma: no cover

    adapter_label = _spec
    if not isinstance(_spec, basestring):  # pragma: no cover
      if isinstance(_spec, type):
        adapter_label = _spec.__name__
      if issubclass(_spec.__class__, abstract.ModelAdapter):
        adapter_label = _spec.__class__.__name__
    raise exceptions.InvalidExplicitAdapter(adapter_label)

  ## = Abstract Methods = ##
  @abc.abstractmethod
  def initialize(cls, name, bases, properties):

    """ Initialize a subclass. Must be overridden by child metaclasses.

        :param name: Target class name to initialize.
        :param bases: Target class bases to mix in.
        :param properties: Bound properties to target class.

        :raises NotImplementedError: Always, if called directly, as this
          method is abstract. """

    raise NotImplementedError('`initialize` is abstract.')  # pragma: no cover


## == Abstract Classes == ##

class AbstractKey(object):

  """ Abstract Key class. Provides base abstract
      functionality supporting concrete Key objects. """

  __schema__ = _DEFAULT_KEY_SCHEMA  # set default key schema

  ## = Encapsulated Classes = ##
  class __metaclass__(MetaFactory):

    """ Metaclass for model keys. Reorganizes internal
        class structure to support object-specific
        behavior. See ``cls.initialize`` for details. """

    # class owner and schema to spawn by default
    __owner__, __schema__ = 'Key', _DEFAULT_KEY_SCHEMA

    @classmethod
    def initialize(mcs, name, bases, pmap):

      """ Initialize a Key class. Reorganize class layout
          to support ``Key`` operations. The class property
          ``__schema__`` (expected to be an iterable of string
          schema item names) is scanned for key structure.

          The following non-standard class-level properties are set:
            - ``__slots__`` - frozen to nothing as everything is class-level
            - ``__owner__`` - owner model object for a key, if any
            - ``__adapter__`` - adapter for this key, resolved at class time
            - ``__persisted__`` - whether this key is known to be persisted

          Each schema'd key chunk is also given an internal-style
          property (read: surrounded by ``__``).

          :param name: Name of ``Key`` class to initialize.
          :param bases: Base classes to mix into target ``Key`` class.
          :param pmap: Map of properties bound to target ``Key`` class.

          :returns: Constructed type extending ``Key``. """

      # resolve adapter
      _adapter = mcs.resolve(name, bases, pmap)
      _module = pmap.get('__module__', 'canteen.model')

      if isinstance(_adapter, tuple):
        _base, _adapter = _adapter
      else:
        _base, _adapter = False, _adapter  # use concrete adapter

      if name == 'AbstractKey':  # <-- must be a string
        return name, bases, dict([('__adapter__', _adapter)] + pmap.items())

      key_class = [  # build initial key class structure
        ('__slots__', set()),  # seal object attributes
        ('__bases__', bases),  # define bases for class
        ('__name__', name),  # set class name internals
        ('__owner__', None),  # reference to current owner entity
        ('__module__', _module),  # add class package
        ('__persisted__', False)]  # default to not persisted

      if _base is not None:  # allow adapter definition to defer upwards
        key_class.append(('__adapter__', _adapter))  # pragma: no cover

      # resolve schema and add key format items, initted to None
      _schema = [
        ('__%s__' % x, None) for x in pmap.get('__schema__', mcs.__schema__)]

      # return an argset for `type`
      return name, bases, dict(_schema + key_class + pmap.items())

    def mro(cls):

      """ Generate a fully-mixed MRO for `AbstractKey` subclasses.
          Handles injection of ``CompoundKey`` object in MRO to support
          ``KeyMixin`` composure.

          The non-base MRO pattern is as follows:
            - for ``AbstractKey``: ``KeyMixin -> object``
            - for ``Key``: `AbstractKey -> KeyMixin -> object``
            - for ``Key`` classes: ``Key -> AbstractKey -> KeyMixin -> object``

          :returns: Properly-mixed MRO for an `AbstractKey` subclass. """

      if cls.__name__ == 'AbstractKey':  # `AbstractKey` MRO
        return (cls, KeyMixin.compound, object)

      if cls.__name__ == 'Key':  # `Key` MRO
        return (cls, AbstractKey, KeyMixin.compound, object)

      # `Key`-subclass MRO, with support for diamond inheritance
      return tuple(filter(lambda x: x not in (Key, AbstractKey), [cls] + (
        list(cls.__bases__))) + [Key, AbstractKey, KeyMixin.compound, object])

    def __repr__(cls):

      """ String representation of a `Key` class. Note that this
          ``__repr__`` handles ``cls`` representation.

          :returns: ``x(y)`` where ``x`` is the `Key` class in use
            and ``y`` is collapsed key schema. """

      # dump key schema
      return '%s(%s)' % (
        cls.__name__, ', '.join((i for i in reversed(cls.__schema__))))

  def __new__(cls, *args, **kwargs):

    """ Intercepts construction requests for abstract model classes.
        Enforces abstractness, but otherwise passes calls to ``super``.

        :raises AbstractConstructionFailure: In the case that an attempt
          is made to construct an ``AbstractKey`` directly.

        :returns: New instance of `cls`, so long as `cls` it is concrete. """

    # prevent direct instantiation of `AbstractKey`
    if cls.__name__ == 'AbstractKey':
      raise exceptions.AbstractConstructionFailure('AbstractKey')

    return super(cls, cls).__new__(*args, **kwargs)  # pragma: no cover

  def __eq__(self, other):

    """ Test whether two keys are functionally identical.

        Tries the following tests to negate equality:
        - target vs self falsyness
        - target vs self schema
        - target vs self type
        - target vs self data

        If all elements are reported to be equal, the keys
        are functionally identical. If any of the tests
        fail, the keys are not identical.

        :param other: Other key to test against ``self``.

        :returns: ``True`` if ``other`` is functionally
          identical to ``self``, otherwise ``False``. """

    if (not self and not other) or (self and other):
      if isinstance(other, self.__class__):  # class check
        if self.__schema__ == other.__schema__:
            # last resort: check each data property
            return all((i for i in map(lambda x: (
                getattr(other, x) == getattr(self, x)), self.__schema__)))
    # didn't pass one of our tests
    return False  # pragma: no cover

  def __repr__(self):

    """ Generate a string representation of this Key object.
        Note that this method is responsible for ``self``
        representation, not ``cls``.

        :returns: String describing the local ``Key`` object,
          like ``x(y)``, where ``x`` is the local ``Key``
          class name and ``y`` is a formatted list of this
          keys's schema and data. """

    pairs = ('%s=%s' % (k, getattr(self, k)) for k in reversed(self.__schema__))
    return "%s(%s)" % (self.__class__.__name__, ', '.join(pairs))

  # util: alias `__repr__` to string magic methods
  __str__ = __unicode__ = __repr__

  # util: support for `__nonzero__` and aliased `__len__`
  __nonzero__ = lambda self: isinstance(self.__id__, (basestring, int, long))
  __len__ = lambda self: (
    int(self.__nonzero__()) if self.__parent__ is None else (
      sum((1 for i in self.ancestry))))

  ## = Property Setters = ##
  def _set_internal(self, name, value):

    """ Set an internal property on a `Key`. Used by ``Key``
        internals to proxy attribute and item calls.

        :param name: Name of the internal property to set.
        :param value: Value to set the internal property to.

        :raises PersistedKey: If an internal property set is
          attempted on an already-persisted ``Key``.

        :returns: ``self``, for chainability. """

    if value is '': value = None

    # fail if we're already persisted (unless we're updating the owner)
    if self.__persisted__ and name != 'owner':
      raise exceptions.PersistedKey(name)
    if name == 'parent' and isinstance(value, Model):
      # special case: if setting parent and it's a model, use the key instead
      value = value.key  # pragma: no cover
    if name == 'id' and isinstance(value, basestring) and ':' in value:
      raise ValueError('Keynames may not contain the ":" character.'
                       ' Got: "%s".' % value)  # pragma: no cover
    setattr(self, '__%s__' % name, value)
    return self

  ## = Property Getters = ##
  def _get_ancestry(self):

    """ Retrieve this ``Key``'s ancestry path, one item at a
        time, starting from this ``Key``'s root and proceeding
        down the chain. Acts as a generator.

        :raises StopIteration: When no more ancestry entries
          are available to yield.

        :returns: Yields ancestry items one at a time, starting
          with this ``Key``'s root, and ending with ``self``. """

    if self.__parent__:  # if we have a parent, yield upward
      for i in self.__parent__.ancestry: yield i
    yield self  # yield self to end the chain, and stop iteration
    raise StopIteration()

  ## = Property Bindings  = ##
  id = property(lambda self: self.__id__,
                lambda self, id: self._set_internal('id', id))

  app = property(lambda self: self.__app__,
                 lambda self, app: self._set_internal('app', app))

  kind = property(lambda self: self.__kind__,
                  lambda self, kind: self._set_internal('kind', kind))

  parent = property(lambda self: self.__parent__,
                    lambda self, p: self._set_internal('parent', p))

  owner = property(lambda self: self.__owner__)  # `owner` is read-only
  ancestry = property(_get_ancestry)  # `ancestry` is read-only

  # ns = `None` when disabled
  namespace = property(lambda self: self.__namespace__ or None,
                       lambda self, ns: self._set_internal('namespace', ns))


## AbstractModel
class AbstractModel(object):

  """ Abstract Model class. Specifies abstract
      properties that apply to all `Model`-style
      data containers.

      Applies an embedded `MetaFactory`-compliant
      metaclass to properly initialize subclasses. """

  __slots__ = tuple()


  ## = Encapsulated Classes = ##
  class __metaclass__(MetaFactory):

    """ Embedded `MetaFactory`-compliant metaclass.
        Handles initialization of new `AbstractModel`
        descendent classes.

        Reorganizes class layout and structure to
        satisfy future internal model operations.
        See ``cls.initialize`` for details. """

    __owner__ = 'Model'

    @staticmethod
    def _get_prop_filter(inverse=False):

      """ Builds a callable that can filter properties
          between internal and user-defined ones. Returns

          By default, the callable produced by this
          method will produce ``False`` if the property
          is internal and ``True`` otherwise.

          To flip this functionality, pass ``True`` to
          inverse, which simply converts the boolean
          response to ``True`` for internal properties,
          and ``False`` for user data.

          :param inverse: Flip the filter to only return
            truthy for internal properties. Defaults to
            ``False`` for internal-filter behavior.

          :returns: Closure that is appropriate for use
            with ``filter`` and returns ``True`` if the
            property ``bundle`` handed in is internal. """

      def _filter_prop(bundle):

        """ Decide whether a property is kept as a data
            value or a class internal. See wrapping
            function for full description.

            :param bundle: Property bundle to examine.

            :returns: ``True`` or ``False`` according
              to the property ``bundle``'s status as
              an internal property. """

        key, value = bundle  # extract, this is dispatched from ``{}.items``
        if key.startswith('_'): return inverse
        if isinstance(value, classmethod): return inverse
        if inspect.isfunction(value) or inspect.ismethod(value): return inverse
        return (not inverse)

      return _filter_prop

    @classmethod
    def initialize(mcs, name, bases, properties):

      """ Initialize a ``Model`` descendent for use as a
          data model class. Reorganizes class internals
          to store descriptors for defined user data
          properties.

          Also injects a few properties into the class
          map to satisfy ``Model`` layer functionality.

          :param name: Name of the ``Model``-descendent
            class to initialize.

          :param bases: Base classes to extend with ``cls``
            target. Must include ``Model``, ``Vertex``,
            ``Edge`` or a class that extends one thereof.

          :param properties: Class property map, as defined
            inline or via a manual call to ``initialize``.
            Always expected to be a ``dict``.

          :raises AssertionError: If incorrect types are
            passed for ``name``, ``bases`` or ``properties``.
            ``__debug__`` must be active to enable assertions.

          :returns: Initialized target ``Model``-descendent
            class, with rewritten property map. """

      assert isinstance(name, basestring), "class name must be a string"
      assert isinstance(bases, tuple), "class bases must be a tuple of types"
      assert isinstance(properties, dict), "class map must be a valid dict"

      property_map = {}
      _nondata_map = {}

      # core classes eval before being defined - must use string name :(
      if name not in frozenset(['AbstractModel', 'Model']):

        modelclass = {}

        # parse spec (`name=<basetype>` or `name=<basetype>,<options>`)
        # also, model properties that start with '_' are ignored
        for prop, spec in (
            filter(mcs._get_prop_filter(), properties.iteritems())):

          if any((char not in string.lowercase for char in prop[1:])) and (
              isinstance(spec, type) and (
                  issubclass(spec, BidirectionalEnum))):  # pragma: no cover
            modelclass[prop] = spec
            continue  # camel-case properties should be skipped for init

          # build a descriptor object and data slot
          basetype, options = (
            (spec, {}) if not isinstance(spec, tuple) else spec)

          # simple strings should accept both str and unicode
          if basetype is str: basetype = basestring

          # (note that specifying unicode explicitly only accepts unicode)
          if isinstance(basetype, Property):
            property_map[prop] = basetype  # pragma: no cover
          else:
            property_map[prop] = Property(prop, basetype, **options)

        # drop non-data-properties into our ``_nondata_map``
        for prop, value in filter(mcs._get_prop_filter(inverse=True),
                                  properties.iteritems()):
          _nondata_map[prop] = value

        # merge and clone all basemodel properties, update dictionary
        if len(bases) > 1 or bases[0] is not Model:

          # build a full property map, after reducing parents left -> right
          _pmap_data = ([(
            [(prop, b.__dict__[prop].clone()) for prop in b.__lookup__])
              for b in bases] + [property_map.items()])

          # calculate full dict property map
          property_map = dict([(key, value) for key, value in (
            reduce(operator.add, _pmap_data))])

        # freeze property lookup
        prop_lookup = frozenset((k for k, v in property_map.iteritems()))

        # resolve default adapter for model
        model_adapter = mcs.resolve(name, bases, properties)
        if isinstance(model_adapter, tuple):
          _base, model_adapter = model_adapter
        else:
          _base = False  # we have an adapter, use it

        _model_internals = {  # build class layout, initialize core model
          '__impl__': {},  # holds cached implementation classes
          '__name__': name,  # add internal class name (should be Model kind)
          '__kind__': name,  # kindname defaults to model class name
          '__bases__': bases,  # stores a model class's bases, so MRO can work
          '__lookup__': prop_lookup,  # frozenset of allocated attributes
          '__module__': properties.get('__module__'),  # add model's module
          '__slots__': tuple()}  # seal-off object attributes

        # resolves default adapter class, but allow deference upwards
        if _base is not None:
          _model_internals['__adapter__'] = model_adapter

        modelclass.update(property_map)  # update at class-level
        modelclass.update(_nondata_map)  # update at class-level with non data
        modelclass.update(_model_internals)  # lastly, apply model internals

        # for top-level graph objects
        if name in ('Edge', 'Vertex'):
          if is_there_a_global(name):
            # make flag for edge/vertex class
            graph_object_flag = '__%s__' % name.lower()

            # mark as graph model and according type
            properties['__graph__'] = properties[graph_object_flag] = True

        # for graph-object subclasses
        elif any((hasattr(b, '__graph__') for b in bases)) or (
                name in ('Edge', 'Vertex')):

          # find owner name
          object_owner = 'Vertex' if (
            any((hasattr(b, '__vertex__') for b in bases))) else 'Edge'

          # set flags
          graph_flag = '__%s__' % object_owner.lower()
          _model_internals['__owner__'] = object_owner
          _model_internals['__graph__'] = _model_internals[graph_flag] = True

        # inject our own property map
        impl = super(MetaFactory, mcs).__new__(mcs, name, bases, modelclass)
        return model_adapter._register(impl)

      return name, bases, properties  # pass-through to `type`

    def mro(cls):

      """ Generate a fully-mixed method resolution order for
          `AbstractModel` subclasses.

          According to the parent base that we're extending,
          models get custom MRO chains to fulfill mixin
          attributes in the same Canteen-style DI at runtime.

          :raises RuntimeError: If MRO cannot be calculated
            because the target ``cls`` is completely invalid.

          :returns: Mixed MRO according to base class structure. """

      def find_nonbase_mro():

        """ Finds proper MRO for non-base classes, i.e. classes
            that actually make use of ``Model``, ``Edge`` or
            ``Vertex`` (not those classes themselves).

            :returns: Class-tree-specific MRO, if possible, in a
              ``list`` suitable for composure into a final MRO. """

        base_models = frozenset((Vertex, Edge, Model, AbstractModel))

        # graph models?
        if any((hasattr(b, '__graph__') for b in cls.__bases__)):
          _vertex = any((hasattr(b, '__vertex__') for b in cls.__bases__))
          return [i for i in cls.__bases__ if i not in base_models] + [
                   Vertex if _vertex else Edge,
                   Model, AbstractModel,
                   VertexMixin.compound if _vertex else EdgeMixin.compound,
                   ModelMixin.compound]

        # vanilla non-base MRO
        return [i for i in cls.__bases__ if i not in base_models] + [
                  Model, AbstractModel, ModelMixin.compound]

      # calculate common `Model` MRO
      base_model_mro = (
        [ModelMixin.compound] if cls.__name__ == 'AbstractModel' else [
          AbstractModel, ModelMixin.compound])

      target_mro = {

        # `AbstractModel` just gets the mixin
        'AbstractModel': lambda: [ModelMixin.compound],

        # `Model` gets `AbstractModel` and mixin
        'Model': lambda: base_model_mro,

        # `Vertex` structure overrides `Model` structure
        'Vertex': lambda: [VertexMixin.compound, Model] + base_model_mro,

        # `Edge` structure overrides `Model` structure
        'Edge': lambda: [EdgeMixin.compound, Model] + base_model_mro,

      }.get(cls.__name__, find_nonbase_mro)()

      if not target_mro or not isinstance(target_mro, list):
        # not a base class
        raise RuntimeError('Unable to calculate MRO for unidentified'
                           ' MetaFactory-descendent'
                           ' Model class: %s.' % cls)  # pragma: no cover

      return tuple([cls] + target_mro + [object])

    def __repr__(cls):

      """ Generate string representation of `Model` class,
          like "Model(<prop1>, <prop n...>)".

          :returns: String representation of this `Model. """

      if hasattr(cls, '__lookup__') and cls.__name__ not in _BASE_MODEL_CLS:
        return '%s(%s)' % (cls.__name__, ', '.join((i for i in cls.__lookup__)))

      elif (cls.__name__ in _BASE_GRAPH_CLS and (
        cls.__owner__ in _BASE_GRAPH_CLS)) or (cls.__name__ == 'Model' or (
            cls.__name__ == 'AbstractModel')):
        return cls.__name__

      return 'Model<%s>' % cls.__name__  # pragma: no cover

    __str__ = __unicode__ = __repr__

    def __setattr__(cls, name, value,
                               exception=exceptions.InvalidAttributeWrite):

      """ Disallow property mutation before instantiation.

          The Exception raised for invalid writes can be set
          via ``exception``.

          :param name: Name of the property we are being asked to mutate.
          :param value: Value that we are being asked to set ``name`` to.
          :param exception: Exception to be raised in the event of an error.
            Handles switchover to ``KeyError``-based exceptions if this method
            is being called from Python's item-style API.

          :raises InvalidItem: In the case that this method is used via Python's
            Item API and an invalid attempt is made to write to a property.

          :raises InvalidAttributeWrite: In the case that this method is used
            in an invalid way to attempt an attribute write.

          :returns: Generally ``None`` if a successful write is made. """

      # cannot mutate data before instantiation
      if name in cls.__lookup__:
        raise exception('mutate', name, cls)

      # setting internal class-level properties
      if name.startswith('__') or callable(value) or (
        isinstance(value, (classmethod, staticmethod))):
        return super(AbstractModel.__metaclass__, cls).__setattr__(name, value)

      # cannot create new properties before instantiation
      raise exception('create', name, cls)

    def __getitem__(cls, name, exception=exceptions.InvalidItem):

      """ Override itemgetter syntax to return property
          objects at the class level.

          :param name: Name of the value we wish to retrieve.
          :param exception: Exception to be raised

          :raises InvalidItem: In the case of an attempt to read
            an invalid or non-existent property value.

          :returns: Value of the target property ``name``, or the
            :py:class:`model.Property` object itself if accessed
            at a class level. """

      if name not in cls.__lookup__:  # pragma: no cover
        raise exception('read', name, cls)  # cannot read non-data properties
      return cls.__dict__[name]


  class _PropertyValue(object):

    """ Named-tuple class for property value bundles.
        DOCSTRING"""

    __fields__, __slots__ = zip(*((f, '__%s__' % f) for f in ('data', 'dirty')))

    def __init__(self, data, dirty=False):

      """ Initialize a new `PropertyValue` instance.

          :param data: The data to store in this ``_PropertyValue`` instance.

          :param dirty: Whether the data has been modified/mutated since
            original instantiation. Defaults to ``False``, as the most common
            use case for constructing a new ``_PropertyValue`` instance is data
            coming back from an adapter. """

      self.__data__, self.__dirty__ = data, dirty

    # util: map data and dirty properties
    data, dirty = (property(lambda self: self.__data__),
                   property(lambda self: self.__dirty__))

    # util: reduce arguments for pickle
    __getnewargs__ = lambda self: tuple(self)

    # util: iterate over items in order
    __iter__ = lambda self: iter((self.__data__, self.__dirty__))

    # util: support getitem syntax, where 0 is `data` and 1 is `dirty`
    __getitem__ = lambda self, item: (
        self.__data__ if not item else self.__dirty__)

    # util: generate a string representation of this `_PropertyValue`
    __repr__ = lambda self: "Value(%s)%s" % ((
          '"%s"' % self.data) if isinstance(self.data, basestring)
              else repr(self.data), '*' if self.dirty else '')

  # = Internal Methods = #
  def __new__(cls, *args, **kwargs):

    """ Intercepts construction requests for directly Abstract model classes.

        Args:
        Kwargs:

        :raises:

        :returns: """

    if cls.__name__ == 'AbstractModel':  # prevent direct instantiation
      raise exceptions.AbstractConstructionFailure('AbstractModel')
    return super(AbstractModel, cls).__new__(cls, *args, **kwargs)

  # util: generate a string representation of this entity
  __repr__ = lambda self: "%s(%s, %s)" % (
      self.__kind__, self.__key__,
        ', '.join(['='.join([k, str(self.__data__.get(k, None))])
                   for k in self.__lookup__]))

  #__str__ = __unicode__ = __repr__  # map repr to str and unicode

  def __setattr__(self, name, value, exception=exceptions.InvalidAttribute):

    """ Attribute write override.

        :param name:
        :param value:
        :param exception:

        :raises:

        :returns: """

    # internal properties, data properties and `key`
    # can be written to after construction
    if name.startswith('__') or name in self.__lookup__ or name == 'key':
      # delegate upwards for write
      return super(AbstractModel, self).__setattr__(name, value)
    raise exception('set', name, self.kind())

  def __getitem__(self, name):

    """ Item getter support.

        :param name:

        :returns: """

    if name not in self.__lookup__:
      # only data properties are exposed via `__getitem__`
      raise exceptions.InvalidItem('get', name, self.kind())
    return getattr(self, name)  # proxy to attribute API

  # util: support for python's item API
  __setitem__ = lambda self, item, value: (
    self.__setattr__(item, value, exceptions.InvalidItem))

  def __context__(self, _type=None, value=None, traceback=None):

    """ Context enter/exit - apply explicit mode.

        :param _type:
        :param value:
        :param traceback:

        :returns: """

    if traceback:  # pragma: no cover
      return False  # in the case of an exception in-context, bubble it up
    self.__explicit__ = (not self.__explicit__)  # toggle explicit status
    return self

  # util: alias context entry/exit to `__context__` toggle method
  __enter__ = __exit__ = __context__

  # util: proxy `len` to length of written data (also alias `__nonzero__`)
  __len__ = lambda self: len(self.__data__)
  __nonzero__ = __len__

  # util: `dirty` property flag, proxies to `_PropertyValue`(s) for dirtyness
  __dirty__ = property(lambda self: any(
    (dirty for value, dirty in self.__data__.itervalues())))

  # util: `persisted` property flag, indicates whether key has been persisted
  __persisted__ = property(lambda self: self.key.__persisted__)

  def __iter__(self):

    """ Allow models to be used as dict-like generators.

        :returns: """

    for name in self.__lookup__:
      value = self._get_value(name, default=Property._sentinel)

      # skip unset properties without a default, except in `explicit` mode
      if (value == Property._sentinel and (not self.__explicit__)):
        if self.__class__.__dict__[name]._default != Property._sentinel:
          # return a prop's default in `implicit` mode
          yield name, self.__class__.__dict__[name].default
        continue  # pragma: no cover
      yield name, value
    raise StopIteration()

  def _set_persisted(self, flag=False):

    """ Notify this entity that it has been persisted to storage.

        :param flag:

        :returns: """

    self.key.__persisted__ = True
    for name in self.__data__:  # iterate over set properties
      # set value to previous, with `False` dirty flag
      self._set_value(name, self._get_value(name,
                      default=Property._sentinel), False)
    return self

  def _get_value(self, name, default=None):

    """ Retrieve the value of a named property on this Entity.

        :param name:
        :param default:

        :raises:
        :returns: """

    if name:  # calling with no args gives all values in (name, value) form
      if name in self.__lookup__:
        value = self.__data__.get(name, Property._sentinel)
        if not value:
          if self.__explicit__ and value is Property._sentinel:
            return Property._sentinel  # return EMPTY sentinel in explicit mode
          if callable(default):  # handle callable defaults
            return default(self)  # pragma: no cover
          return default  # return default value passed in
        return value.data  # return property value
      raise exceptions.InvalidAttribute('get', name, self.kind())
    return [(i, getattr(self, i)) for i in self.__lookup__]

  def _set_value(self, name, value=EMPTY, _dirty=True):

    """ Set (or reset) the value of a named property on this Entity.

        :param name:
        :param value:
        :param _dirty:

        :raises:
        :returns: """

    if not name: return self  # empty strings or dicts or iterables return self

    if isinstance(name, (list, dict)):
      if isinstance(name, dict):
        name = name.items()  # convert dict to list of tuples
      # filter out flags from caller
      return [self._set_value(k, i, _dirty=_dirty) for k, i in name if (
                k not in ('key', '_persisted'))]

    # allow a tuple of (name, value), for use in map/filter/etc
    if isinstance(name, tuple):  # pragma: no cover
      name, value = name

    if name == 'key':  # if it's a key, set through _set_key
      return self._set_key(value).owner  # returns `self` :)

    if name in self.__lookup__:  # check property lookup

      prop = self.__class__[name]  # extract property

      # inflate ISO-formatted datetimes
      if prop.basetype in (datetime.date, datetime.datetime) and not (
          isinstance(value, (datetime.date, datetime.datetime))):
        if isinstance(value, basestring):  # pragma: no cover
          # try to inflate from ISO
          value = dtparser.parse(value)
          if prop.basetype is datetime.date:
            value = value.date

        elif isinstance(value, (int, float)):  # pragma: no cover
          # try to inflate from timestamp
          value = datetime.datetime.fromtimestamp(value) if (
            prop.basetype is datetime.datetime) else (
            datetime.date.fromtimestamp(value))

      # if it's a valid property, create a namedtuple value placeholder
      self.__data__[name] = self.__class__._PropertyValue(value, _dirty)
      return self
    raise exceptions.InvalidAttribute('set', name, self.kind())

  def _set_key(self, value=None, **kwargs):

    """ Set this Entity's key manually.

        kwargs

        :param value:

        :raises:
        :returns: """

    _valid_key_classes = (self.__class__.__keyclass__, tuple, basestring)

    # cannot provide both a value and formats
    if value and kwargs:
      raise exceptions.MultipleKeyValues(self.kind(), value, kwargs)

    if issubclass(self, Vertex) and (
        isinstance(value, Key) and not isinstance(value, VertexKey)):
      value = VertexKey.from_urlsafe(value.urlsafe())  # pragma: no cover
    elif issubclass(self, Edge) and (
        isinstance(value, Key) and not isinstance(value, EdgeKey)):
      value = EdgeKey.from_urlsafe(value.urlsafe())  # pragma: no cover

    # for a literal key value
    if value is not None:
      if not isinstance(value, _valid_key_classes):  # filter out invalid keys
        raise exceptions.InvalidKey(*(
          type(value),
          value,
          self.__class__.__keyclass__.__name__))

      # set local key from result of dict->get(<formatter>)->__call__(<value>)
      self.__key__ = {

        # return keys directly
        self.__class__.__keyclass__: lambda x: x,

        # pass tuples through `from_raw`
        tuple: self.__class__.__keyclass__.from_raw,

        # pass strings through `from_urlsafe`
        basestring: self.__class__.__keyclass__.from_urlsafe

      }.get(type(value), lambda x: x)(value)._set_internal('owner', self)

      return self.__key__  # return key

    if kwargs:  # filter out multiple formats
      formatter, value = kwargs.items()[0]
      if len(kwargs) > 1:  # disallow multiple format kwargs
        raise exceptions.MultipleKeyFormats(', '.join(kwargs.keys()))

      # resolve key converter, if any, set owner, and `__key__`, and return
      self.__key__ = {

        # for raw, pass through `from_raw`
        'raw': self.__class__.__keyclass__.from_raw,

        # for strings, pass through `from_urlsafe`
        'urlsafe': self.__class__.__keyclass__.from_urlsafe,

        # by default it's a constructed key
        'constructed': lambda x: x

      }.get(formatter, lambda x: x)(value)._set_internal('owner', self)
      return self.__key__

    # except in the case of a null value and no formatter args
    raise exceptions.UndefinedKey(value, kwargs)

  ## = Property Bindings  = ##
  key = property(lambda self: self.__key__, _set_key)  # bind model key


## == Concrete Classes == ##

## Key
class Key(AbstractKey):

  """ Concrete Key class.
      DOCSTRING """

  __separator__ = u':'  # separator for joined/encoded keys
  __schema__ = (_DEFAULT_KEY_SCHEMA if not (
                _MULTITENANCY) else _MULTITENANT_KEY_SCHEMA)

  ## = Internal Methods = ##
  def __new__(cls, *parts, **formats):

    """ Constructs keys from various formats.

        args, kwargs

        :raises:
        :returns: """

    # extract 1st-provided format
    formatter, value = (
      formats.items()[0] if formats else ('__constructed__', None))

    # disallow multiple key formats
    try:
      _fmtgen = (i for i in formats if i != '_persisted')
      _fmtgen.next(), _fmtgen.next()
      raise exceptions.MultipleKeyFormats(', '.join(formats.keys()))
    except StopIteration:
      pass

    if not parts and formats.get('raw'):
      return cls.from_raw(formats['raw'])
    if not parts and formats.get('urlsafe'):
      return cls.from_urlsafe(formats['urlsafe'])

    return super(AbstractKey, cls).__new__(cls, *parts, **formats)

  def __init__(self, *parts, **kwargs):

    """ Initialize this Key.

        args, kwargs

        :raises:
        :returns: """

    if kwargs.get('raw') or kwargs.get('urlsafe') and (
        self.kind and self.id): return  # if we're re-initializing, return

    if len(parts) > 1:  # normal case: it's a full/partially-spec'd key

      # it's a fully- or partially-spec'ed key
      if len(parts) <= len(self.__schema__):
        _parts_diff = (len(self.__schema__) - len(parts))
        _pluck_kind = lambda x: x.kind() if hasattr(x, 'kind') else x
        mapped = zip([i for i in reversed(self.__schema__)][_parts_diff:],
                         map(_pluck_kind, parts))

      else:
        # for some reason the schema falls short of our parts
        raise exceptions.KeySchemaMismatch(*(
              self.__class__.__name__,
              len(self.__schema__),
              str(self.__schema__)))

      _pluck = lambda x: (x[0], x[1].kind()) if isinstance(x[1], Model) else x
      for name, value in map(_pluck, mapped):
        setattr(self, name, value)  # set appropriate attribute via setter

    elif len(parts) == 1:  # special case: it's a kinded, empty key
      if hasattr(parts[0], 'kind'):
        parts = (parts[0].kind(),)  # quick ducktyping: is it a model?
      self.__kind__ = parts[0]

    # if we *know* this is an existing key, `_persisted` should be `true`
    self._set_internal('parent', kwargs.get('parent'))

    # also set kwarg-passed parent.
    self._set_internal('persisted', kwargs.get('_persisted'))

  def __setattr__(self, name, value):

    """ Block attribute overwrites.

        :param name:
        :param value:

        :raises:
        :returns: """

    if not name.startswith('__'):
      if name not in self.__schema__:
        raise exceptions.InvalidKeyAttributeWrite('create', name, self)
      if getattr(self, name) is not None:
        raise exceptions.InvalidKeyAttributeWrite('overwrite', name, self)
    return super(AbstractKey, self).__setattr__(name, value)


class VertexKey(Key):

    """ Key class for ``Vertex`` records. """


class EdgeKey(Key):

    """ Key class for ``Edge`` records. """


## Property
class Property(object):

  """ Concrete Property class.
      DOCSTRING """

  __metaclass__ = abc.ABCMeta  # enforce definition of `validate` for subclasses
  __slots__ = _PROPERTY_SLOTS  # setup slots for property options
  _sentinel = sentinel = EMPTY  # default sentinel for basetypes/values

  ## = Internal Methods = ##
  def __init__(self, name, basetype,
                           default=_sentinel,
                           required=False,
                           repeated=False,
                           indexed=True,
                           **options):

    """ Initialize this Property.

        kwargs

        :param name:
        :param basetype:
        :param default:
        :param required:
        :param repeated:
        :param indexed:

        :raises:
        :returns: """

    # shim choices in from enum
    if isinstance(basetype, type) and (
        issubclass(basetype, BidirectionalEnum)) and not (
          options.get('choices')):
      options['choices'] = [choice for (key, choice) in basetype]

    # copy locals specified above onto object properties of the same name,
    # specified in `self.__slots__`
    map(lambda args: setattr(self, *args), (
          zip(self.__slots__, (
            name, options, indexed, required, repeated, basetype, default))))

  ## = Descriptor Methods = ##
  def __get__(self, instance, owner):

    """ Descriptor attribute access.

        :param instance:
        :param owner:

        :raises:
        :returns: """

    if isinstance(instance, Model):  # proxy to internal entity method.

      is_empty = (
        instance._get_value(self.name, default=Property.sentinel) is (
          Property.sentinel))
      if (self.options.get('embedded') is True) and (
              isinstance(self.basetype, type)) and (
              issubclass(self.basetype, Model)):

        # if we're an embedded submodel and so far we're unset, initialize
        # the property first with an empty instance
        if is_empty and not instance.__explicit__:
          ## @TODO(sgammon): embedded models that know about their encapsulators
          instance._set_value(self.name, self.basetype(), False)

        elif is_empty and instance.__explicit__:
          # if we have an empty or missing submodel in explicit mode, return
          # value directly, which should be the empty sentinel
          return Property.sentinel  # pragma: no cover

      # grab value, returning special
      # a) property default or
      # b) sentinel if we're in explicit mode and it is unset
      if self.default != Property.sentinel:  # we have a set default
        value = instance._get_value(self.name, default=self.default)
      else:
        value = instance._get_value(self.name, default=Property.sentinel)

      return None if (not value and (value == Property.sentinel) and
                      instance.__explicit__ is False) else value

    # otherwise, class-level access is always the property in question
    return self

  def __set__(self, instance, value):

    """ Descriptor attribute write.

        :param instance:
        :param value:

        :raises:
        :returns: """

    if instance is not None:  # only allow data writes after instantiation
      # delegate to `AbstractModel._set_value`
      return instance._set_value(self.name, value)
    raise exceptions.InvalidAttributeWrite('set', self.name, self.kind)

  # delegate to `__set__` to clear the property
  __delete__ = lambda self, instance: instance.__set__(instance, None)

  def valid(self, instance):

    """ Validate the value of this property, if any.

        :param instance:

        :raises:
        :returns: """

    if self.__class__ != Property and hasattr(self, 'validate'):
      # check for subclass-defined validator to delegate validation to
      return self.validate(instance)  # pragma: no cover

    value = instance._get_value(self.name)  # retrieve value

    # check required-ness
    if value is None or value is self.sentinel:
      if self.required:
        raise exceptions.PropertyRequired(self.name, instance.kind())
      # empty value, non-required, all good :)
      if value is self._sentinel: return True

    if isinstance(value, (list, tuple, set, frozenset)):  # check multi-ness
      if not self.repeated:
        raise exceptions.PropertyNotRepeated(self.name, instance.kind())
    else:
      if self.repeated:
        raise exceptions.PropertyRepeated(self.name, instance.kind())
      value = (value,)  # make value iterable

    for v in value:  # check basetype

      # it validates if:
      # 1) the field is typeless, or
      # 2) the value is `None` or an instance of it's basetype
      # 3) the value is a submodel and complies with:
      #    - being a valid model and kind (if embedded)
      #    - being a valid key (if not embedded)
      # 4) the value is a `Key` and we are an `Edge` seeking a `VertexKey`
      if self.basetype is None or (
        (v is not self.sentinel) and isinstance(v, (
                                      self.basetype, type(None)))):
        continue
      if isinstance(self.basetype, type):  # pragma: no cover
        if issubclass(self.basetype, Model):
          if self.options.get('embedded') and isinstance(v, self.basetype):
            continue  # embedded & compliant model
          elif not self.options.get('embedded') and isinstance(v, Key):
            continue  # non-embedded & compliant key
        elif issubclass(self.basetype, BidirectionalEnum):
          if v in self.basetype:
            continue  # consider bidirectional enums

      raise exceptions.InvalidPropertyValue(*(
        self.name, instance.kind(), type(v).__name__, self.basetype.__name__))
    return True  # validation passed! :)

  def __repr__(self):

    """ Generate a string representation
      of this :py:class:`Property`.

      :returns: Stringified, human-readable
      value describing this :py:class:`Property`. """

    return "%s(%s, type=%s)" % (
      self.__class__.__name__, self.name, self._basetype.__name__)

  __unicode__ = __str__ = __repr__

  # util method to clone `Property` objects
  clone = lambda self: self.__class__(self.name, self._basetype, self._default,
                                      self._required, self._repeated,
                                      self._indexed, **self._options)

  # config accessors
  basetype, required, repeated, indexed, options = (
    property(lambda self: self._basetype),
    property(lambda self: self._required),
    property(lambda self: self._repeated),
    property(lambda self: self._indexed),
    property(lambda self: self._options))

  @property
  def default(self):

    """ Accessor that collapses callback-based default values or returns any
        explicitly-set default value for a ``Property`` object.

        :returns: Default value at ``self._default`` if explicitly set. If
          ``self._default`` is a callable, it is called with the local model
          as the first and only parameter, and expected to return a value
          suitable for storage. """

    if callable(self._default):
      return self._default(self)  # pragma: no cover
    return self._default

  ## == Query Overrides (Operators) == ##
  __sort__ = lambda self, direction: (  # internal sort spawn
    query.Sort(self, operator=(direction or query.Sort.ASCENDING)))
  __filter__ = lambda self, other, operator: (  # internal filter spawn
    query.Filter(self, other, operator=(operator or query.Filter.EQUALS)))

  ## == Sort Spawn == ##
  __pos__ = lambda self: (
    self.__sort__(query.Sort.ASCENDING))  # `+` operator
  __neg__ = lambda self: (
    self.__sort__(query.Sort.DESCENDING))  # `-` operator

  ## == Filter Spawn == ##
  __eq__ = lambda self, other: (
    self.__filter__(other, query.Filter.EQUALS))  # `==` operator
  __ne__ = lambda self, other: (
    self.__filter__(other, query.Filter.NOT_EQUALS))  # `!=` operator
  __gt__ = lambda self, other: (
    self.__filter__(other, query.Filter.GREATER_THAN))  # `>` operator
  __ge__ = lambda self, other: (
    self.__filter__(other, query.Filter.GREATER_THAN_EQUAL_TO))  # `>=` operator
  __lt__ = lambda self, other: (
    self.__filter__(other, query.Filter.LESS_THAN))  # `<` operator
  __le__ = lambda self, other: (
    self.__filter__(other, query.Filter.LESS_THAN_EQUAL_TO))  # `<=` operator


class Model(AbstractModel):

  """ Concrete Model class. """

  __keyclass__ = Key

  ## = Internal Methods = ##
  def __init__(self, **properties):

    """ Initialize this Model.

        args

        :raises:
        :returns: """

    # grab key / persisted flag, if any, and set explicit flag to `False`
    self.__explicit__, self.__initialized__ = False, True

    # initialize key, internals, and map any kwargs into data
    self.key, self.__data__ = (
      properties.get('key') or self.__keyclass__(self.kind(), _persisted=False),
      {})

    self._set_value(properties, _dirty=(not properties.get('_persisted')))

  ## = Class Methods = ##
  kind = classmethod(lambda cls: cls.__name__)


class Vertex(Model):

  """ Concrete Vertex class.
      DOCSTRING """

  __owner__, __keyclass__ = 'Vertex', VertexKey

  class __metaclass__(AbstractModel.__metaclass__):

    """ DOCSTRING """

    def __gt__(cls, target):

      """ Syntactic sugar for creating an on-the-fly :py:class:`Edge` subclass.

          Overrides the syntax:
            ``class Friends(Person > Person): [...]```

          :param other: Target :py:class:`Vertex` subclass to factory an
            :py:class:`Edge` to.

          :returns: Factoried :py:class:`Edge` subclass that represents a type
            connecting ``self`` to ``other``. """

      return cls.to(target, directed=False)

    def __lt__(cls, origin):

      """ Syntactic sugar for creating an on-the-fly :py:class:`Edge` subclass.

          Overrides the syntax:
            ``class Supporting(Organization < Person): [...]```

          :param other: Target :py:class:`Vertex` subclass to factory an
            :py:class:`Edge` to.

          :returns: Factoried :py:class:`Edge` subclass that represents a type
            connecting ``self`` to ``other``. """

      return origin.to(cls, directed=False)

    def __rshift__(cls, target):

      """ Syntactic sugar for creating an on-the-fly :py:class:`Edge` subclass.
          This syntactic extension spawns a **directed** ``Edge``.

          Overrides the syntax:
            ``class Comment(Person >> Post): [...]```

          :param other: Target :py:class:`Vertex` subclass to factory an
            :py:class:`Edge` to.

          :returns: Factoried :py:class:`Edge` subclass that represents a type
            connecting ``self`` to ``other``. """

      return cls.to(target, directed=True)

    def __lshift__(cls, origin):

      """ Syntactic sugar for creating an on-the-fly :py:class:`Edge` subclass.
          This syntactic extension spawns a **directed** ``Edge``.

          Overrides the syntax:
            ``class Contribution(Legislator << Contributor): [...]```

          :param other: Target :py:class:`Vertex` subclass to factory an
            :py:class:`Edge` to.

          :returns: Factoried :py:class:`Edge` subclass that represents a type
            connecting ``self`` to ``other``. """

      return origin.to(cls, directed=True)

  @classmethod
  def to(cls, *targets, **options):

    """ Syntactic sugar for creating an on-the-fly :py:class:`Edge` subclass.

        :param target: Target :py:class:`Vertex` subclass to factory
          an :py:class:`Edge` to.

        :param directed: Whether the target :py:class:`Edge` subclass should be
          considered directional in nature.

        :raises TypeError: In the case that a non-``Vertex`` is passed for
          ``target``.

        :returns: Factoried :py:class:`Edge` subclass that represents a type
          connecting ``self`` to ``other``. """

    # classes only plz
    for target in targets:
      if not issubclass(target, Vertex):  # pragma: no cover
        raise TypeError("Vertices can only point to other Vertices."
                        " An Edge from `%s` to `%s` was requested, but `%s` is"
                        " not a Vertex." % (cls, target, target))

    return Edge.__spec_class__(cls, targets, options.get('directed', False))


class EdgeSpec(object):

  """ Specifies the peering and directed-ness of an :py:class:`Edge`. """

  __slots__ = ('origin', 'peering', 'directed')

  def __new__(cls, name=None, bases=None, pmap=None):

    """  """

    # subtype construction generates ``Edge`` subclasses
    if name and bases and isinstance(pmap, dict):
      return type(name, (Edge,), dict(pmap or {}, __spec__=bases[0]))

    # different signature if we're not constructing (for clarity)
    origin, peering, directed = name, bases, pmap

    # allow construction as well (for use as a config symbol, essentially)
    return object.__new__(cls, origin, peering, directed)

  def __init__(self, origin, peering, directed):

    """ Initialize an ``EdgeSpec`` class, according to the specified ``peering``
        configuration.

        :param origin:

        :param peering: An ordered iterable of ``Vertex`` subclasses to connect
          via an ``Edge``, where index ``0`` is always considered the **origin**
          and the remaining items are targets that can be validly connected to
          **origin**.

        :param directed: Flag (``bool``) indicating that this ``Edge``
          represents a directional relationship between each
          ``origin -> target`` pair. """

    if not len(peering) > 0:  # pragma: no cover
      raise TypeError('Cannot specify an `Edge` without at'
                      ' least one target.')

    self.origin, self.peering, self.directed = (
      origin, peering[0] if len(peering) == 1 else peering, directed)

  def __repr__(self):  # pragma: no cover

    """ Generate a string representation for the relationship specified by this
        ``EdgeSpec`` class.

        :returns: String, like ``(origin <-> target, ...)`` (if undirected),
          otherwise ``(origin -> target, ...)`` (if directed). """

    return "(%s %s %s)" % (
      self.origin.kind(), '<->' if not self.directed else '->', (
        self.peering.kind() if issubclass(self.peering, Model) else ', '.join((
          k.kind() for k in self.peering))))


class Edge(Model):

  """ Concrete Edge class.
      DOCSTRING """

  __owner__, __keyclass__ = 'Edge', EdgeKey


  class __metaclass__(Model.__metaclass__):

    """ DOCSTRING """

    @classmethod
    def initialize(mcs, name, bases, pmap):

      """ DOCSTRING """

      if '__spec__' not in pmap or pmap['__spec__'].directed:
        pmap['source'] = Key
        pmap['target'] = Key, {'repeated': True}
      if '__spec__' not in pmap or (
           '__spec__' in pmap and not pmap['__spec__'].directed):
        pmap['peers'] = Key, {'indexed': True, 'repeated': True}
      return super(mcs, mcs).initialize(name, bases, pmap)

  __spec_class__ = EdgeSpec

  ## = Internal Methods = ##
  def __init__(self, source=None, *targets, **properties):

    """ Initialize this ``Edge`` with a ``Vertex`` ``source`` and ``target``
        pair.

        :param pair_or_source:
        :param maybe_target:
        :param **properties:

        :raises:
        :returns: """

    _key_from_model = lambda obj: obj.key if isinstance(obj, Vertex) else obj
    _keyify = lambda k: (
      VertexKey.from_urlsafe(k) if isinstance(k, basestring) else (
        _key_from_model(k)))


    # @TODO(sgammon): better validation on inflation (directed vs not, etc)

    if not source and not targets and 'peers' in properties:
      source, target = tuple(properties['peers'])
      targets = (target,)  # should be length of 1

      if len(targets) > 1:  # pragma: no cover
        raise TypeError('Undirected `Edge` got multiple targets,'
                        ' where only one was expected:'
                        ' "%s".' % targets)

    if not source and 'source' in properties:  # pragma: no cover
      source = properties['source']
    if not targets and 'targets' in properties:  # pragma: no cover
      targets = properties['targets']

    if (source is None or not targets) and not (
        properties.get('_persisted')):  # pragma: no cover
      raise TypeError('Constructing an `Edge` requires at least'
                      ' one `source` and `target`, or one pair'
                      ' (`source`, `target`).')

    if hasattr(self, '__spec__') and self.__spec__.directed:
      properties['source'], properties['target'] = (
        _keyify(source), tuple((_keyify(t) for t in targets)))
    else:
      properties['peers'] = tuple([_keyify(source)] + list((
                                        _keyify(target) for target in targets)))

    super(Edge, self).__init__(**properties)


# Module Globals
__abstract__ = (abstract,
                MetaFactory,
                AbstractKey,
                AbstractModel)

__concrete__ = (concrete,
                Property,
                KeyMixin,
                ModelMixin,
                Key,
                Model,
                Vertex,
                Edge)

# All modules
__all__ = ('concrete',
           'abstract',
           'MetaFactory',
           'AbstractKey',
           'AbstractModel',
           'query',
           'Property',
           'KeyMixin',
           'ModelMixin',
           'Key',
           'Model',
           'Vertex',
           'Edge',
           'adapter',
           'exceptions')
