import inflection
from . import fields
from . import gitdb
__all__ = ["Model", "ReturnSet", "MetaModel"]
def make_property(name, field):
def getter(self):
return self._attrs[name]
getter.__name__ = name
getter.__doc__ = "Getter for {name}".format(name=name)
def setter(self, val):
try:
val = field.get_value(val)
except ValueError:
m = "Disallowed value {val} failed field checks"
raise ValueError(m.format(val=val))
self._attrs[name] = val
setter.__name__ = name
setter.__doc__ = "Setter for {name}" .format(name=name)
return property(fget=getter, fset=setter)
def make_init(initialiser):
def __init__(self, *args, **kwargs):
initialiser(self, *args, **kwargs)
if not hasattr(self, "id") or self.id is None:
raise TypeError("This class has not been initialised")
return __init__
[docs]class Model(metaclass=MetaModel):
"""Base class for models
Subclass this class to declare a model. :py:class:`~.Model` provides a
default initialiser, an equivalence function, and the
:py:meth:`~.Model.save` and :py:meth:`~.Model.find` methods.
:param int model_id: If this is provided, the model will be initialised
with the attributes specified by the document with that id in the
database. This is generally for internal use (i.e, creating the object
after a search has been completed) but it may be useful.
:param mixed kwargs: This is the more usual way of initialising the model
- that is, by passing in key=val pairs describing the values passed to
the fields specified by the model. The default initialiser will then
go through all of the arguments, check that they are known fields, and
assign them.
:attribute id: The instance will always have this attribute referring to
the id that it refers to in the database. It will be None if the
instance has not been inserted into the database (this should never
happen, though!)
"""
def __init__(self, model_id=None, **kwargs):
self._attrs = {}
self.id = None
if model_id is None:
self._init_from_kwargs(kwargs)
else:
self.id = model_id
self._init_from_kwargs(self._table.get(model_id), save=False)
assert self.id is not None
def _init_from_kwargs(self, kwargs, save=True):
to_set = {}
attrs = MetaModel.get_attributes(self)
for key, val in kwargs.items():
if key in attrs:
to_set[key] = val
else:
emsg = "Unrecognised keyword argument {k}".format(k=key)
raise ValueError(emsg)
for key, field in attrs.items():
val = to_set.get(key, None)
try:
val = field.get_value(val)
except ValueError:
emsg = "Value {v} failed acceptance check for key {k}"
raise ValueError(emsg.format(k=key, v=val))
else:
setattr(self, key, val)
if save:
self.save()
[docs] def save(self):
"""Saves this instance to the database.
If this instance has been saved before, this will update the database
document corresponding to the current id. Otherwise, it will insert
a new document into the database, storing the document id.
"""
if self.id is None:
self.id = self._table.insert(self._attrs)
else:
self.id = self._table.update(self.id, self._attrs)
return self.id
@classmethod
[docs] def get_table(cls):
"""Returns the table associated with this model."""
return cls._table
@classmethod
[docs] def find(cls, **kwargs):
"""Finds documents in the database.
Given keyword arguments (which have the same format as the arguments
given to :py:meth:`.gitdb.GitDB.find`), this method returns a
:py:class:`~.ReturnSet` containing all of the matching documents.
:param mixed kwargs: See :py:meth:`.gitdb.GitDB.find` for the full
finding syntax.
:return: :py:class:`~.ReturnSet` of all of the matching documents.
"""
for i in kwargs:
if i not in MetaModel.get_attributes(cls):
m = "Cannot find on attributes not owned by this class ({key})"
raise TypeError(m.format(key=i))
return ReturnSet(cls._table.find_ids(kwargs), cls)
def __eq__(self, other):
if not isinstance(other, type(self)):
return False
return self._attrs == other._attrs
[docs]class ReturnSet:
"""A class representing the documents returned by a particular query.
This class can be used to further narrow down the search
(:py:meth:`~.find`), or return initialised instances of the model that
found them (:py:meth:`~.first`, :py:meth:`~.all`,
:py:meth:`~.__getitem__`). It can also tell you how many items the set
currently contains (:py:meth:`~.__len__`)
The documents are returned sorted in order of the ids. This ensures that
further operations on a set will preserve order, but should not be relied
on, as the specifics of document ids is not part of the public interface.
"""
def __init__(self, ids, cls):
self.ids = sorted(ids)
self.cls = cls
def __len__(self):
return len(self.ids)
def __eq__(self, other):
return hasattr(other, 'ids') and self.ids == other.ids
[docs] def find(self, **kwargs):
"""Refine the terms of the original search
Using the model that created this set, find which documents match the
new queres, then update this set to point to the intersection of both
the old and new queries.
:param mixed kwargs: See :py:meth:`.Model.find`.
:return: This set, to allow for chaining method calls.
"""
other_ids = self.cls.find(**kwargs).ids
self.ids = sorted(set(other_ids).intersection(self.ids))
return self
[docs] def first(self):
"""Returns the first document, or None"""
if not self.ids:
return None
return self[0]
[docs] def all(self):
"""Returns a list of all of the documents."""
return [self.cls(model_id=i) for i in self.ids]
def __getitem__(self, i):
return self.cls(model_id=self.ids[i])