#!/usr/bin/env python
"""
ORM style interface for working with RethinkDB and having a native wrapper and
some helper functions for working with the wrapper. This module contains
the base model which should be inherited.
"""
import rethinkdb as r
[docs]class RethinkModel(object):
"""
Emulates a python object for the data which is returned from rethinkdb and
the official Python client driver. Raw data from the database is stored in
_data to keep the objects namespace clean. For more information look at how
_get() and _set() function in order to keep the namespace cleaner but still
provide easy access to data.
This object has a __repr__ method which can be used with print or logging
statements. It will give the id and a representation of the internal _data
dict for debugging purposes.
"""
_protectedItems = []
_conn = None
_join = None
_joinedField = ""
table = "" #: The table which this document object will be stored in
primaryKey = "id" #: The current primary key of the table
durability = "soft"
"""Can either be Hard or Soft, and is passed to RethinkDB"""
non_atomic = False
"""Determins if the transaction can be non atomic or not"""
upsert = True
"""Will either update, or create a new object if true and a primary key is
given."""
[docs] def __init__(self, id=False, **kwargs):
"""
Initializes the main object, if `id` is in kwargs, then we assume
this is already in the database, and will try to pull its data, if not,
then we assume this is a new entry that will be inserted.
(Optional, only if not using .repl()) `conn` or `connection` can also
be passed, which will be used in all the .run() clauses.
"""
protectedItems = dir(self)
protectedItems.append(self._protectedItems)
self._protectedItems = protectedItems
"""
List of strings to not store in the database; automatically set to
the built in properties of this object to prevent any accidental stuff
"""
# Is this a new object, or already in the database? (set later)
self._new = True
self._data = {} # STORE ALL THE DATA!!
# If we're given a connection, we'll use it, if not, we'll assume
# .repl() was called on r.connect()
if hasattr(kwargs, "conn") or hasattr(kwargs, "connection"):
self._conn = kwargs["conn"]
key = kwargs[self.primaryKey] if self.primaryKey in kwargs else id
if key is None or key == "" and len(kwargs) == 0:
raise Exception("""Cannot have an empty or type None key""")
elif key and len(kwargs) > 0:
# Assume we have data from a collection, just go with it and set
# our data.
#self._makeNew(kwargs)
raise Exception("""Cannot supply primary key and additional \
arguments while searching for Documents.""")
if key and not self._grabData(key):
raise Exception("""Could not find key in database""")
self._makeNew(kwargs)
if key:
self._data[self.primaryKey] = key
# Hook to run any inherited class code, if needed
self.finishInit()
def _makeNew(self, kwargs):
# We assume this is a new object, and that we'll insert it
for key in kwargs:
if key not in ["conn", "connection"] or key[0] != "_":
self._data[key] = kwargs[key]
def _grabData(self, key):
"""
Tries to find the existing document in the database, if it is found,
then the objects _data is set to that document, and this returns
`True`, otherwise this will return `False`
:param key: The primary key of the object we're looking for
:type key: Str
:return: True if a document was found, otherwise False
:rtype: Boolean
"""
rawCursor = r.table(self.table).get(key).run(self._conn)
if rawCursor:
self._data = rawCursor
self._new = False
return True
else:
return False
[docs] def finishInit(self):
"""
A hook called at the end of the main `__init__` to allow for
custom inherited classes to customize their init process without having
to redo all of the existing int.
This should accept nothing besides `self` and nothing should be
returned.
"""
pass
def _get(self, item):
"""
Helper function to keep the __getattr__ and __getitem__ calls
KISSish
"""
if item not in object.__getattribute__(self, "_protectedItems") \
and item[0] != "_":
data = object.__getattribute__(self, "_data")
if item in data:
return data[item]
return object.__getattribute__(self, item)
def _set(self, item, value):
"""
Helper function to keep the __setattr__ and __setitem__ calls
KISSish
Will only set the objects _data if the given items name is not prefixed
with _ or if the item exists in the protected items List.
"""
if item not in object.__getattribute__(self, "_protectedItems") \
and item[0] != "_":
keys = object.__getattribute__(self, "_data")
if not hasattr(value, '__call__'):
keys[item] = value
return value
if hasattr(value, '__call__') and item in keys:
raise Exception("""Cannot set model data to a function, same \
name exists in data""")
return object.__setattr__(self, item, value)
def __getattr__(self, item):
return self._get(item)
def __getitem__(self, item):
return self._get(item)
def __setattr__(self, item, value):
return self._set(item, value)
def __setitem__(self, item, value):
return self._set(item, value)
[docs] def __delitem__(self, item):
"""
Deletes the given item from the objects _data dict, or if from the
objects namespace, if it does not exist in _data.
"""
keys = object.__getattribute__(self, "_data")
if item in keys:
del(keys[item])
else:
object.__delitem__(self, item)
[docs] def __contains__(self, item):
"""
Allows for the use of syntax similar to::
if "blah" in model:
This only works with the internal _data, and does not include other
properties in the objects namepsace.
"""
keys = object.__getattribute__(self, "_data")
if item in keys:
return True
return False
@classmethod
def fromRawEntry(cls, **kwargs):
"""
Helper function to allow wrapping existing data/entries, such as
those returned by collections.
"""
id = kwargs["id"]
kwargs.pop("id")
what = cls(**kwargs)
what._new = False
what.id = id
return what
@classmethod
[docs] def new(cls, **kwargs):
"""
Creates a new instance, filling out the models data with the keyword
arguments passed, so long as those keywords are not in the protected
items array.
"""
return cls(**kwargs)
@classmethod
[docs] def create(cls, id=None, **kwargs):
"""
Similar to new() however this calls save() on the object before
returning it.
"""
what = cls(**kwargs)
if id:
setattr(what, cls.primaryKey, id)
what.save()
return what
@classmethod
[docs] def find(cls, id):
"""
Loads an existing entry if one can be found, otherwise an exception is
raised.
:param id: The id of the given entry
:type id: Str
:return: `cls` instance of the given `id` entry
"""
return cls(id)
[docs] def save(self):
"""
If an id exists in the database, we assume we'll update it, and if not
then we'll insert it. This could be a problem with creating your own
id's on new objects, however luckily, we keep track of if this is a new
object through a private _new variable, and use that to determine if we
insert or update.
"""
if not self._new:
reply = r.table(self.table) \
.update(self._data,
durability=self.durability,
non_atomic=self.non_atomic) \
.run(self._conn)
else:
reply = r.table(self.table) \
.insert(self._data,
durability=self.durability,
upsert=self.upsert) \
.run(self._conn)
self._new = False
if "generated_keys" in reply and reply["generated_keys"]:
self._data[self.primaryKey] = reply["generated_keys"][0]
if "errors" in reply and reply["errors"] > 0:
raise Exception("Could not insert entry: %s"
% reply["first_error"])
return True
[docs] def delete(self):
"""
Deletes the current instance. This assumes that we know what we're
doing, and have a primary key in our data already. If this is a new
instance, then we'll let the user know with an Exception
"""
if self._new:
raise Exception("This is a new object, %s not in data, \
indicating this entry isn't stored." % self.primaryKey)
r.table(self.table).get(self._data[self.primaryKey]) \
.delete(durability=self.durability).run(self._conn)
return True
[docs] def __repr__(self):
"""
Allows for the representation of the object, for debugging purposes
"""
return "< %s at %s with data: %s >" % (self.__class__.__name__,
id(self),
self._data)
@property
def protectedItems(self):
"""
Provides a cleaner interface to dynamically add items to the models
list of protected functions to not store in the database
"""
return self._protectedItems
@protectedItems.setter
[docs] def protectedItems(self, value):
if type(value) is list:
self._protectedItems.extend(value)
else:
assert type(value) is str
self._protectedItems.append(value)
return self._protectedItems