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 __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
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 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)
def __init__(self, router): self.transaction = None self._models = OrderedDict() self._router = router
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
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
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
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
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
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
def __init__(self, backend, query_class=None): self.backend = getdb(backend) self.transaction = None self._models = OrderedDict() self.query_class = query_class or Query
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