Exemple #1
0
 def __init__(self, manager):
     self.manager = manager
     self._new = OrderedDict()
     self._deleted = OrderedDict()
     self._delete_query = []
     self._modified = OrderedDict()
     self._queries = []
     self._structures = set()
Exemple #2
0
 def __init__(self, backend, autocommit = False, query_class = None):
     self.backend = getdb(backend)
     self.transaction = None
     self.autocommit = autocommit
     self._new = OrderedDict()
     self._deleted = OrderedDict()
     self._modified = OrderedDict()
     self._loaded = {}
     self.query_class = query_class or Query
Exemple #3
0
 def __init__(self, meta, session):
     self.meta = meta
     self.session = session
     self.backend.setup_model(meta)
     self._new = OrderedDict()
     self._deleted = OrderedDict()
     self._delete_query = []
     self._modified = OrderedDict()
     self._loaded = {}
Exemple #4
0
 def __init__(self, manager):
     self.manager = manager
     self._new = OrderedDict()
     self._deleted = OrderedDict()
     self._delete_query = []
     self._modified = OrderedDict()
     self._queries = []
     self._structures = set()
Exemple #5
0
def get_fields(bases, attrs):
    #
    fields = []
    for name, field in list(attrs.items()):
        if isinstance(field, Field):
            fields.append((name, attrs.pop(name)))
    #
    fields = sorted(fields, key=lambda x: x[1].creation_counter)
    #
    for base in bases:
        if hasattr(base, '_meta'):
            fields = list(
                (name, deepcopy(field))
                for name, field in base._meta.dfields.items()) + fields
    #
    return OrderedDict(fields)
Exemple #6
0
 def __init__(self, router):
     self.transaction = None
     self._models = OrderedDict()
     self._router = router
Exemple #7
0
class Session(object):
    '''The middleware for persistent operations on the back-end.

        It is created via the :meth:`Router.session` method.

    .. attribute:: transaction

        A :class:`Transaction` instance. Not ``None`` if this :class:`Session`
        is in a :ref:`transactional state <transactional-state>`

    .. attribute:: router

        Instance of the :class:`Router` which created this :class:`Session`.
    '''
    def __init__(self, router):
        self.transaction = None
        self._models = OrderedDict()
        self._router = router

    def __str__(self):
        return str(self._router)

    def __repr__(self):
        return '%s: %s' % (self.__class__.__name__, self._router)

    def __iter__(self):
        for sm in self._models.values():
            yield sm

    def __len__(self):
        return len(self._models)

    @property
    def router(self):
        return self._router

    @property
    def dirty(self):
        '''The set of instances in this :class:`Session` which have
been modified.'''
        return frozenset(chain(*tuple((sm.dirty for sm
                                       in itervalues(self._models)))))

    def begin(self, **options):
        '''Begin a new :class:`Transaction`. If this :class:`Session`
is already in a :ref:`transactional state <transactional-state>`,
an error will occur. It returns the :attr:`transaction` attribute.

This method is mostly used within a ``with`` statement block::

    with session.begin() as t:
        t.add(...)
        ...

which is equivalent to::

    t = session.begin()
    t.add(...)
    ...
    session.commit()

``options`` parameters are passed to the :class:`Transaction` constructor.
'''
        if self.transaction is not None:
            raise InvalidTransaction("A transaction is already begun.")
        else:
            self.transaction = Transaction(self, **options)
        return self.transaction

    def commit(self):
        """Commit the current :attr:`transaction`.

        If no transaction is in progress, this method open one.
        Rarely used directly, see the :meth:`begin` method for details on
        how to start and close a transaction using the `with` construct.
        """
        if self.transaction is None:
            self.begin()
        return self.transaction.commit()

    def query(self, model, **kwargs):
        '''Create a new :class:`Query` for *model*.'''
        sm = self.model(model)
        query_class = sm.manager.query_class or Query
        return query_class(sm._meta, self, **kwargs)

    def empty(self, model):
        '''Returns an empty :class:`Query` for ``model``.'''
        return EmptyQuery(self.manager(model)._meta, self)

    def update_or_create(self, model, **kwargs):
        '''Update or create a new instance of ``model``.

        This method can raise an exception if the ``kwargs`` dictionary
        contains field data that does not validate.

        :param model: a :class:`StdModel`
        :param kwargs: dictionary of parameters.
        :returns: A two elements tuple containing the instance and a boolean
            indicating if the instance was created or not.
        '''
        backend = self.model(model).backend
        return backend.execute(self._update_or_create(model, **kwargs))

    def add(self, instance, modified=True, **params):
        '''Add an ``instance`` to the session.

        If the session is not in a
        :ref:`transactional state <transactional-state>`, this operation
        commits changes to the back-end server immediately.

        :parameter instance: a :class:`Model` instance. It must be registered
            with the :attr:`router` which created this :class:`Session`.
        :parameter modified: a boolean flag indicating if the instance was
            modified.
        :return: the ``instance``.

        If the instance is persistent (it is already stored in the database),
        an updated will be performed, otherwise a new entry will be created
        once the :meth:`commit` method is invoked.
        '''
        sm = self.model(instance)
        instance.session = self
        o = sm.add(instance, modified=modified, **params)
        if modified and not self.transaction:
            transaction = self.begin()
            return transaction.commit(lambda: o)
        else:
            return o

    def delete(self, instance_or_query):
        '''Delete an ``instance`` or a ``query``.

        Adds ``instance_or_query`` to this :class:`Session` list
        of data to be deleted. If the session is not in a
        :ref:`transactional state <transactional-state>`, this operation
        commits changes to the backend server immediately.

        :parameter instance_or_query: a :class:`Model` instance or
            a :class:`Query`.
        '''
        sm = self.model(instance_or_query)
        # not an instance of a Model. Assume it is a query.
        if is_query(instance_or_query):
            if instance_or_query.session is not self:
                raise ValueError('Adding a query generated by another session')
            sm._delete_query.append(instance_or_query)
        else:
            instance_or_query = sm.delete(instance_or_query, self)
        if not self.transaction:
            transaction = self.begin()
            return transaction.commit(
                lambda: transaction.deleted.get(sm._meta))
        else:
            return instance_or_query

    def flush(self, model):
        '''Completely flush a :class:`Model` from the database. No keys
associated with the model will exists after this operation.'''
        return self.model(model).flush()

    def clean(self, model):
        '''Remove empty keys for a :class:`Model` from the database. No
empty keys associated with the model will exists after this operation.'''
        return self.model(model).clean()

    def keys(self, model):
        '''Retrieve all keys for a *model*.'''
        return self.model(model).keys()

    def __contains__(self, instance):
        sm = self.model(instance, False)
        return instance in sm if sm is not None else False

    def model(self, model, create=True):
        '''Returns the :class:`SessionModel` for ``model`` which
can be :class:`Model`, or a :class:`MetaClass`, or an instance
of :class:`Model`.'''
        manager = self.manager(model)
        sm = self._models.get(manager)
        if sm is None and create:
            sm = SessionModel(manager)
            self._models[manager] = sm
        return sm

    def expunge(self, instance=None):
        '''Remove ``instance`` from this :class:`Session`. If ``instance``
is not given, it removes all instances from this :class:`Session`.'''
        if instance is not None:
            sm = self._models.get(instance._meta)
            if sm:
                return sm.expunge(instance)
        else:
            self._models.clear()

    def manager(self, model):
        '''Retrieve the :class:`Manager` for ``model`` which can be any of the
values valid for the :meth:`model` method.'''
        try:
            return self.router[model]
        except KeyError:
            meta = getattr(model, '_meta', model)
            if meta.type == 'structure':
                # this is a structure
                if hasattr(model, 'model'):
                    structure_model = model.model
                    if structure_model:
                        return self.manager(structure_model)
                    else:
                        manager = self.router.structure(model)
                        if manager:
                            return manager
            raise InvalidTransaction('"%s" not valid in this session' % meta)

    def backends_data(self):
        backends = {}
        for sm in self:
            for backend, data in sm.backends_data(self):
                be = backends.get(backend)
                if be is None:
                    backends[backend] = be = []
                be.append(data)
        return backends.items()

    #######################################################################
    #    INTERNALS
    def _update_or_create(self, model, **kwargs):
        pkname = model._meta.pkname()
        pk = kwargs.pop(pkname, None)
        query = self.query(model)
        item = None
        #
        if pk:
            # primary key available
            items = yield query.filter(pkname=pk).all()
            if items:
                item = items[0]
            else:
                kwargs[pkname] = pk
        else:
            params = {}
            rest = {}
            fields = model._meta.dfields
            for field, value in iteritems(kwargs):
                if field in fields and fields[field].index:
                    params[field] = value
                else:
                    rest[field] = value
            if params:
                items = yield query.filter(**params).all()
                if len(items) == 1:
                    item = items[0]
                    kwargs = rest
        if item:
            if kwargs:
                for field, value in iteritems(kwargs):
                    setattr(item, field, value)
                item = yield self.add(item)
            yield item, False
        #
        else:
            item = yield self.add(model(**kwargs))
            yield item, True
Exemple #8
0
class SessionModel(object):
    '''A :class:`SessionModel` is the container of all objects for a given
:class:`Model` in a stdnet :class:`Session`.'''
    def __init__(self, manager):
        self.manager = manager
        self._new = OrderedDict()
        self._deleted = OrderedDict()
        self._delete_query = []
        self._modified = OrderedDict()
        self._queries = []
        self._structures = set()

    def __len__(self):
        return (len(self._new) + len(self._modified) + len(self._deleted) +
                len(self.commit_structures) + len(self.delete_structures))

    def __repr__(self):
        return self._meta.__repr__()
    __str__ = __repr__

    @property
    def backend(self):
        '''The backend for this :class:`SessionModel`.'''
        return self.manager.backend

    @property
    def read_backend(self):
        '''The read-only backend for this :class:`SessionModel`.'''
        return self.manager.read_backend

    @property
    def model(self):
        '''The :class:`Model` for this :class:`SessionModel`.'''
        return self.manager.model

    @property
    def _meta(self):
        '''The :class:`Metaclass` for this :class:`SessionModel`.'''
        return self.manager._meta

    @property
    def new(self):
        '''The set of all new instances within this ``SessionModel``. This
instances will be inserted in the database.'''
        return tuple(itervalues(self._new))

    @property
    def modified(self):
        '''The set of all modified instances within this ``Session``. This
instances will.'''
        return tuple(itervalues(self._modified))

    @property
    def deleted(self):
        '''The set of all instance pks marked as `deleted` within this
:class:`Session`.'''
        return tuple((p.pkvalue() for p in itervalues(self._deleted)))

    @property
    def dirty(self):
        '''The set of all instances which have changed, but not deleted,
within this :class:`SessionModel`.'''
        return tuple(self.iterdirty())

    def iterdirty(self):
        '''Ordered iterator over dirty elements.'''
        return iter(chain(itervalues(self._new), itervalues(self._modified)))

    def __contains__(self, instance):
        iid = instance.get_state().iid
        return (iid in self._new or
                iid in self._modified or
                iid in self._deleted or
                instance in self._structures)

    def add(self, instance, modified=True, persistent=None,
            force_update=False):
        '''Add a new instance to this :class:`SessionModel`.

:param modified: Optional flag indicating if the ``instance`` has been
    modified. By default its value is ``True``.
:param force_update: if ``instance`` is persistent, it forces an update of the
    data rather than a full replacement. This is used by the
    :meth:`insert_update_replace` method.
:rtype: The instance added to the session'''
        if instance._meta.type == 'structure':
            return self._add_structure(instance)
        state = instance.get_state()
        if state.deleted:
            raise ValueError('State is deleted. Cannot add.')
        self.pop(state.iid)
        pers = persistent if persistent is not None else state.persistent
        pkname = instance._meta.pkname()
        if not pers:
            instance._dbdata.pop(pkname, None)  # to make sure it is add action
            state = instance.get_state(iid=None)
        elif persistent:
            instance._dbdata[pkname] = instance.pkvalue()
            state = instance.get_state(iid=instance.pkvalue())
        else:
            action = 'update' if force_update else None
            state = instance.get_state(action=action, iid=state.iid)
        iid = state.iid
        if state.persistent:
            if modified:
                self._modified[iid] = instance
        else:
            self._new[iid] = instance
        return instance

    def delete(self, instance, session):
        '''delete an *instance*'''
        if instance._meta.type == 'structure':
            return self._delete_structure(instance)
        inst = self.pop(instance)
        instance = inst if inst is not None else instance
        if instance is not None:
            state = instance.get_state()
            if state.persistent:
                state.deleted = True
                self._deleted[state.iid] = instance
                instance.session = session
            else:
                instance.session = None
            return instance

    def pop(self, instance):
        '''Remove ``instance`` from the :class:`SessionModel`. Instance
could be a :class:`Model` or an id.

:parameter instance: a :class:`Model` or an ``id``.
:rtype: the :class:`Model` removed from session or ``None`` if
    it was not in the session.
'''
        if isinstance(instance, self.model):
            iid = instance.get_state().iid
        else:
            iid = instance
        instance = None
        for d in (self._new, self._modified, self._deleted):
            if iid in d:
                inst = d.pop(iid)
                if instance is None:
                    instance = inst
                elif inst is not instance:
                    raise ValueError('Critical error: %s is duplicated' % iid)
        return instance

    def expunge(self, instance):
        '''Remove *instance* from the :class:`Session`. Instance could be a
:class:`Model` or an id.

:parameter instance: a :class:`Model` or an *id*
:rtype: the :class:`Model` removed from session or ``None`` if
    it was not in the session.
'''
        instance = self.pop(instance)
        instance.session = None
        return instance

    def post_commit(self, results):
        '''\
Process results after a commit.

:parameter results: iterator over :class:`stdnet.instance_session_result`
    items.
:rtype: a two elements tuple containing a list of instances saved and
    a list of ids of instances deleted.'''
        tpy = self._meta.pk_to_python
        instances = []
        deleted = []
        errors = []
        # The length of results must be the same as the length of
        # all committed instances
        for result in results:
            if isinstance(result, Exception):
                errors.append(result.__class__('Exception while committing %s.'
                                               ' %s' % (self._meta, result)))
                continue
            instance = self.pop(result.iid)
            id = tpy(result.id, self.backend)
            if result.deleted:
                deleted.append(id)
            else:
                if instance is None:
                    raise InvalidTransaction('{0} session received id "{1}"\
 which is not in the session.'.format(self, result.iid))
                setattr(instance, instance._meta.pkname(), id)
                instance = self.add(instance,
                                    modified=False,
                                    persistent=result.persistent)
                instance.get_state().score = result.score
                if instance.get_state().persistent:
                    instances.append(instance)
        return instances, deleted, errors

    def flush(self):
        '''Completely flush :attr:`model` from the database. No keys
associated with the model will exists after this operation.'''
        return self.backend.flush(self._meta)

    def clean(self):
        '''Remove empty keys for a :attr:`model` from the database. No
empty keys associated with the model will exists after this operation.'''
        return self.backend.clean(self._meta)

    def keys(self):
        '''Retrieve all keys for a :attr:`model`. Uses the
:attr:`Manager.read_backend`.'''
        return self.read_backend.model_keys(self._meta)

    ## INTERNALS
    def get_delete_query(self, session):
        queries = self._delete_query
        deleted = self.deleted
        if deleted:
            self._deleted.clear()
            q = self.manager.query(session)
            queries.append(q.filter(**{self._meta.pkname(): deleted}))
        if queries:
            self._delete_query = []
            q = queries[0]
            if len(queries) > 1:
                q = q.union(*queries[1:])
            return q

    def backends_data(self, session):
        transaction = session.transaction
        models = session.router
        be = self.backend
        rbe = self.read_backend
        model = self.model
        meta = model._meta
        dirty = self.dirty
        deletes = self.get_delete_query(session)
        has_delete = deletes is not None
        structures = self._structures
        queries = self._queries
        if dirty or has_delete or queries is not None or structures:
            if transaction.signal_delete and has_delete:
                models.pre_delete.fire(model, instances=deletes,
                                       session=session)
            if dirty and transaction.signal_commit:
                models.pre_commit.fire(model, instances=dirty,
                                       session=session)
            if be == rbe:
                yield be, session_data(meta, dirty, deletes, queries,
                                       structures)
            else:
                if dirty or has_delete or structures:
                    yield be, session_data(meta, dirty, deletes, (),
                                           structures)
                if queries:
                    yield rbe, session_data(meta, (), (), queries, ())

    def _add_structure(self, instance):
        instance.action = 'update'
        self._structures.add(instance)
        return instance

    def _delete_structure(self, instance):
        instance.action = 'delete'
        self._structures.add(instance)
        return instance
Exemple #9
0
class Session(object):
    '''The manager of persistent operations on the backend data server for
:class:`StdModel` classes.

.. attribute:: backend

    the :class:`stdnet.BackendDataServer` instance
    
.. attribute:: autocommit

    When ``True``, the :class:`Session`` does not keep a persistent transaction
    running, and will acquire connections from the engine on an as-needed basis,
    returning them immediately after their use.
    Flushes will begin and commit (or possibly rollback) their own transaction
    if no transaction is present. When using this mode, the :meth:`begin`
    method may be used to begin a transaction explicitly.
          
    Default: ``False``. 
          
.. attribute:: transaction

    A :class:`Transaction` instance. Not ``None`` if this :class:`Session`
    is in a transactional state.
    
.. attribute:: query_class

    class for querying. Default is :class:`Query`.
'''
    _structures = {}
    def __init__(self, backend, autocommit = False, query_class = None):
        self.backend = getdb(backend)
        self.transaction = None
        self.autocommit = autocommit
        self._new = OrderedDict()
        self._deleted = OrderedDict()
        self._modified = OrderedDict()
        self._loaded = {}
        self.query_class = query_class or Query
    
    @property
    def new(self):
        "The set of all new instances within this ``Session``"
        return frozenset(self._new.values())
    
    @property
    def modified(self):
        "The set of all modified instances within this ``Session``"
        return frozenset(self._modified.values())
    
    @property
    def deleted(self):
        "The set of all instances marked as 'deleted' within this ``Session``"
        return frozenset(self._loaded.values())
    
    @property
    def deleted(self):
        "The set of all instances marked as 'deleted' within this ``Session``"
        return frozenset(self._deleted.values())
    
    def __str__(self):
        return str(self.backend)
    
    def __repr__(self):
        return '{0}({1})'.format(self.__class__.__name__,self)
    
    def begin(self):
        '''Begin a new class:`Transaction`.
If this :class:`Session` is already within a transaction, an error is raised.'''
        if self.transaction is not None:
            raise InvalidTransaction("A transaction is already begun.")
        else:
            self.transaction = Transaction(self)
        return self.transaction
    
    def query(self, model):
        '''Create a new :class:`Query` for *model*.'''
        return self.query_class(model._meta, self)
    
    def get_or_create(self, model, **kwargs):
        '''Get an instance of *model* from the internal cache (only if the
dictionary *kwargs* is of length 1 and has key given by ``id``) or from the
server. If it the instance is not available, it tries to create one
from the **kwargs** parameters.

:parameter model: a :class:`StdModel`
:parameter kwargs: dictionary of parameters.
:rtype: an instance of  two elements tuple containing the instance and a boolean
    indicating if the instance was created or not.
'''
        try:
            res = self.query(model).get(**kwargs)
            created = False
        except model.DoesNotExist:
            res = self.add(model(**kwargs))
            created = True
        return res,created

    def get(self, model, id):
        uuid = model.get_uuid(id)
        if uuid in self._modified:
            return self._modified.get(uuid)
        elif uuid in self._deleted:
            return self._modified.get(uuid)
        
    def add(self, instance, modified = True):
        '''Add an *instance* to the session.
        
:parameter instance: a class:`StdModel` or a :class:`Structure` instance.
:parameter modified: a boolean flag indictaing if the instance was modified.
    
'''
        state = instance.state()
        if state.deleted:
            raise ValueError('State is deleted. Cannot add.')
        instance.session = self
        if state.persistent:
            if modified:
                self._loaded.pop(state,None)
                self._modified[instance] = instance
            elif state not in self._modified:
                self._loaded[instance] = instance
        else:
            self._new[instance] = instance
        
        return instance
    
    def delete(self, instance):
        self.expunge(instance)
        if instance.state().persistent:
            self._deleted[instance] = instance
        
    def flush(self, model):
        return self.backend.flush(model._meta)
    
    def __contains__(self, instance):
        return instance in self._new or state in self._deleted\
                                     or state in self._modified
                                  
    def __iter__(self):
        """Iterate over all pending or persistent instances within this
Session."""
        for v in self._new:
            yield v
        for m in self._modified:
            yield m
        for d in self._deleted:
            yield d
        
    def commit(self):
        """Flush pending changes and commit the current transaction.

        If no transaction is in progress, this method raises an
        InvalidRequestError.

        By default, the :class:`.Session` also expires all database
        loaded state on all ORM-managed attributes after transaction commit.
        This so that subsequent operations load the most recent 
        data from the database.   This behavior can be disabled using
        the ``expire_on_commit=False`` option to :func:`.sessionmaker` or
        the :class:`.Session` constructor.

        If a subtransaction is in effect (which occurs when begin() is called
        multiple times), the subtransaction will be closed, and the next call
        to ``commit()`` will operate on the enclosing transaction.

        For a session configured with autocommit=False, a new transaction will
        be begun immediately after the commit, but note that the newly begun
        transaction does *not* use any connection resources until the first
        SQL is actually emitted.

        """
        if self.transaction is None:
            if not self.autocommit:
                self.begin()
            else:
                raise InvalidTransaction('No transaction was started')

        self.transaction.commit()
        
    def expunge(self, instance):
        instance._dbdata.pop('state',None)
        for d in (self._new,self._modified,self._loaded,self._deleted):
            if instance in d:
                d.pop(instance)
                return True
        return False
    
    def server_update(self, instance, id = None):
        '''Callback by the :class:`stdnet.BackendDataServer` once the commit is
finished. Remove the deleted instances and updated the modified and new
instances.'''
        state = instance.state()
        self.expunge(instance)
        if not state.deleted:
            if id:
                instance.id = instance._meta.pk.to_python(id)
                instance._dbdata['id'] = instance.id
            self.add(instance, False)
            return instance
        
    @classmethod
    def clearall(cls):
        pass
Exemple #10
0
 def __init__(self, router):
     self.transaction = None
     self._models = OrderedDict()
     self._router = router
Exemple #11
0
class Session(object):
    '''The middleware for persistent operations on the back-end.

        It is created via the :meth:`Router.session` method.

    .. attribute:: transaction

        A :class:`Transaction` instance. Not ``None`` if this :class:`Session`
        is in a :ref:`transactional state <transactional-state>`

    .. attribute:: router

        Instance of the :class:`Router` which created this :class:`Session`.
    '''
    def __init__(self, router):
        self.transaction = None
        self._models = OrderedDict()
        self._router = router

    def __str__(self):
        return str(self._router)

    def __repr__(self):
        return '%s: %s' % (self.__class__.__name__, self._router)

    def __iter__(self):
        for sm in self._models.values():
            yield sm

    def __len__(self):
        return len(self._models)

    @property
    def router(self):
        return self._router

    @property
    def dirty(self):
        '''The set of instances in this :class:`Session` which have
been modified.'''
        return frozenset(
            chain(*tuple((sm.dirty for sm in itervalues(self._models)))))

    def begin(self, **options):
        '''Begin a new :class:`Transaction`. If this :class:`Session`
is already in a :ref:`transactional state <transactional-state>`,
an error will occur. It returns the :attr:`transaction` attribute.

This method is mostly used within a ``with`` statement block::

    with session.begin() as t:
        t.add(...)
        ...

which is equivalent to::

    t = session.begin()
    t.add(...)
    ...
    session.commit()

``options`` parameters are passed to the :class:`Transaction` constructor.
'''
        if self.transaction is not None:
            raise InvalidTransaction("A transaction is already begun.")
        else:
            self.transaction = Transaction(self, **options)
        return self.transaction

    def commit(self):
        """Commit the current :attr:`transaction`.

        If no transaction is in progress, this method open one.
        Rarely used directly, see the :meth:`begin` method for details on
        how to start and close a transaction using the `with` construct.
        """
        if self.transaction is None:
            self.begin()
        return self.transaction.commit()

    def query(self, model, **kwargs):
        '''Create a new :class:`Query` for *model*.'''
        sm = self.model(model)
        query_class = sm.manager.query_class or Query
        return query_class(sm._meta, self, **kwargs)

    def empty(self, model):
        '''Returns an empty :class:`Query` for ``model``.'''
        return EmptyQuery(self.manager(model)._meta, self)

    def update_or_create(self, model, **kwargs):
        '''Update or create a new instance of ``model``.

        This method can raise an exception if the ``kwargs`` dictionary
        contains field data that does not validate.

        :param model: a :class:`StdModel`
        :param kwargs: dictionary of parameters.
        :returns: A two elements tuple containing the instance and a boolean
            indicating if the instance was created or not.
        '''
        backend = self.model(model).backend
        return backend.execute(self._update_or_create(model, **kwargs))

    def add(self, instance, modified=True, **params):
        '''Add an ``instance`` to the session.

        If the session is not in a
        :ref:`transactional state <transactional-state>`, this operation
        commits changes to the back-end server immediately.

        :parameter instance: a :class:`Model` instance. It must be registered
            with the :attr:`router` which created this :class:`Session`.
        :parameter modified: a boolean flag indicating if the instance was
            modified.
        :return: the ``instance``.

        If the instance is persistent (it is already stored in the database),
        an updated will be performed, otherwise a new entry will be created
        once the :meth:`commit` method is invoked.
        '''
        sm = self.model(instance)
        instance.session = self
        o = sm.add(instance, modified=modified, **params)
        if modified and not self.transaction:
            transaction = self.begin()
            return transaction.commit(lambda: o)
        else:
            return o

    def delete(self, instance_or_query):
        '''Delete an ``instance`` or a ``query``.

        Adds ``instance_or_query`` to this :class:`Session` list
        of data to be deleted. If the session is not in a
        :ref:`transactional state <transactional-state>`, this operation
        commits changes to the backend server immediately.

        :parameter instance_or_query: a :class:`Model` instance or
            a :class:`Query`.
        '''
        sm = self.model(instance_or_query)
        # not an instance of a Model. Assume it is a query.
        if is_query(instance_or_query):
            if instance_or_query.session is not self:
                raise ValueError('Adding a query generated by another session')
            sm._delete_query.append(instance_or_query)
        else:
            instance_or_query = sm.delete(instance_or_query, self)
        if not self.transaction:
            transaction = self.begin()
            return transaction.commit(
                lambda: transaction.deleted.get(sm._meta))
        else:
            return instance_or_query

    def flush(self, model):
        '''Completely flush a :class:`Model` from the database. No keys
associated with the model will exists after this operation.'''
        return self.model(model).flush()

    def clean(self, model):
        '''Remove empty keys for a :class:`Model` from the database. No
empty keys associated with the model will exists after this operation.'''
        return self.model(model).clean()

    def keys(self, model):
        '''Retrieve all keys for a *model*.'''
        return self.model(model).keys()

    def __contains__(self, instance):
        sm = self.model(instance, False)
        return instance in sm if sm is not None else False

    def model(self, model, create=True):
        '''Returns the :class:`SessionModel` for ``model`` which
can be :class:`Model`, or a :class:`MetaClass`, or an instance
of :class:`Model`.'''
        manager = self.manager(model)
        sm = self._models.get(manager)
        if sm is None and create:
            sm = SessionModel(manager)
            self._models[manager] = sm
        return sm

    def expunge(self, instance=None):
        '''Remove ``instance`` from this :class:`Session`. If ``instance``
is not given, it removes all instances from this :class:`Session`.'''
        if instance is not None:
            sm = self._models.get(instance._meta)
            if sm:
                return sm.expunge(instance)
        else:
            self._models.clear()

    def manager(self, model):
        '''Retrieve the :class:`Manager` for ``model`` which can be any of the
values valid for the :meth:`model` method.'''
        try:
            return self.router[model]
        except KeyError:
            meta = getattr(model, '_meta', model)
            if meta.type == 'structure':
                # this is a structure
                if hasattr(model, 'model'):
                    structure_model = model.model
                    if structure_model:
                        return self.manager(structure_model)
                    else:
                        manager = self.router.structure(model)
                        if manager:
                            return manager
            raise InvalidTransaction('"%s" not valid in this session' % meta)

    def backends_data(self):
        backends = {}
        for sm in self:
            for backend, data in sm.backends_data(self):
                be = backends.get(backend)
                if be is None:
                    backends[backend] = be = []
                be.append(data)
        return backends.items()

    #######################################################################
    #    INTERNALS
    def _update_or_create(self, model, **kwargs):
        pkname = model._meta.pkname()
        pk = kwargs.pop(pkname, None)
        query = self.query(model)
        item = None
        #
        if pk:
            # primary key available
            items = yield query.filter(pkname=pk).all()
            if items:
                item = items[0]
            else:
                kwargs[pkname] = pk
        else:
            params = {}
            rest = {}
            fields = model._meta.dfields
            for field, value in iteritems(kwargs):
                if field in fields and fields[field].index:
                    params[field] = value
                else:
                    rest[field] = value
            if params:
                items = yield query.filter(**params).all()
                if len(items) == 1:
                    item = items[0]
                    kwargs = rest
        if item:
            if kwargs:
                for field, value in iteritems(kwargs):
                    setattr(item, field, value)
                item = yield self.add(item)
            yield item, False
        #
        else:
            item = yield self.add(model(**kwargs))
            yield item, True
Exemple #12
0
class SessionModel(object):
    '''A :class:`SessionModel` is the container of all objects for a given
:class:`Model` in a stdnet :class:`Session`.'''
    def __init__(self, manager):
        self.manager = manager
        self._new = OrderedDict()
        self._deleted = OrderedDict()
        self._delete_query = []
        self._modified = OrderedDict()
        self._queries = []
        self._structures = set()

    def __len__(self):
        return (len(self._new) + len(self._modified) + len(self._deleted) +
                len(self.commit_structures) + len(self.delete_structures))

    def __repr__(self):
        return self._meta.__repr__()

    __str__ = __repr__

    @property
    def backend(self):
        '''The backend for this :class:`SessionModel`.'''
        return self.manager.backend

    @property
    def read_backend(self):
        '''The read-only backend for this :class:`SessionModel`.'''
        return self.manager.read_backend

    @property
    def model(self):
        '''The :class:`Model` for this :class:`SessionModel`.'''
        return self.manager.model

    @property
    def _meta(self):
        '''The :class:`Metaclass` for this :class:`SessionModel`.'''
        return self.manager._meta

    @property
    def new(self):
        '''The set of all new instances within this ``SessionModel``. This
instances will be inserted in the database.'''
        return tuple(itervalues(self._new))

    @property
    def modified(self):
        '''The set of all modified instances within this ``Session``. This
instances will.'''
        return tuple(itervalues(self._modified))

    @property
    def deleted(self):
        '''The set of all instance pks marked as `deleted` within this
:class:`Session`.'''
        return tuple((p.pkvalue() for p in itervalues(self._deleted)))

    @property
    def dirty(self):
        '''The set of all instances which have changed, but not deleted,
within this :class:`SessionModel`.'''
        return tuple(self.iterdirty())

    def iterdirty(self):
        '''Ordered iterator over dirty elements.'''
        return iter(chain(itervalues(self._new), itervalues(self._modified)))

    def __contains__(self, instance):
        iid = instance.get_state().iid
        return (iid in self._new or iid in self._modified
                or iid in self._deleted or instance in self._structures)

    def add(self,
            instance,
            modified=True,
            persistent=None,
            force_update=False):
        '''Add a new instance to this :class:`SessionModel`.

:param modified: Optional flag indicating if the ``instance`` has been
    modified. By default its value is ``True``.
:param force_update: if ``instance`` is persistent, it forces an update of the
    data rather than a full replacement. This is used by the
    :meth:`insert_update_replace` method.
:rtype: The instance added to the session'''
        if instance._meta.type == 'structure':
            return self._add_structure(instance)
        state = instance.get_state()
        if state.deleted:
            raise ValueError('State is deleted. Cannot add.')
        self.pop(state.iid)
        pers = persistent if persistent is not None else state.persistent
        pkname = instance._meta.pkname()
        if not pers:
            instance._dbdata.pop(pkname, None)  # to make sure it is add action
            state = instance.get_state(iid=None)
        elif persistent:
            instance._dbdata[pkname] = instance.pkvalue()
            state = instance.get_state(iid=instance.pkvalue())
        else:
            action = 'update' if force_update else None
            state = instance.get_state(action=action, iid=state.iid)
        iid = state.iid
        if state.persistent:
            if modified:
                self._modified[iid] = instance
        else:
            self._new[iid] = instance
        return instance

    def delete(self, instance, session):
        '''delete an *instance*'''
        if instance._meta.type == 'structure':
            return self._delete_structure(instance)
        inst = self.pop(instance)
        instance = inst if inst is not None else instance
        if instance is not None:
            state = instance.get_state()
            if state.persistent:
                state.deleted = True
                self._deleted[state.iid] = instance
                instance.session = session
            else:
                instance.session = None
            return instance

    def pop(self, instance):
        '''Remove ``instance`` from the :class:`SessionModel`. Instance
could be a :class:`Model` or an id.

:parameter instance: a :class:`Model` or an ``id``.
:rtype: the :class:`Model` removed from session or ``None`` if
    it was not in the session.
'''
        if isinstance(instance, self.model):
            iid = instance.get_state().iid
        else:
            iid = instance
        instance = None
        for d in (self._new, self._modified, self._deleted):
            if iid in d:
                inst = d.pop(iid)
                if instance is None:
                    instance = inst
                elif inst is not instance:
                    raise ValueError('Critical error: %s is duplicated' % iid)
        return instance

    def expunge(self, instance):
        '''Remove *instance* from the :class:`Session`. Instance could be a
:class:`Model` or an id.

:parameter instance: a :class:`Model` or an *id*
:rtype: the :class:`Model` removed from session or ``None`` if
    it was not in the session.
'''
        instance = self.pop(instance)
        instance.session = None
        return instance

    def post_commit(self, results):
        '''\
Process results after a commit.

:parameter results: iterator over :class:`stdnet.instance_session_result`
    items.
:rtype: a two elements tuple containing a list of instances saved and
    a list of ids of instances deleted.'''
        tpy = self._meta.pk_to_python
        instances = []
        deleted = []
        errors = []
        # The length of results must be the same as the length of
        # all committed instances
        for result in results:
            if isinstance(result, Exception):
                errors.append(
                    result.__class__('Exception while committing %s.'
                                     ' %s' % (self._meta, result)))
                continue
            instance = self.pop(result.iid)
            id = tpy(result.id, self.backend)
            if result.deleted:
                deleted.append(id)
            else:
                if instance is None:
                    raise InvalidTransaction('{0} session received id "{1}"\
 which is not in the session.'.format(self, result.iid))
                setattr(instance, instance._meta.pkname(), id)
                instance = self.add(instance,
                                    modified=False,
                                    persistent=result.persistent)
                instance.get_state().score = result.score
                if instance.get_state().persistent:
                    instances.append(instance)
        return instances, deleted, errors

    def flush(self):
        '''Completely flush :attr:`model` from the database. No keys
associated with the model will exists after this operation.'''
        return self.backend.flush(self._meta)

    def clean(self):
        '''Remove empty keys for a :attr:`model` from the database. No
empty keys associated with the model will exists after this operation.'''
        return self.backend.clean(self._meta)

    def keys(self):
        '''Retrieve all keys for a :attr:`model`. Uses the
:attr:`Manager.read_backend`.'''
        return self.read_backend.model_keys(self._meta)

    ## INTERNALS
    def get_delete_query(self, session):
        queries = self._delete_query
        deleted = self.deleted
        if deleted:
            self._deleted.clear()
            q = self.manager.query(session)
            queries.append(q.filter(**{self._meta.pkname(): deleted}))
        if queries:
            self._delete_query = []
            q = queries[0]
            if len(queries) > 1:
                q = q.union(*queries[1:])
            return q

    def backends_data(self, session):
        transaction = session.transaction
        models = session.router
        be = self.backend
        rbe = self.read_backend
        model = self.model
        meta = model._meta
        dirty = self.dirty
        deletes = self.get_delete_query(session)
        has_delete = deletes is not None
        structures = self._structures
        queries = self._queries
        if dirty or has_delete or queries is not None or structures:
            if transaction.signal_delete and has_delete:
                models.pre_delete.fire(model,
                                       instances=deletes,
                                       session=session)
            if dirty and transaction.signal_commit:
                models.pre_commit.fire(model, instances=dirty, session=session)
            if be == rbe:
                yield be, session_data(meta, dirty, deletes, queries,
                                       structures)
            else:
                if dirty or has_delete or structures:
                    yield be, session_data(meta, dirty, deletes, (),
                                           structures)
                if queries:
                    yield rbe, session_data(meta, (), (), queries, ())

    def _add_structure(self, instance):
        instance.action = 'update'
        self._structures.add(instance)
        return instance

    def _delete_structure(self, instance):
        instance.action = 'delete'
        self._structures.add(instance)
        return instance
Exemple #13
0
class SessionModel(object):
    '''A :class:`SessionModel` is the container of all objects for a given
:class:`Model` in a stdnet :class:`Session`.'''
    def __init__(self, meta, session):
        self.meta = meta
        self.session = session
        self.backend.setup_model(meta)
        self._new = OrderedDict()
        self._deleted = OrderedDict()
        self._delete_query = []
        self._modified = OrderedDict()
        self._loaded = {}

    def __len__(self):
        return len(self._new) + len(self._modified) + len(self._deleted) +\
                len(self._loaded)

    def __repr__(self):
        return self.meta.__repr__()
    __str__ = __repr__
    
    def __iter__(self):
        """Iterate over all pending or persistent instances within this
Session model."""
        return iter(self.all())

    def all(self):
        return chain(self._new,self._modified,self._loaded,self._deleted)
    
    @property
    def backend(self):
        return self.session.backend
    
    @property
    def model(self):
        return self.meta.model

    @property
    def new(self):
        "The set of all modified instances within this ``Session``"
        return frozenset(itervalues(self._new))

    @property
    def modified(self):
        "The set of all modified instances within this ``Session``"
        return frozenset(itervalues(self._modified))

    @property
    def loaded(self):
        '''The set of all unmodified, but not deleted, instances within this
:class:`Session`.'''
        return frozenset(itervalues(self._loaded))

    @property
    def deleted(self):
        '''The set of all instances marked as 'deleted' within this
:class:`Session`.'''
        return frozenset(itervalues(self._deleted))

    @property
    def dirty(self):
        '''The set of all instances which have changed, but not deleted,
within this :class:`Session`.'''
        return frozenset(self.iterdirty())

    def iterdirty(self):
        '''Ordered iterator over dirty elements.'''
        return iter(chain(itervalues(self._new), itervalues(self._modified)))

    def __contains__(self, instance):
        iid = instance.state().iid
        return iid in self._new or\
               iid in self._modified or\
               iid in self._deleted or\
               iid in self._loaded

    def get(self, id):
        if id in self._modified:
            return self._modified.get(id)
        elif id in self._deleted:
            return self._deleted.get(id)
        elif id in self._loaded:
            return self._loaded.get(id)

    def add(self, instance, modified=True, persistent=None):
        '''Add a new instance to this :class:`SessionModel`.

:modified: Optional flag indicating if the *instance* has been modified. By
    default its value is ``True``.
:rtype: The instance added to the session'''
        state = instance.state()
        if state.deleted:
            raise ValueError('State is deleted. Cannot add.')
        self.pop(state.iid)
        persistent = persistent if persistent is not None else state.persistent
        pkname = instance._meta.pkname()
        if persistent:
            instance._dbdata[pkname] = instance.pkvalue()
        else:
            instance._dbdata.pop(pkname, None)
        state = instance.state(update=True)
        iid = state.iid
        if state.persistent:
            if modified:
                self._modified[iid] = instance
            else:
                self._loaded[iid] = instance
        else:
            self._new[iid] = instance
        return instance

    def delete(self, instance, session):
        '''delete an *instance*'''
        inst = self.pop(instance)
        instance = inst if inst is not None else instance
        if instance is not None:
            state = instance.state()
            if state.persistent:
                state.deleted = True
                self._deleted[state.iid] = instance
                instance.session = session
            else:
                instance.session = None
            return instance

    def pop(self, instance):
        '''Remove *instance* from the :class:`Session`. Instance could be a
:class:`Model` or an id.

:parameter instance: a :class:`Model` or an *id*
:rtype: the :class:`Model` removed from session or ``None`` if
    it was not in the session.
'''
        if isinstance(instance,self.meta.model):
            iid = instance.state().iid
        else:
            iid = instance
        instance = None
        for d in (self._new, self._modified, self._loaded, self._deleted):
            if iid in d:
                inst = d.pop(iid)
                if instance is None:
                    instance = inst
                elif inst is not instance:
                    raise ValueError(\
                    'Critical error. Instance {0} is duplicated'.format(iid))
        return instance

    def expunge(self, instance):
        '''Remove *instance* from the :class:`Session`. Instance could be a
:class:`Model` or an id.

:parameter instance: a :class:`Model` or an *id*
:rtype: the :class:`Model` removed from session or ``None`` if
    it was not in the session.
'''
        instance = self.pop(instance)
        instance.session = None
        return instance

    def get_delete_query(self, **kwargs):
        queries = self._delete_query
        if queries:
            q = queries[0]
            if len(queries) > 1:
                q = q.union(*queries[1:])
            return q.backend_query(**kwargs)

    def query(self):
        return self.session.query(self.model)

    def pre_commit(self, transaction):
        d = self.deleted
        if d:
            self._deleted.clear()
            if self.model._model_type == 'object':
                q = self.query().filter(id__in  = d)
                self._delete_query.append(q)
            else:
                self._delete_query.extend(d)
            if transaction.signal_delete:
                pre_delete.send(self.model, instances=self._delete_query,
                                transaction=transaction)
        dirty = tuple(self.iterdirty())
        if dirty and transaction.signal_commit:
            pre_commit.send(self.model, instances=dirty,
                            transaction=transaction)
        return len(self._delete_query) + len(dirty)

    def post_commit(self, results):
        '''\
Process results after a commit.

:parameter results: iterator over :class:`stdnet.instance_session_result`
    items.
:rtype: a two elements tuple containing a list of instances saved and
    a list of ids of instances deleted.'''
        tpy = self.meta.pk_to_python
        instances = []
        deleted = []
        errors = []
        # The length of results must be the same as the length of
        # all committed instances
        for result in results:
            if isinstance(result, Exception):
                errors.append(result.__class__(
                'Exception while commiting {0}. {1}'.format(self.meta, result)))
                continue
            instance = self.pop(result.iid)
            id = tpy(result.id)
            if result.deleted:
                deleted.append(id)
            else:
                if instance is None:
                    raise InvalidTransaction('{0} session received id "{1}"\
 which is not in the session.'.format(self, result.iid))
                setattr(instance, instance._meta.pkname(), id)
                instance = self.add(instance,
                                    modified=False,
                                    persistent=result.persistent)
                instance.state().score = result.score
                if instance.state().persistent:
                    instances.append(instance)
        return instances, deleted, errors
Exemple #14
0
 def __init__(self, backend, query_class=None):
     self.backend = getdb(backend)
     self.transaction = None
     self._models = OrderedDict()
     self.query_class = query_class or Query
Exemple #15
0
class Session(object):
    '''The manager of persistent operations on the backend data server for
:class:`StdModel` classes.

.. attribute:: backend

    the :class:`stdnet.BackendDataServer` instance

.. attribute:: transaction

    A :class:`Transaction` instance. Not ``None`` if this :class:`Session`
    is in a transactional state.

.. attribute:: query_class

    class for querying. Default is :class:`Query`.
'''
    _structures = {}
    def __init__(self, backend, query_class=None):
        self.backend = getdb(backend)
        self.transaction = None
        self._models = OrderedDict()
        self.query_class = query_class or Query

    def __str__(self):
        return str(self.backend)

    def __repr__(self):
        return '{0}({1})'.format(self.__class__.__name__,self)

    def __iter__(self):
        for sm in self._models.values():
            yield sm

    def __len__(self):
        return len(self._models)

    @property
    def dirty(self):
        '''set of all changed instances in the session'''
        return frozenset(chain(*tuple((sm.dirty for sm\
                                        in itervalues(self._models)))))
        
    def session(self):
        '''Create a new session from this :class:`Session`'''
        return self.__class__(self.backend,self.query_class)

    def model(self, meta):
        if hasattr(meta, '_meta'):
            meta = meta._meta
        sm = self._models.get(meta)
        if sm is None:
            if meta.model._model_type == 'structure':
                sm = SessionStructure(meta, self)
            else:
                sm = SessionModel(meta, self)
            self._models[meta] = sm
        return sm

    def expunge(self, instance = None):
        if instance is not None:
            sm = self._models.get(instance._meta)
            if sm:
                return sm.expunge(instance)
        else:
            self._models.clear()

    def begin(self, **options):
        '''Begin a new :class:`Transaction`.
If this :class:`Session` is already within a transaction, an error is raised.
It returns the :attr:`transaction` attribute. It can be used in a `with`
construct::
    
    with session.begin() as t:
        t.add(...
        ...
        
which is equivalent to::
    
    t = session.begin()
    t.add(...
    ...
    session.commit()
    
`options` parameter are passed to the :class:`Transaction` constructor.
'''
        if self.transaction is not None:
            raise InvalidTransaction("A transaction is already begun.")
        else:
            self.transaction = Transaction(self, **options)
        return self.transaction

    def commit(self):
        """Commit the current :attr:`transaction`. If no transaction is in
progress, this method open one. Rarely used directly, see the :meth:`begin`
method for details on how to start and close a transaction using the `with`
construct."""
        if self.transaction is None:
            self.begin()
        return self.transaction.commit()
    
    def query(self, model, query_class=None, **kwargs):
        '''Create a new :class:`Query` for *model*.'''
        query_class = query_class or self.query_class
        return query_class(model._meta, self, **kwargs)

    def empty(self, model):
        return EmptyQuery(model._meta, self)

    def get_or_create(self, model, **kwargs):
        '''Get an instance of *model* from the internal cache (only if the
dictionary *kwargs* is of length 1 and has key given by ``id``) or from the
server. If it the instance is not available, it tries to create one
from the **kwargs** parameters.

:parameter model: a :class:`StdModel`
:parameter kwargs: dictionary of parameters.
:rtype: an instance of  two elements tuple containing the instance and a boolean
    indicating if the instance was created or not.
'''
        try:
            res = self.query(model).get(**kwargs)
            created = False
        except model.DoesNotExist:
            res = self.add(model(**kwargs))
            created = True
        return res,created

    def get(self, model, id):
        sm = self._models.get(model._meta)
        if sm:
            return sm.get(id)

    @commit_element_when_no_transaction
    def add(self, instance, modified=True):
        '''Add an *instance* to the session.

:parameter instance: a class:`StdModel` or a :class:`Structure` instance.
:parameter modified: a boolean flag indicating if the instance was modified.
'''
        sm = self.model(instance._meta)
        instance.session = self
        return sm.add(instance, modified)

    @commit_element_when_no_transaction
    def delete(self, instance, **kwargs):
        '''Add an *instance* to the session instances to be deleted.

:parameter instance: a class:`StdModel` or a :class:`Structure` instance.
'''
        sm = self.model(instance._meta)
        # not an instance of a Model. Assume it is a query.
        if is_query(instance):
            if instance.session is not self:
                raise ValueError('Adding a query generated by another session')
            sm._delete_query.append(instance)
            return instance
        else:
            return sm.delete(instance, self)

    def flush(self, model):
        '''Completely flush a :class:`Model` from the database. No keys
associated with the model will exists after this operation.'''
        return self.backend.flush(model._meta)

    def clean(self, model):
        '''Remove empty keys for a :class:`Model` from the database. No
empty keys associated with the model will exists after this operation.'''
        return self.backend.clean(model._meta)

    def keys(self, model):
        '''Retrieve all keys for a *model*.'''
        return self.backend.model_keys(model._meta)

    def __contains__(self, instance):
        sm = self._models.get(instance._meta)
        return instance in sm if sm is not None else False

    def structure(self, instance):
        '''Return a :class:`stdnet.BackendStructure` for a given
:class:`Structure` *instance*.'''
        return self.backend.structure(instance)

    @classmethod
    def clearall(cls):
        pass