import inspect
import copy
from .query import Query
from . import fields
from . import signals
import logging
logger = logging.getLogger(__name__)
[docs]class Manager:
"""
the ``Manager`` class is the parent/owner of all the
:class:`alkali.model.Model` instances. Each ``Model`` has it's own
manager. ``Manager`` could rightly be called ``Table``.
"""
def __init__( self, model_class ):
"""
:param Model model_class: the model that we should store (not an instance)
"""
assert inspect.isclass(model_class)
self._model_class = model_class
self._instances = {}
self._dirty = False
self.clear()
def __repr__(self):
return "<{}: {}>".format(self._name, len(self))
def __len__(self):
return len(self._instances)
def __getattr__(self, attr):
# return a Query object, this prevents us from having to
# make pass-through functions for each Query param.
# eg. Manager().filter() -> Query().filter()
return getattr(Query(self), attr)
@property
def model_class(self):
return self._model_class
@property
def count(self):
"""
**property**: number of model instances we're holding
"""
return len(self)
@property
def _name(self):
"""
**property**: pretty version of our class name, based on our model
eg. *MyModelManager*
"""
return "{}Manager".format(self.model_class.__name__)
@property
def pks(self):
"""
**property**: return all primary keys
:rtype: ``list``
"""
return list(self._instances.keys())
@property
def instances(self):
"""
**property**: return all model instances
:rtype: ``list``
"""
return [copy.copy(obj) for obj in self._instances.values()]
@property
def dirty(self):
"""
**property**: return True if any model instances are dirty
:rtype: ``bool``
"""
if self._dirty:
return True
[docs] @staticmethod
def sorter(elements, reverse=False ):
"""
yield model instances in primary key order
:param Manager.instances elements: our instances
:param kw:
* reverse: return in reverse order
:rtype: ``generator``
"""
for key in sorted(elements.keys(), reverse=reverse):
yield elements[key]
[docs] def save(self, instance, dirty=True, copy_instance=True):
"""
Copy instance into our collection. We make a copy so that caller
can't change its object and affect our version without calling
save() again.
:param Model instance:
:param dirty: don't mark us as dirty if False, used during loading
"""
#logger.debug( "saving model instance: %s", str(instance.pk) )
signals.pre_save.send(self.model_class, instance=instance )
assert instance.pk is not None, \
"{}.save(): instance '{}' has None for pk".format(self._name, instance)
if copy_instance:
instance = self._instances[instance.pk] = copy.copy(instance)
else:
self._instances[instance.pk] = instance
# THINK may be mistake to send the actual object out via the signal but probably
# what any reciever actually wants
signals.post_save.send( self.model_class, instance=instance )
# self._dirty is required because think what would happen
# if we add a clean model instance
if dirty:
self._dirty = True
[docs] def clear(self):
"""
remove all instances of our models. we'll be marked as
dirty if we previously had model instances.
**Note**: this does not affect on-disk files until
:func:`Manager.save` is called.
"""
logger.debug( "%s: clearing all models", self._name )
self._dirty = len(self) > 0
self._instances = {}
[docs] def delete(self, instance):
"""
remove an instance from our models by calling ``del`` on it
:param Model instance:
"""
# TODO should probably take an pk instead of an instance
# logger.debug( "deleting model instance: %s", str(instance.pk) )
signals.pre_delete.send(self.model_class, instance=instance)
try:
del self._instances[ instance.pk ]
self._dirty = True
signals.post_delete.send(self.model_class, instance=instance)
except KeyError:
pass
[docs] def cb_delete_foreign(self, sender, instance ):
"""
called when our foreign parent is about to be deleted
"""
# keep in sync with metamodel._add_relmanagers()
fk_set = self.model_class.__name__.lower() + '_set'
# eg. instance.auxinfo_set.all()
for elem in getattr(instance, fk_set).all():
self.delete(elem)
[docs] def cb_create_foreign(self, sender, instance ):
"""
called when our foreign parent (likely OneToOneField) is created
"""
# keep in sync with metamodel._add_relmanagers()
elem = self.model_class(pk=instance)
self.save(elem, dirty=False, copy_instance=False)
[docs] def store(self, storage, force=False):
"""
save all our instances to storage
:param Storage storage: an instance
:param bool force: force save even if we're not dirty
"""
if not storage:
logger.debug("%s: no storage instance for storing, exiting", self._name)
return
if force:
self._dirty = True
if self.dirty:
signals.pre_store.send(self.model_class)
logger.debug( "%s: has dirty records, saving", self._name )
logger.debug( "%s: storing models via storage class: %s", self._name, storage._name )
gen = Manager.sorter(self._instances)
storage.write(self.model_class, gen)
logger.debug( "%s: finished storing %d records", self._name, len(self) )
signals.post_store.send(self.model_class)
else:
logger.debug( "%s: has no dirty records, not saving", self._name )
self._dirty = False
[docs] def load(self, storage):
"""
load all our instances from storage
:param Storage storage: an instance
:raises KeyError: if there are duplicate primary keys
"""
if not storage:
logger.debug("%s: no storage instance for loading, exiting", self._name)
return
def validate_fk_fields(fk_fields, elem):
for fk_field_name in fk_fields:
try:
getattr(elem, fk_field_name) # this does a lookup on foreign key object
except KeyError: # THINK
# get elem's pk value, need to do it in this convoluted way since
# elem.pk might try to lookup the very thing that is missing
field_name = elem.Meta.pk_fields.keys()[0]
pk_value = elem.__dict__[field_name]
logger.warning( "%s.%s: foreign instance missing: %s",
self.model_class.__name__, elem.__dict__[fk_field_name], pk_value)
# THINK/TODO we need to delete ourselves
return False
return True
assert not inspect.isclass(storage), "storage is not an instance"
logger.debug( "%s: loading models via storage class: %s", self._name, storage._name )
signals.pre_load.send(self.model_class)
self.clear()
dirty = False
fk_fields = self.model_class.Meta.field_filter(fields.ForeignKey)
for elem in storage.read( self.model_class ):
if isinstance(elem, dict):
elem = self.model_class( **elem )
if not validate_fk_fields(fk_fields, elem):
logger.debug("failed to validate_fk_fields")
dirty = True
continue
if elem.pk in self._instances: # THINK
raise KeyError( '%s: pk collision detected during load: %s'
% (self.model_class.__name__, str(elem.pk)) )
if elem.pk is None:
raise self.model_class.EmptyPrimaryKey()
self.save(elem, dirty=False, copy_instance=False)
self._dirty = dirty
logger.debug( "%s: finished loading %d records", self._name, len(self) )
signals.post_load.send(self.model_class)
[docs] def get(self, *pk, **kw):
"""
perform a query that returns a single instance of a model
:param pk: optional primary key
:type pk: value or ``tuple`` if multi-pk
:param kw: optional ``field_name=value``
:rtype: single :class:`alkali.model.Model` instance
:raises DoesNotExist: if 0 instances returned
:raises MultipleObjectsReturned: if more than 1 instance returned
::
m = MyModel.objects.get(1) # equiv to
m = MyModel.objects.get(pk=1)
m = MyModel.objects.get(some_field='a unique value')
m = MyModel.objects.get(field1='a unique', field2='value')
"""
# FIXME need to support direct access multi pk models
if len(pk) == 0 and list(kw.keys()) == ['pk']:
pk = list(kw.values())
# NOTE without this, direct access ForeignKeys are 100x slower
if len(pk) == 1:
pk = self.model_class.Meta.pk_fields.values()[0].cast(pk[0])
return copy.copy( self._instances[pk] )
results = Query(self).filter(**kw)
if len(results) == 0:
raise self.model_class.DoesNotExist("{}: no results for: {}".format(
self.model_class.__name__, str(kw)) )
if len(results) > 1:
raise self.model_class.MultipleObjectsReturned("{}: got {} results for: {}".format(
self.model_class.__name__, len(results), str(kw)) )
return results[0]
[docs] def get_or_create(self, **kw):
try:
return self.get(**kw)
except self.model_class.DoesNotExist:
pass
return self.model_class(**kw).save()