def generate_secret(length=128):
    chars = "0123456789"\
        "abcdefghijklmnopqrstuvwxyz"\
        "ABCDEFGHIJKLMNOPQRSTUVWXYZ"\
        ".,_-+*@:;[](){}~!?|<>=/\&$#"
    return "".join(random.choice(chars) for _ in xrange(length)) def properties_dict(sa_object):
    """
    Returns a dictionary of column-properties for the given SQLAlchemy
    mapped object.
    """
    mapper = object_mapper(sa_object)
    return dict((prop.key, getattr(sa_object, prop.key))
                for prop in mapper.iterate_properties
                if isinstance(prop, ColumnProperty)) def column_properties(sa_variant):
    "Returns a list of column-properties."
    mapper = class_mapper(sa_variant) if inspect.isclass(sa_variant) \
        else object_mapper(sa_variant)
    return [prop.key for prop in mapper.iterate_properties
            if isinstance(prop, ColumnProperty)] def get_pk(sa_variant):
    "Returns the primary key name for the given mapped class or object."
    mapper = class_mapper(sa_variant) if inspect.isclass(sa_variant) \
        else object_mapper(sa_variant)
    return mapper.primary_key[0].key def query_model(session, sa_class, only_pk=False):
    """
    Returns a query for *sa_class* that doesn't load any relationship
    attribute.
    """
    opts = (noload('*'),)
    if only_pk:
        pk = get_pk(sa_class)
        opts += tuple(
            defer(prop.key)
            for prop in class_mapper(sa_class).iterate_properties
            if isinstance(prop, ColumnProperty)
            if prop.key != pk)
    return session.query(sa_class).options(*opts) class EventRegister(object):

    def __init__(self):
        self._listeners = []

    def __iter__(self):
        for listener in self._listeners:
            yield listener

    def listen(self, listener):
        "Register a listener. May be used as a decorator."
        assert inspect.isroutine(listener), "invalid listener"
        if listener not in self._listeners:
            self._listeners.append(listener)
        return listener MAX_SQL_VARIABLES = 900
INTERNAL_SESSION_ATTR = '_dbsync_internal'
def Session():
def session_closing(fn):
    def wrapped(*args, **kwargs):
def session_committing(fn):
    def wrapped(*args, **kwargs):
def committing_context():
def set_engine(engine):
def get_engine():
    def __setattr__(self, *args):
    def __init__(self, model=None, id=None):
def install(self, model):
def tracked_model(operation):
def extend(model, fieldname, fieldtype, loadfn, savefn, deletefn=None):
def _has_extensions(obj):
def _has_delete_functions(obj):
def save_extensions(obj):
def delete_extensions(old_obj, new_obj):
def toggle_listening(enabled=None):
def with_listening(enabled):
    def wrapper(proc):
        def wrapped(*args, **kwargs):
def _track_added(fn, added):
    def tracked(o, **kws):
def _track_deleted(fn, deleted, session, always=False):
    def tracked(o, **kws):
def with_transaction(include_extensions=True):
    def wrapper(proc):
        def wrapped(*args, **kwargs):
def make_content_type_id(model):
def generate_content_types(session=None):
def is_synched(obj, session=None):
def get_latest_version_id(session=None):
class ConfigurationError(Exception): pass
class tracked_record(object): class Version(Base):
    """
    A database version.

    These are added for each 'push' accepted and executed without
    problems.
    """

    __tablename__ = "versions"

    version_id = Column(Integer, primary_key=True)
    node_id = Column(Integer, ForeignKey(Node.__tablename__ + ".node_id"))
    created = Column(DateTime)

    node = relationship(Node)

    def __repr__(self):
        return u"<Version version_id: {0}, created: {1}>".\
            format(self.version_id, self.created) class Node(Base):
    """
    A node registry.

    A node is a client application installed somewhere else.
    """

    __tablename__ = "nodes"

    node_id = Column(Integer, primary_key=True)
    registered = Column(DateTime)
    registry_user_id = Column(Integer)
    secret = Column(String(128))

    def __init__(self, *args, **kwargs):
        super(Node, self).__init__(*args, **kwargs)

    def __repr__(self):
        return u"<Node node_id: {0}, registered: {1}, "\
            u"registry_user_id: {2}, secret: {3}>".\
            format(self.node_id,
                   self.registered,
                   self.registry_user_id,
                   self.secret) class OperationError(Exception): pass class Operation(Base):
    """
    A database operation (insert, delete or update).

    The operations are grouped in versions and ordered as they are
    executed.
    """

    __tablename__ = "operations"

    row_id = Column(Integer)
    version_id = Column(
        Integer,
        ForeignKey(Version.__tablename__ + ".version_id"),
        nullable=True)
    content_type_id = Column(BigInteger)
    tracked_model = None # to be injected
    command = Column(String(1))
    command_options = ('i', 'u', 'd')
    order = Column(Integer, primary_key=True)

    version = relationship(Version, backref=backref("operations", lazy="joined"))

    @validates('command')
    def validate_command(self, key, command):
        assert command in self.command_options
        return command

    def __repr__(self):
        return u"<Operation row_id: {0}, model: {1}, command: {2}>".\
            format(self.row_id, self.tracked_model, self.command)

    def references(self, obj):
        "Whether this operation references the given object or not."
        if self.row_id != getattr(obj, get_pk(obj), None):
            return False
        model = self.tracked_model
        if model is None:
            return False # operation doesn't even refer to a tracked model
        return model is type(obj)

    def perform(operation, container, session, node_id=None):
        """
        Performs *operation*, looking for required data in
        *container*, and using *session* to perform it.

        *container* is an instance of
        dbsync.messages.base.BaseMessage.

        *node_id* is the node responsible for the operation, if known
        (else ``None``).

        If at any moment this operation fails for predictable causes,
        it will raise an *OperationError*.
        """
        model = operation.tracked_model
        if model is None:
            raise OperationError("no content type for this operation", operation)

        if operation.command == 'i':
            obj = query_model(session, model).\
                filter(getattr(model, get_pk(model)) == operation.row_id).first()
            pull_obj = container.query(model).\
                filter(attr('__pk__') == operation.row_id).first()
            if pull_obj is None:
                raise OperationError(
                    "no object backing the operation in container", operation)
            if obj is None:
                session.add(pull_obj)
            else:
                # Don't raise an exception if the incoming object is
                # exactly the same as the local one.
                if properties_dict(obj) == properties_dict(pull_obj):
                    logger.warning(u"insert attempted when an identical object "
                                   u"already existed in local database: "
                                   u"model {0} pk {1}".format(model.__name__,
                                                              operation.row_id))
                else:
                    raise OperationError(
                        u"insert attempted when the object already existed: "
                        u"model {0} pk {1}".format(model.__name__,
                                                   operation.row_id))

        elif operation.command == 'u':
            obj = query_model(session, model).\
                filter(getattr(model, get_pk(model)) == operation.row_id).first()
            if obj is None:
                # For now, the record will be created again, but is an
                # error because nothing should be deleted without
                # using dbsync
                # raise OperationError(
                #     "the referenced object doesn't exist in database", operation)
                logger.warning(
                    u"The referenced object doesn't exist in database. "
                    u"Node %s. Operation %s",
                    node_id,
                    operation)

            pull_obj = container.query(model).\
                filter(attr('__pk__') == operation.row_id).first()
            if pull_obj is None:
                raise OperationError(
                    "no object backing the operation in container", operation)
            session.merge(pull_obj)

        elif operation.command == 'd':
            obj = query_model(session, model, only_pk=True).\
                filter(getattr(model, get_pk(model)) == operation.row_id).first()
            if obj is None:
                # The object is already deleted in the server
                # The final state in node and server are the same. But
                # it's an error because nothing should be deleted
                # without using dbsync
                logger.warning(
                    "The referenced object doesn't exist in database. "
                    u"Node %s. Operation %s",
                    node_id,
                    operation)
            else:
                session.delete(obj)

        else:
            raise OperationError(
                "the operation doesn't specify a valid command ('i', 'u', 'd')",
                operation) class BaseMessage(object):
    "The base type for messages with a payload."

    #: dictionary of (model name, set of wrapped objects)
    payload = None

    def __init__(self, raw_data=None):
        self.payload = {}
        if raw_data is not None:
            self._from_raw(raw_data)

    def _from_raw(self, data):
        getm = lambda k: synched_models.model_names.get(k, null_model).model
        for k, v, m in ifilter(lambda (k, v, m): m is not None,
                               imap(lambda (k, v): (k, v, getm(k)),
                                    data['payload'].iteritems())):
            self.payload[k] = set(
                map(lambda dict_: ObjectType(k, dict_[get_pk(m)], **dict_),
                    imap(decode_dict(m), v)))

    def query(self, model):
        "Returns a query object for this message."
        return MessageQuery(model, self.payload)

    def to_json(self):
        "Returns a JSON-friendly python dictionary."
        encoded = {}
        encoded['payload'] = {}
        for k, objects in self.payload.iteritems():
            model = synched_models.model_names.get(k, null_model).model
            if model is not None:
                encoded['payload'][k] = map(encode_dict(model),
                                            imap(method('to_dict'), objects))
        return encoded

    def add_object(self, obj, include_extensions=True):
        "Adds an object to the message, if it's not already in."
        class_ = type(obj)
        classname = class_.__name__
        obj_set = self.payload.get(classname, set())
        if ObjectType(classname, getattr(obj, get_pk(class_))) in obj_set:
            return self
        properties = properties_dict(obj)
        if include_extensions:
            for field, ext in model_extensions.get(classname, {}).iteritems():
                _, loadfn, _, _ = ext
                properties[field] = loadfn(obj)
        obj_set.add(ObjectType(
                classname, getattr(obj, get_pk(class_)), **properties))
        self.payload[classname] = obj_set
        return self class RegisterMessage(object):
    """A register message with node information."""

    #: The node to be registered in the client application
    node = None

    def __init__(self, raw_data=None):
        if raw_data is not None:
            self._build_from_raw(raw_data)

    def _build_from_raw(self, data):
        self.node = object_from_dict(Node, decode_dict(Node)(data['node']))

    def to_json(self):
        encoded = {}
        encoded['node'] = None
        if self.node is not None:
            encoded['node'] = encode_dict(Node)(properties_dict(self.node))
        return encoded class PullMessage(BaseMessage):
    """
    A pull message.

    A pull message can be queried over by version, operation or model,
    and can be filtered multiple times.

    It can be instantiated from a raw data dictionary, or can be made
    empty and filled later with specific methods (``add_version``,
    ``add_operation``, ``add_object``).
    """

    #: Datetime of creation.
    created = None

    #: List of operations to perform in the node.
    operations = None

    #: List of versions being pulled.
    versions = None

    def __init__(self, raw_data=None):
        """
        *raw_data* must be a python dictionary, normally the
        product of JSON decoding. If not given, the message will be
        empty and should be filled with the appropriate methods
        (add_*).
        """
        super(PullMessage, self).__init__(raw_data)
        if raw_data is not None:
            self._build_from_raw(raw_data)
        else:
            self.created = datetime.datetime.now()
            self.operations = []
            self.versions = []

    def _build_from_raw(self, data):
        self.created = decode(types.DateTime())(data['created'])
        self.operations = map(partial(object_from_dict, Operation),
                              imap(decode_dict(Operation), data['operations']))
        self.versions = map(partial(object_from_dict, Version),
                            imap(decode_dict(Version), data['versions']))

    def query(self, model):
        "Returns a query object for this message."
        return MessageQuery(
            model,
            dict(self.payload, **{
                    'models.Operation': self.operations,
                    'models.Version': self.versions}))

    def to_json(self):
        """
        Returns a JSON-friendly python dictionary. Structure::

            created: datetime,
            operations: list of operations,
            versions: list of versions,
            payload: dictionary with lists of objects mapped to model names
        """
        encoded = super(PullMessage, self).to_json()
        encoded['created'] = encode(types.DateTime())(self.created)
        encoded['operations'] = map(encode_dict(Operation),
                                    imap(properties_dict, self.operations))
        encoded['versions'] = map(encode_dict(Version),
                                  imap(properties_dict, self.versions))
        return encoded

    @session_closing
    def add_operation(self, op, swell=True, session=None):
        """
        Adds an operation to the message, including the required
        object if it's possible to include it.

        If *swell* is given and set to ``False``, the operation and
        object will be added bare, without parent objects. Otherwise,
        the parent objects will be added to aid in conflict
        resolution.

        A delete operation doesn't include the associated object. If
        *session* is given, the procedure won't instantiate a new
        session.

        This operation might fail, (due to database inconsitency) in
        which case the internal state of the message won't be affected
        (i.e. it won't end in an inconsistent state).

        DEPRECATED in favor of `fill_for`
        """
        model = op.tracked_model
        if model is None:
            raise ValueError("operation linked to model %s "\
                                 "which isn't being tracked" % model)
        if model not in pulled_models:
            return self
        obj = query_model(session, model).\
            filter_by(**{get_pk(model): op.row_id}).first() \
            if op.command != 'd' else None
        self.operations.append(op)
        # if the object isn't there it's because the operation is old,
        # and should be able to be compressed out when performing the
        # conflict resolution phase
        if obj is not None:
            self.add_object(obj)
            if swell:
                # add parent objects to resolve possible conflicts in merge
                for parent in parent_objects(obj, synched_models.models.keys(),
                                             session):
                    self.add_object(parent)
        return self

    @session_closing
    def add_version(self, v, swell=True, session=None):
        """
        Adds a version to the message, and all associated
        operations and objects.

        This method will either fail and leave the message instance as
        if nothing had happened, or it will succeed and return the
        modified message.

        DEPRECATED in favor of `fill_for`
        """
        for op in v.operations:
            if op.content_type_id not in synched_models.ids:
                raise ValueError("version includes operation linked "\
                                 "to model not currently being tracked", op)
        self.versions.append(v)
        for op in v.operations:
            self.add_operation(op, swell=swell, session=session)
        return self

    @session_closing
    def fill_for(self, request, swell=False, include_extensions=True,
                 session=None):
        """
        Fills this pull message (response) with versions, operations
        and objects, for the given request (PullRequestMessage).

        The *swell* parameter is deprecated and considered ``True``
        regardless of the value given. This means that parent objects
        will always be added to the message.

        *include_extensions* dictates whether the pull message will
        include model extensions or not.
        """
        assert isinstance(request, PullRequestMessage), "invalid request"
        versions = session.query(Version)
        if request.latest_version_id is not None:
            versions = versions.\
                filter(Version.version_id > request.latest_version_id)
        required_objects = {}
        required_parents = {}
        for v in versions:
            self.versions.append(v)
            for op in v.operations:
                model = op.tracked_model
                if model is None:
                    raise ValueError("operation linked to model %s "\
                                         "which isn't being tracked" % model)
                if model not in pulled_models: continue
                self.operations.append(op)
                if op.command != 'd':
                    pks = required_objects.get(model, set())
                    pks.add(op.row_id)
                    required_objects[model] = pks

        for model, pks in ((m, batch)
                           for m, pks in required_objects.iteritems()
                           for batch in grouper(pks, MAX_SQL_VARIABLES)):
            for obj in query_model(session, model).filter(
                    getattr(model, get_pk(model)).in_(list(pks))).all():
                self.add_object(obj, include_extensions=include_extensions)
                # add parent objects to resolve conflicts in merge
                for pmodel, ppk in parent_references(obj,
                                                     synched_models.models.keys()):
                    parent_pks = required_parents.get(pmodel, set())
                    parent_pks.add(ppk)
                    required_parents[pmodel] = parent_pks

        for pmodel, ppks in ((m, batch)
                             for m, pks in required_parents.iteritems()
                             for batch in grouper(pks, MAX_SQL_VARIABLES)):
            for parent in query_model(session, pmodel).filter(
                    getattr(pmodel, get_pk(pmodel)).in_(list(ppks))).all():
                self.add_object(parent, include_extensions=include_extensions)
        return self class PullRequestMessage(BaseMessage):
    """
    A pull request message.

    The message includes information for the server to decide whether
    it should send back related (parent) objects to those directly
    involved, for use in conflict resolution, or not. This is used to
    allow for thinner PullMessage(s) to be built, through the
    add_version and add_operation methods.
    """

    #: List of operation the node has performed since the last
    #  synchronization. If empty, the pull is a full 'fast-forward'
    #  thin procedure.
    operations = None

    #: The identifier used to select the operations to be included in
    #  the pull response.
    latest_version_id = None

    def __init__(self, raw_data=None):
        """
        *raw_data* must be a python dictionary. If not given, the
        message should be filled with the or
        add_unversioned_operations method.
        """
        super(PullRequestMessage, self).__init__(raw_data)
        if raw_data is not None:
            self._build_from_raw(raw_data)
        else:
            self.latest_version_id = get_latest_version_id()
            self.operations = []

    def _build_from_raw(self, data):
        self.operations = map(partial(object_from_dict, Operation),
                              imap(decode_dict(Operation), data['operations']))
        self.latest_version_id = decode(types.Integer())(
            data['latest_version_id'])

    def query(self, model):
        "Returns a query object for this message."
        return MessageQuery(
            model,
            dict(self.payload, **{'models.Operation': self.operations}))

    def to_json(self):
        "Returns a JSON-friendly python dictionary."
        encoded = super(PullRequestMessage, self).to_json()
        encoded['operations'] = map(encode_dict(Operation),
                                    imap(properties_dict, self.operations))
        encoded['latest_version_id'] = encode(types.Integer())(
            self.latest_version_id)
        return encoded

    def add_operation(self, op):
        """
        Adds an operation to the message, including the required
        object if possible.
        """
        assert op.version_id is None, "the operation {0} is already versioned".\
            format(op)
        model = op.tracked_model
        if model is None:
            raise ValueError("operation linked to model %s "\
                                 "which isn't being tracked" % model)
        if model not in pulled_models: return self
        self.operations.append(op)
        return self

    @session_closing
    def add_unversioned_operations(self, session=None):
        """
        Adds all unversioned operations to this message and
        required objects.
        """
        operations = session.query(Operation).\
            filter(Operation.version_id == None).all()
        if any(op.content_type_id not in synched_models.ids
               for op in operations):
            raise ValueError("version includes operation linked "\
                                 "to model not currently being tracked")
        for op in operations:
            self.add_operation(op)
        return self class PushMessage(BaseMessage):
    """
    A push message.

    A push message contains the latest version information, the node
    information, and the list of unversioned operations and the
    required objects for those to be performed.

    The message can be instantiated from a raw data dictionary or can
    be made empty and filled later with the
    ``add_unversioned_operations`` method, in which case the node
    attribute and the latest version identifier should be assigned
    explicitly as well. The method ``set_node`` is required to be used
    for proper key generation.

    If the node is not assigned the message will still behave
    normally, since verification of its presence is not enforced on
    the client, and might not be enforced on the server. Likewise, if
    the latest version isn't assigned, it'll be just interpreted on
    the server to be the initial data load.

    To verify correctness, use ``islegit`` giving a session with
    access to the synch database.
    """

    #: Datetime of creation
    created = None

    #: Node primary key
    node_id = None

    #: Secret used internally to mitigate obnoxiousness.
    _secret = None

    #: Key to this message
    key = None

    #: The latest version
    latest_version_id = None

    #: List of unversioned operations
    operations = None

    def __init__(self, raw_data=None):
        """
        *raw_data* must be a python dictionary. If not given, the
        message will be empty and should be filled after
        instantiation.
        """
        super(PushMessage, self).__init__(raw_data)
        if raw_data is not None:
            self._build_from_raw(raw_data)
        else:
            self.created = datetime.datetime.now()
            self.operations = []

    def _build_from_raw(self, data):
        self.created = decode(types.DateTime())(data['created'])
        self.node_id = decode(types.Integer())(data['node_id'])
        self.key = decode(types.String())(data['key'])
        self.latest_version_id = decode(types.Integer())(
            data['latest_version_id'])
        self.operations = map(partial(object_from_dict, Operation),
                              imap(decode_dict(Operation), data['operations']))

    def query(self, model):
        "Returns a query object for this message."
        return MessageQuery(
            model,
            dict(
                self.payload,
                **{'models.Operation': self.operations}))

    def to_json(self):
        """
        Returns a JSON-friendly python dictionary. Structure::

            created: datetime,
            node_id: node primary key or null,
            key: a string generated from the secret and part of the message,
            latest_version_id: number or null,
            operations: list of operations,
            payload: dictionay with lists of objects mapped to model names
        """
        encoded = super(PushMessage, self).to_json()
        encoded['created'] = encode(types.DateTime())(self.created)
        encoded['node_id'] = encode(types.Integer())(self.node_id)
        encoded['key'] = encode(types.String())(self.key)
        encoded['latest_version_id'] = encode(types.Integer())(
            self.latest_version_id)
        encoded['operations'] = map(encode_dict(Operation),
                                    imap(properties_dict, self.operations))
        return encoded

    def _portion(self):
        "Returns part of this message as a string."
        portion = "".join("&{0}#{1}#{2}".\
                              format(op.row_id, op.content_type_id, op.command)
                          for op in self.operations)
        return portion

    def _sign(self):
        if self._secret is not None:
            self.key = hashlib.sha512(self._secret + self._portion()).hexdigest()

    def set_node(self, node):
        "Sets the node and key for this message."
        if node is None: return
        self.node_id = node.node_id
        self._secret = node.secret
        self._sign()

    def islegit(self, session):
        "Checks whether the key for this message is proper."
        if self.key is None or self.node_id is None: return False
        node = session.query(Node).filter(Node.node_id == self.node_id).first()
        return node is not None and \
            self.key == hashlib.sha512(node.secret + self._portion()).hexdigest()

    @session_closing
    def add_unversioned_operations(self, session=None, include_extensions=True):
        """
        Adds all unversioned operations to this message, including the
        required objects for them to be performed.
        """
        operations = session.query(Operation).\
            filter(Operation.version_id == None).all()
        if any(op.content_type_id not in synched_models.ids
               for op in operations):
            raise ValueError("version includes operation linked "\
                                 "to model not currently being tracked")
        required_objects = {}
        for op in operations:
            model = op.tracked_model
            if model not in pushed_models: continue
            self.operations.append(op)
            if op.command != 'd':
                pks = required_objects.get(model, set())
                pks.add(op.row_id)
                required_objects[model] = pks
        for model, pks in ((m, batch)
                           for m, pks in required_objects.iteritems()
                           for batch in grouper(pks, MAX_SQL_VARIABLES)):
            for obj in query_model(session, model).filter(
                    getattr(model, get_pk(model)).in_(list(pks))).all():
                self.add_object(obj, include_extensions=include_extensions)
        if self.key is not None:
            # overwrite since it's probably an incorrect key
            self._sign()
        return self def find_unique_conflicts(push_message, session):
    """
    Returns a list of conflicts caused by unique constraints in the
    given push message contrasted against the database. Each conflict
    is a dictionary with the following fields::

        object: the conflicting object in database, bound to the
                session
        columns: tuple of column names in the unique constraint
        new_values: tuple of values that can be used to update the
                    conflicting object.
    """
    conflicts = []

    for pk, model in ((op.row_id, op.tracked_model)
                      for op in push_message.operations
                      if op.command != 'd'):
        if model is None: continue

        for constraint in ifilter(lambda c: isinstance(c, UniqueConstraint),
                                  class_mapper(model).mapped_table.constraints):

            unique_columns = tuple(col.name for col in constraint.columns)
            remote_obj = push_message.query(model).\
                filter(attr('__pk__') == pk).first()
            remote_values = tuple(getattr(remote_obj, col, None)
                                  for col in unique_columns)

            if all(value is None for value in remote_values): continue
            local_obj = query_model(session, model).\
                filter_by(**dict(izip(unique_columns, remote_values))).first()
            if local_obj is None: continue
            local_pk = getattr(local_obj, get_pk(model))
            if local_pk == pk: continue

            push_obj = push_message.query(model).\
                filter(attr('__pk__') == local_pk).first()
            if push_obj is None: continue # push will fail

            conflicts.append(
                {'object': local_obj,
                 'columns': unique_columns,
                 'new_values': tuple(getattr(push_obj, col)
                                     for col in unique_columns)})

    return conflicts def get_logger(name):
    logger = logging.getLogger(name)
    logger.setLevel(logging.WARNING)
    loggers.add(logger)
    if log_handler is not None:
        logger.addHandler(log_handler)
    return logger
import datetime
from sqlalchemy.orm import make_transient
from dbsync.lang import *
from dbsync.utils import (
    generate_secret,
    properties_dict,
    column_properties,
    get_pk,
    query_model,
    EventRegister)
from dbsync import core
from dbsync.models import (
    Version,
    Node,
    OperationError,
    Operation)
from dbsync.messages.base import BaseMessage
from dbsync.messages.register import RegisterMessage
from dbsync.messages.pull import PullMessage, PullRequestMessage
from dbsync.messages.push import PushMessage
from dbsync.server.conflicts import find_unique_conflicts
from dbsync.logs import get_logger
"""
Registry, pull, push and other request handlers.

The pull cycle consists in receiving a version identifier and sending
back a PullMessage filled with versions above the one received.

The push cycle consists in receiving a complete PushMessage and either
rejecting it based on latest version or signature, or accepting it and
performing the operations indicated in it. The operations should also
be inserted in the operations table, in the correct order but getting
new keys for the 'order' column, and linked with a newly created
version. If it accepts the message, the push handler should also
return the new version identifier to the node (and the programmer is
tasked to send the HTTP response).
"""





logger = get_logger(__name__)


@core.session_closing
def handle_query(data, session=None):
    "Responds to a query request."
    model = core.synched_models.model_names.\
        get(data.get('model', None), core.null_model).model
    if model is None: return None
    mname = model.__name__
    filters = dict((k, v) for k, v in ((k[len(mname) + 1:], v)
                                       for k, v in data.iteritems()
                                       if k.startswith(mname + '_'))
                   if k and k in column_properties(model))
    message = BaseMessage()
    q = query_model(session, model)
    if filters:
        q = q.filter_by(**filters)
    for obj in q:
        message.add_object(obj)
    return message.to_json()


@core.session_closing
def handle_repair(data=None, session=None):
    "Handle repair request. Return whole server database."
    include_extensions = 'exclude_extensions' not in (data or {})
    latest_version_id = core.get_latest_version_id(session=session)
    message = BaseMessage()
    for model in core.synched_models.models.iterkeys():

        for obj in query_model(session, model):