import sqlobject
import datetime

### SQLOBJECT SUBCLASSES
class LookupableSQLObject(sqlobject.SQLObject):
    """
    Subclass of SQLObject supporting the lookup() method, which returns
    a unique instance of the class (or None).

    The class definition must include a signature, which is a tuple of
    class attributes whose combination suffices to distinguish uniquely
    an instance of the class.

    To use lookup(), invoke it as a class method:
        cat = Animal.lookup(genus='Felis', species='catus')
    If Animal.signature = ('genus', 'species'), then you could also call:
        cat = Animal.lookup('Felis', 'catus')
    """
    signature = ('fixMe',) # override this tuple in the class definition

    @classmethod
    def _groomLookup(cls, *arguments, **keywords):
        """
        Backend for cls._groom().  The signature is stripped out & returned
        as its own dictionary.  The other keywords are assumed to be
        attributes, which are returned as another dictionary.
        """
        try:
            assert isinstance(cls.signature, tuple)
            assert cls.signature != ('fixMe',)
        except AssertionError:
            raise AttributeError('Undefined signature in class definition %s',
                                cls.__name__)
        try:
            assert len(arguments) + len(keywords) >= len(cls.signature)
            assert len(cls.signature) >= len(arguments)
            delta = len(cls.signature)-len(arguments)
            if delta:
                for sigField in cls.signature[-delta:]:
                    arguments += (keywords.pop(sigField),) # tuple concatenation
        except AssertionError, KeyError:
            raise SignatureError(cls.signature, arguments, keywords)

        signature = dict(zip(cls.signature,arguments))
        attributes = keywords
        return signature,attributes

    @classmethod
    def _groom(cls, *arguments, **keywords):
        """
        Convenience function to groom argument/keywords passed in by callers.
        Calls a backend, which may be overriden in a subclass definition.
        """
        return cls._groomLookup(cls, *arguments, **keywords)

    @classmethod
    def _selectUnique(cls,selectResults):
        """
        Convenience function used when there are zero or one possible query results.
        Takes as input a SQLObject SelectResults object.  Returns the unique object, or None.
        Throws an error if there's more than one hit.
        """
        if selectResults.count():
            try:
                assert selectResults.count()==1
            except AssertionError:
                selectResults = list(selectResults)
                className = selectResults[0].__class__.__name__
                message = "Conflicting database instances \
                        discovered for class "+className
                raise DbInternalConflict(
                        list(selectResults),message)
            object = selectResults[0]
        else:
            object = None
        return object

    @classmethod
    def _lookup(cls, **signature):
        """
        Lookup backend; accepts a signature dict as output by cls._groom().
        """
        results = cls.selectBy(**signature)
        return cls._selectUnique(results)

    @classmethod
    def lookup(cls, *arguments, **keywords):
        """
        Uses signature only; all other keywords are ignored.
        """
        signature, attributes = cls._groom(*arguments, **keywords)
        return cls._lookup(**signature)

class MergeableSQLObject(LookupableSQLObject):
    """
    Subclass of LookupableSQLObject supporting the merge() method, which
    is designed to update the DB table from new data inputs while avoiding
    duplication and inconsistency.  Like LookupableSQLObject,
    MergeableSQLObject needs a signature tuple in the class definition.

    merge() is a class method; pass it instance attributes like so:
        cat = Animal.merge(genus='Felis', species='catus', note='Foo')
    If Animal.signature = ('genus', 'species'), then you could also call:
        cat = Animal.merge('Felis', 'catus', note='Foo')

    Optionally, you can pass in some arguments to set policy in case of
    conflicts between existing data and incoming data.  These arguments
    are explained below in the docstring for cls._mediate().
    """
    @classmethod
    def _mediate(cls, instance, policy, **attributes):
        """
        Handles potential conflicts between existing data persisted in the DB,
        and new information being processed.

        Input a MergeableSQLObject instance, a dictionary of policy arguments,
        and a candidate instance embodied in a dictionary of attributes.
        These two instances are in fact supposed to be the same object.
        If any attributes explicitly differ, an error is raised.

        The 'significantNull' policy argument (a boolean) controls whether
        instances that differ only by one of them having attribute(s) that
        are None/NULL should be judged to differ significantly (and thus
        throw an error in mediation).  If NULLs are significant, then an empty
        attribute is interpreted to mean that the object lacks that attribute;
        an error will be raised if the other instance has a nonempty attribute.
        If NULLs are not significant, than an empty attribute is interpreted
        as a lack of data (not a lack of attribute) and the function will try
        to sync the attribute with fuller information from the other instance.

        Setting the 'clobber' policy to True simply causes the incoming
        instance to overwrite the DB instance, without any attempt to check
        for consistency or preserve the integrity of existing data.  It's
        mostly for debugging purposes.
        """
        if policy['clobber']:
            instance.set(**attributes)
        else:
            # Don't clobber the existing instance.
            # Verify that DB attributes match our input
            try:
                for (attribute,value) in attributes.iteritems():
                    attributeValue = getattr(instance, attribute)
                    if attributeValue is None and not policy['significantNull']:
                        # Update NULL in DB with fuller info
                        setattr(instance, attribute, value)
                    else:
                        assert attributeValue==value
            except AssertionError:
                sig = zip(cls.signature, signature)
                attributes.update(sig)
                raise DbExternalConflict(instance, attributes)
        return instance

    @classmethod
    def _doMerge(cls, instance, policy, **attributes):
        """
        Merge logic; may be overridden in subclass definition.
        The base definition here simply hands off its input to _mediate().
        """
        return cls._mediate(instance, policy, **attributes)


    @classmethod
    def _groom(cls, *arguments, **keywords):
        """
        Overrides LookupableSQLObject._groom().
        Returns 3 dictionaries: (signature, attributes, policy).
        """
        signature,attributes = cls._groomLookup(*arguments, **keywords)
        policy = {}
        for argument in ('significantNull', 'clobber'):
            if attributes.has_key(argument):
                value = attributes.pop(argument)
                try:
                    assert isinstance(value, bool)
                except AssertionError:
                    raise ValueError('%s must be Boolean True/False' % argument)
                policy[argument] = value
            else:
                policy[argument] = False
        return signature, attributes, policy

    @classmethod
    def merge(cls, *arguments, **keywords):
        """
        Input instance attributes; update DB.  Conflicts get passed
        to cls._mediate(); its docstring explains the details.
        """
        signature,attributes,policy = cls._groom(*arguments,**keywords)

        # See if an instance with this signature already exists.
        instance = cls.lookup(**signature)
        # Complete set of attributes includes signature
        attributes.update(signature)
        if not instance:
            instance = cls(**attributes)
        else:
            instance = cls._doMerge(instance, policy, **attributes)
        return instance

class DynamicState(MergeableSQLObject):
    """
    Persists attribute history for DynamicSQLObject, q.v. its docstring.

    While MergeableSQLObjects represent static objects, DynamicStates change
    dynamically, which causes considerable differences handling lookup/merge,
    particularly regarding the interpretation of NULL/None attribute values.
    NULL could mean that the attribute affirmatively doesn't exist, or it
    could mean that we lack information about that attribute.  The design
    of the class and its methods allows the user to choose an interpretation
    by setting the significantNull keyword(True/False).  If NULLs are
    significant, the appearance or disappearance of a NULL value in the
    datastream vs. most current instance persisted in the database will be
    treated as a state change, causing the creation of a new row.  If NULLs
    aren't significant, then the system will try to fill in NULL attributes
    in the database by updating existing instances when possible.

    This behavior is based on a data model that our database tables are
    intended to represent changes in the *actual* intrinsic state of the
    object, rather than to persist our source datastream as faithfully
    as possible.  That is to say, we choose to process data at input
    time rather than later interpret it as it's used.
    """
    parent = None # override with ForeignKey in subclass definition; set notNone=True
    signature = ('id', 'date') # override this tuple in the class definition - MUST INCLUDE DATE
    date = sqlobject.DateCol(notNone=True)

    @classmethod
    def _lookupBySignature(cls, *arguments, **keywords):
        """
        Backend for cls.lookup().  Pass in a signature (including date)
        to return a unique matching instance (or else None).
        Any attributes passed in are ignored.

        Insted of requiring an exact match of the input signature, we require
        an exact match of just the signature ex-date, and then return the most
        recent instance on or before the given date.
        """
        signature, attributes = cls._groomLookup(*arguments, **keywords)
        try:
            date = signature.pop('date')
            assert isinstance(date, datetime.date)
        except:
            raise TypeError("Date attribute must be type datetime.date")
        # SQLObject select chaining kung fu
        return cls.selectBy(**signature).filter(cls.q.date<=date).orderBy('-date')

    @classmethod
    def _groomParentDate(cls, parent, date=datetime.date.today(), **keywords):
        """
        Strips out parent & date from input; verifies types.
        """
        try:
            assert isinstance(parent, DynamicSQLObject)
            assert isinstance(date, datetime.date)
        except AssertionError:
            raise TypeError
        return parent, date

    @classmethod
    def _lookupByParent(cls, *arguments, **keywords):
        """
        Backend for cls.lookup().  Pass in an instance of the parent
        class and a datetime.date to return the most recent DynamicState
        of that parent on or before that date (or else None).
        Any attributes passed in are ignored.
        """
        parent, date = cls._groomParentDate(*arguments, **keywords)
        return cls.select(sqlobject.AND(cls.q.parentID==parent.id,
                                cls.q.date<=date), orderBy='-date')

    @classmethod
    def lookup(cls, *arguments, **keywords):
        """
        Overrides MergeableSQLObject.lookup().  Allows DynamicState instances
        to be looked up either by parent/date (as used in
        DynamicSQLObject.lookupAttributes()) or else by signature/date (as
        used in cls.merge()).
        """
        try:
            results = cls._lookupByParent(*arguments, **keywords)
        except TypeError:
            results = cls._lookupBySignature(*arguments, **keywords)
        return cls._selectUnique(results)


    def _lookupNext(self):
        """
        Returns the DynamicState of the same parent with the next date.
        """
        cls = type(self)
        results = cls.select(sqlobject.AND(cls.q.parentID==self.parentID,
                            cls.q.date>self.date),
                            orderBy='date')
        if results.count():
            result = results[0]
        else:
            result = None
        return result

    @classmethod
    def _sync(cls, instance, policy, **attributes):
        """
        Returns whether or not the instance & the candidate instance are
        materially different.  If NULLs aren't significant, then any NULLs
        are synced with data from the other instance, if available.
        """
        identical = True
        for (attribute,value) in attributes.iteritems():
            dbValue = getattr(instance, attribute)
            if  value != dbValue:
                if policy['significantNull']:
                    # NULLs are significant.  Any attribute difference
                    # is considered material.
                    identical = False
                else:
                    # NULLs aren't significant.
                    if (value is not None) and (dbValue is not None):
                        # Material difference
                        identical = False
                    elif dbValue is None:
                        # Update db NULL attribute with candidate data
                        setattr(instance, attribute, value)
                    else:
                        # Update candidate NULL attribute with db data
                        attributes[attribute] = dbValue
        return identical

    @classmethod
    def _doMerge(cls, instance, policy, **attributes):
        """
        Overrides _doMerge method of MergeableSQLObject - q.v. its docstring.

        For an DynamicState, we don't require the dates to match.  If the instance in memory is more recent than the DB instance returned by lookup(), then keep the DB instance and also persist the instance in memory.  If the two dates are the same, then the attributes should match.  Current policy is to populate the DB with the maximum amount of information, so if the in-memory instance has attributes that are NULL in the DB, then the DB instance gets updated with the missing attributes.

        If clobber=True is passed in, conflicts will be resolved in favor of the in-memory instance.   If clobber=False, the opposite is true.
        """
        if instance.date == attributes['date']:
            # The candidate instance should be a duplicate of what's already in the DB
            instance = cls._mediate(instance, policy, **attributes)
        else:
            # Our attributes are more recent than the existing attributes.
            # Check to see if attributes are materially different.
            if not cls._sync(instance, policy, **attributes):
                # Check to see whether candidate attributes are materially
                # different from the next DynamicState
                nextInstance = instance._lookupNext()
                if not cls._sync(nextInstance, policy, **attributes):
                    # If different, then instantiate candidate instance
                    instance = cls(parent=parent, date=date, **attributes)
                else:
                    # If the same, then backdate the next Instance.
                    nextInstance.date = date
        return instance

class DynamicSQLObject(LookupableSQLObject):
    """
    Subclass of LookupableSQLObject designed to represent individual objects
    with dynamic attributes, allowing the entire 'worm trail' of their
    attribute histories to be persisted & accessed by date.

    Each subclass of DynamicSQLObject is related to a subclass of
    DynamicState, which latter holds the history of attributes. You have
    to spell out the relationship in the class definitions, using the
    'parent' attribute of DynamicState, and the 'stateClass' and
    'states' atrributes of DynamicSQLObject.  In order to set
    up the join, the DynamicState definition must come before the
    DynamicSQLObject definition.  Here's an example:
        class SecurityDynamicState(sqlobjectextensions.DynamicState):
            parent = sqlobject.ForeignKey('Security')
            cusip = sqlobject.StringCol(length=9, default=None)
            symbol = sqlobject.StringCol(length=8, default=None)
            signature = ('cusip', 'date')
        class Security(sqlobjectextensions.DynamicSQLObject):
            stateClass = SecurityDynamicState
            states = sqlobject.MultipleJoin('SecurityDynamicState',
                                                    joinColumn='parent_id')

    DynamicSQLObject provides instance methods used to manage
    its attribute history, namely lookupAttributes() and mergeAttributes().
    Both of these methods require a date to be passed in, e.g.:
            att = khdh.lookupAttributes(datetime.date(2007,1,1))
                    # Returns a SecurityDynamicState instance
            att = khdh.mergeAttributes(datetime.date(2007,1,1),
                                cusip='482462108', symbol='KHDH')
    MergeAttributes() supports the clobber argument, q.v. MergeableSQLObject.

    Just as with LookupableSQLObject, the class definition for a subclass
    of DynamicSQLObject specifies a signature, which is a tuple of
    attributes (stored in the related subclass of DynamicState) that allows
    an instance of the DynamicSQLObject subclass to be identified
    uniquely via the lookup() class method.  Here's an example:
        khdh = Security.lookup(cusip='482462108')

    DynamicSQLObject also supports merge(), much like MergeableSQLObject
    except that a date needs to be passed in as well:
        khdh = Security.merge(cusip='482462108', date=datetime.date(2007,1,1),
                             symbol='KHDH')
    As before, the signature can be passed as arguments rather than keywords.

    Policy arguments can be passed in as keywords; q.v. the docstring for
    MergeableSQLObject._mediate().
    """
    stateClass = None # override with classname in class definition
    # The following join screws things up if defined here, even if it's overridden
    #   in the subclass definition.  So, copy/edit/uncomment in subclass definition.
    #states = sqlobject.MultipleJoin('DynamicState',joinColumn='parent_id')
    signature = None # Don't bother overriding in subclass definition; no sig

    def lookupAttributes(self, date):
        return self.stateClass.lookup(self, date)

    def mergeAttributes(self, *arguments, **keywords):
        """Requires a date as part of the signature."""
        return self.stateClass.merge(*arguments, **keywords)

    @classmethod
    def lookup(cls, *arguments, **keywords):
        """
        Overrides LookupableSQLObject.lookup().
        Requires a date as part of the signature.  Attributes are ignored.
        """
        signature, attributes = cls.stateClass._groomLookup(*arguments, **keywords)
        # The following lookup answers the question, "Who last had this signature"?
        targetState = cls.stateClass.lookup(**signature)
        if targetState is None:
            result = None
        else:
            result = targetState.parent
            # Now make sure that the result still owns the signature as of
            # the lookup date.
            date = signature.pop('date')
            latestState = result.lookupAttributes(date)
            sigMatch = True
            for attribute,value in signature.iteritems():
                if getattr(latestState, attribute) != value:
                    sigMatch = False
            if not sigMatch:
                result = None
        return result

    @classmethod
    def merge(cls, *arguments, **keywords):
        """
        Overrides MergeableSQLObject.merge().
        Requires a date as part of the signature.
        """
        signature, attributes, policy = cls.stateClass._groom(*arguments, **keywords)
        # See if an instance with this signature already exists.
        instance = cls.lookup(**signature)
        # Complete set of attributes includes signature
        attributes.update(signature)
        if not instance:
            # Make a new instance
            instance = cls()
            # Create the DynamicState
            dynamicState = cls.stateClass(parent=instance, **attributes)
        else:
            # We have an existing instance; merge attributes
            # First append policy to attributes
            attributes.update(policy)
            dynamicState = instance.mergeAttributes(**attributes)
        return instance

### EXCEPTIONS

class Error(Exception):
    """Base class for custom exceptions."""
    def __str__(self):
        return self.message

class SignatureError(Error):
    """
    Exception raised when incorrect signature is passed to a lookup function
    """
    def __init__(self, classSig, inputArgs, inputKw, message=None):
        if message == None:
            message = "Input signature doesn't match class definition"
        message += ', class signature: %s' % classSig
        message += ', input arguments: %s' % inputArgs
        message += ', input keywords: %s' % inputKw
        self.message = message

class DbInternalConflict(Error):
    """
    Exception raised when crosschecks reveal DB internal inconsistency.

    Attributes:
        instances -- problematic instances packed into sequence
        field -- if known, the field that caused the conflict
        message -- explanation of the error
    """
    def __init__(self, instances, field=None, message=None):
        if message == None:
            message = "Database records conflict with each other"
        if field:
            message += ", field: %s" % field
        message += ", instances: %s" % repr(instances)
        self.message = message

class DbExternalConflict(Error):
    """
    Exception raised when incoming datastream inconsistent with a DB record.

    Attributes:
        dbInstance -- version of instance in DB
        memoryInstance -- version of instance in program
        field -- if known, the field that caused the conflict
        message -- explanation of the error
s   """
    def __init__(self, dbInstance, memoryInstance, field=None, message=None):
        if message == None:
            message = "Incoming data conflicts with database"
        if field:
            message += ", field: %s" % field
        message += ", dbInstance: %s" % repr(dbInstance)
        message += ", memoryInstance: %s" % repr(memoryInstance)
        self.message = message
