Source code for alkali.model
from collections import OrderedDict, Iterable
import json
from .memoized_property import memoized_property
from .metamodel import MetaModel
from . import fields
from . import signals
[docs]class ObjectDoesNotExist(Exception):
"""
base class for a model specific exception (eg. MyModel.DoesNotExist)
raised when a query yields no results
"""
pass
[docs]class Model(metaclass=MetaModel):
"""
main class for the database.
the definition of this class defines a table schema but instances of
this class hold a row.
model fields are available as attributes. eg. ``m.my_field = 'foo'``
the Django docs at https://docs.djangoproject.com/en/1.10/topics/db/models/
will be fairly relevant to alkali
see :mod:`alkali.database` for some example code
"""
def __init__(self, *args, **kw):
# MetaModel.__call__ has put fields in self,
# put any other keywords into self
for name, value in kw.items():
setattr(self, name, value)
# note, this is called twice, once during initial object creation
# and then again when this object is copied during a save
signals.creation.send(self.__class__, instance=self)
# called via copy.copy() module, when getting from manager
def __copy__(self):
new = type(self)()
new.__dict__.update(self.__dict__)
return new
def __repr__(self):
return "<{}: {}>".format(self.__class__.__name__, self.pk)
def __eq__(self, other):
# this is obviously a very shallow comparison
return type(self) == type(other) and self.pk == other.pk
def __ne__(self, other):
return not self.__eq__(other)
[docs] def set_field(self, field, value):
"""
set a field value, this method is automatically called when setting
a field value. safe to call externally.
fires :data:`alkali.signals.field_update` for any listeners
:param field: instance of Field
:type field: :class:`alkali.fields.Field`
:param value: the already-cast value to store
:type value: ``Field.field_type``
"""
# if we're setting a field value and that value is different
# than current value, mark self as modified
curr_val = getattr(self, field.name)
# do not let user change pk after it has been set
if field.primary_key:
if curr_val is not None and curr_val != value:
_vals = (self.__class__.__name__, self.pk, value)
raise RuntimeError( "{}: trying to change set pk value: {} to {}".format(*_vals) )
self.__dict__[field.name] = value # actually set the value
if curr_val != value:
self.__dict__['_dirty'] = True
signals.field_update.send(self.__class__, field=field.name, old_val=curr_val, new_val=value)
@property
def dirty(self):
"""
**property**: return True if our fields have changed since creation
"""
return self._dirty
@property
def schema(self):
"""
**property**: a string that quickly shows the fields and types
"""
def fmt(name, field):
return "{}:{}".format(name, field.field_type.__name__)
name_type = [ fmt(n, f) for n, f in self.Meta.fields.items() ]
fields = ", ".join( name_type )
return "<{}: {}>".format(self.__class__.__name__, fields)
@memoized_property
def pk(self):
"""
**property**: returns this models primary key value. If the model is
comprised of several primary keys then return a tuple of them.
:rtype: ``Field.field_type`` or tuple-of-Field.field_type
"""
pks = list(self.Meta.pk_fields.values())
foreign_pks = list(filter(lambda f: isinstance(f, fields.ForeignKey), pks))
if foreign_pks:
pk_vals = tuple( getattr(self, f.name).pk for f in pks )
if len(pk_vals) == 1:
return pk_vals[0]
assert False, "not actually supported at this time" # pragma: nocover
return pk_vals # pragma: nocover, not actually supported at this time
else:
pk_vals = tuple( getattr(self, f.name) for f in pks )
if len(pk_vals) == 1:
return pk_vals[0]
return pk_vals
@property
def valid_pk(self):
pk = self.pk
if isinstance(pk, Iterable):
return all( map(lambda e: e is not None, pk) )
return pk is not None
@property
def dict(self):
"""
**property**: returns a dict of all the fields, the fields are
json consumable
:rtype: ``OrderedDict``
"""
return OrderedDict( [(name, field.dumps(getattr(self, name)))
for name, field in self.Meta.fields.items() ])
@property
def json(self):
"""
**property**: returns json that holds all the fields
:rtype: ``str``
"""
return json.dumps(self.dict)
[docs] def save(self):
"""
add ourselves to our :class:`alkali.manager.Manager` and mark
ourselves as no longer dirty.
it's up to our ``Manager`` to persistently save us
"""
self.__class__.objects.save(self)
self._dirty = False
return self