Source code for bulbs.model
# -*- coding: utf-8 -*-
#
#James Thornton (http://jamesthornton.com)
# BSD License (see LICENSE for details)
#
"""
Base classes for modeling domain objects that wrap vertices and edges.
"""
import six # Python 3
import inspect
import types
from collections import Callable
from bulbs.property import Property
from bulbs.element import Element, Vertex, VertexProxy, Edge, EdgeProxy, \
coerce_vertices, build_data
from bulbs.utils import initialize_element, get_logger
# Model Modes
NORMAL = 1
STRICT = 2
log = get_logger(__name__)
class ModelMeta(type):
"""Metaclass used to set database Property definitions on Models."""
def __init__(cls, name, base, namespace):
"""Store Property instance definitions on the class as a dictionary."""
# Get inherited Properties
cls._properties = cls._get_initial_properties()
# Add new Properties
cls._register_properties(namespace)
def _get_initial_properties(cls):
"""
Get Properties defined in the parent and inherit them.
:rtype: dict
"""
try:
parent_properties = getattr(cls, '_properties')
properties = parent_properties.copy()
except:
# Set it to an empty dict if the Model doesn't have a parent Model.
properties = {}
return properties
def _register_properties(cls, namespace):
"""
Loop through the class namespace looking for Property instances.
:param namespace: Class namespace
:type namespace: dict
:rtype: None
"""
# e.g. age = Integer()
for key in namespace: # Python 3
value = namespace[key]
assert key not in cls._properties, \
"Can't redefine Property '%s'" % key
if isinstance(value, Property):
property_instance = value # for clarity
cls._properties[key] = property_instance
cls._set_property_name(key,property_instance)
cls._initialize_property(key,property_instance)
# not doing this b/c some Properties are calculated at savetime
#delattr(cls, key)
def _set_property_name(cls, key, property_instance):
"""
Set Property name to attribute key unless explicitly set via kwd param.
:param key: Class attribute key
:type key: str
:param property_instance: Property instance
:type property_instance bulbs.property.Property
:rtype None
"""
if property_instance.name is None:
property_instance.name = key
def _initialize_property(cls, key, property_instance):
"""
Set the Model class attribute based on the Property definition.
:param key: Class attribute key
:type key: str
:param property_instance: Property instance
:type property_instance bulbs.property.Property
"""
if property_instance.fget:
# TODO: make this configurable
# this is a calculated property (should it persist?)
# wrapped fset and fdel in str() to make the default None not
# error on getattr
fget = getattr(cls, property_instance.fget)
# TODO: implement fset and fdel (maybe)
#fset = getattr(cls, str(property_instance.fset), None)
#fdel = getattr(cls, str(property_instance.fdel), None)
fset = None
fdel = None
property_value = property(fget, fset, fdel)
else:
property_value = None
setattr(cls, key, property_value)
class Model(six.with_metaclass(ModelMeta, object)): # Python 3
"""Abstract base class for Node and Relationship container classes."""
#: The mode for saving attributes to the database.
#: If set to STRICT, only defined Properties are saved.
#: If set to NORMAL, all attributes are saved.
#: Defaults to NORMAL.
__mode__ = NORMAL
#: A dict containing the database Property instances.
_properties = None
def __setattr__(self, key, value):
"""
Set model attributes, possibly coercing database Properties to the
defined types.
:param key: Attribute key
:type key: str
:param value: Attribute value
:type value: object
:rtype: None
"""
if key in self._properties:
self._set_database_property(key, value)
else:
# If _mode = STRICT, set an instance var, which isn't saved to DB.
# If _mode = NORMAL, store in self._data, which is saved to DB
self._set_normal_attribute(key, value)
def _set_database_property(self, key, value):
"""
Set Property attributes after coercing them into the defined types.
:param key: Attribute key
:type key: str
:param value: Attribute value
:type value: object
:rtype: None
"""
# we want Model Properties to be set be set as actual attributes
# because they can be real Python propertes or calculated values,
# which are calcualted/set upon each save().
# Don't set calculated (fget) properties; they're calculated at save.
if not self._is_calculated_property(key):
value = self._coerce_property_value(key, value)
object.__setattr__(self, key, value)
def _set_normal_attribute(self, key, value):
"""
Set normal/non-database Property attributes, depending on the __mode__.
:param key: Attribute key
:type key: str
:param value: Attribute value
:type value: object
:rtype: None
"""
if self.__mode__ == STRICT:
# Set as a Python attribute, which won't be saved to the database.
object.__setattr__(self, key, value)
else:
# Store the attribute in self._data, which are saved to database.
Element.__setattr__(self, key, value)
def _is_calculated_property(self, key):
"""
Returns True if the Property is a cacluated property, i.e. has fget set.
:param key: Attribute key
:type key: str
:rtype: bool
"""
# TODO: fget works, but fset, fdel have not been tested
property_instance = self._properties[key]
return (property_instance.fget is not None)
def _coerce_property_value(self, key, value):
"""
Coerce database Property value into its defined type.
:param key: Attribute key
:type key: str
:param value: Attribute value
:type value: object
:rtype: object
"""
if value is not None:
property_instance = self._properties[key]
value = property_instance.coerce(key, value)
return value
def _set_property_defaults(self):
"""
Set the default values for all the database Properties.
:rtype: None
"""
for key in self._properties:
default_value = self._get_property_default(key)
setattr(self, key, default_value)
def _get_property_default(self, key):
"""
Coerce database Property value into its defined type.
:param key: Attribute key
:type key: str
:rtype: object
"""
# TODO: make this work for model methods?
# The value entered could be a scalar or a function name
# Should we defer the call until all properties are set,
# or only for calculated properties?
property_instance = self._properties[key]
default_value = property_instance.default
if isinstance(default_value, Callable):
default_value = default_value()
return default_value
def _set_keyword_attributes(self, _data, kwds):
"""
Sets Python attributes using the _data and keywords passed in by user.
:param _data: Data that was passed in via a dict.
:type _data: dict
:param kwds: Data that was passed in via name/value pairs.
:type kwds: dict
:rtype: None
"""
# NOTE: keys may have been passed in that are not defined as Properties
data = build_data(_data, kwds)
for key in data: # Python 3
value = data[key]
# Notice that __setattr__ is overloaded
setattr(self, key, value)
def _set_property_data(self):
"""
Sets Property data after it is retrieved from the DB.
:rtype: None
.. note:: Sets the value to None if it's an invalid type.
"""
type_system = self._client.type_system
for key in self._properties: # Python 3
# Don't set calculted property values, i.e. those with fset defined.
if self._is_calculated_property(key): continue
property_instance = self._properties[key]
#name = property_instance.name
value = self._data.get(key, None)
value = property_instance.convert_to_python(type_system, key, value)
# TODO: Maybe need to wrap this in try/catch too.
# Notice that __setattr__ is overloaded. No need to coerce it twice.
object.__setattr__(self, key, value)
def _get_property_data(self):
"""
Returns validated Property data, ready to be saved in the DB.
:rtype: dict
"""
# If __mode__ is STRICT, data set to empty; otherwise set to self._data
data = self._get_initial_data()
type_var = self._client.config.type_var
type_system = self._client.type_system
if hasattr(self, type_var):
# Add element_type to the database properties to be saved;
# but don't worry about "label", it's always saved on the edge.
data[type_var] = object.__getattribute__(self, type_var)
# Convert database Property values to their database types.
for key in self._properties: # Python 3
property_instance = self._properties[key]
value = self._get_property_value(key)
property_instance.validate(key, value)
#name = property_instance.name
db_value = property_instance.convert_to_db(type_system, key, value)
data[key] = db_value
return data
def _get_initial_data(self):
"""
Returns empty dict if __mode__ is set to STRICT, otherwise self._data.
:rtype: dict
"""
data = {} if self.__mode__ == STRICT else self._data.copy()
return data
def _get_property_value(self, key):
"""
Returns the value of a Property, calculated via a function if needed.
:param key: Attribute key
:type key: str
:rtype: object
"""
# Notice that __getattr__ is overloaded in Element.
value = object.__getattribute__(self, key)
if isinstance(value, Callable):
return value()
return value
def get_bundle(self, _data=None, **kwds):
"""
Returns a tuple contaning the property data, index name, and index keys.
:param _data: Data that was passed in via a dict.
:type _data: dict
:param kwds: Data that was passed in via name/value pairs.
:type kwds: dict
:rtype: tuple
"""
self._set_property_defaults()
self._set_keyword_attributes(_data, kwds)
data = self._get_property_data()
index_name = self.get_index_name(self._client.config)
keys = self.get_index_keys()
return data, index_name, keys
def get_index_keys(self):
"""
Returns Property keys to index in DB. Defaults to None (index all keys).
:rtype: list or None
"""
# TODO: Derive this from Property definitions.
return None
def get_property_keys(self):
"""
Returns a list of all the Property keys.
:rtype: list
"""
return self._properties.keys()
def data(self):
"""
Returns a the element's property data.
:rtype: dict
"""
data = dict()
if self.__mode__ == NORMAL:
data = self._data
for key in self._properties:
# TODO: make this work for calculated values.
# Calculated props shouldn't be stored, but components should be.
data[key] = object.__getattribute__(self, key)
return data
def map(self):
"""
Deprecated. Returns the element's property data.
:rtype: dict
"""
log.debug("This is deprecated; use data() instead.")
return self.data()
def __check__(self,data):
"""
Override this method in the child class to throw an exception if the data dictionary is invalid
:param data: Collection of parameters to be set for this Model
:type data: dict
"""
pass
[docs]class Node(Model, Vertex): """ Abstract base class used for creating a Vertex Model. It's used to create classes that model domain objects, and it's not meant to be used directly. To use it, create a subclass specific to the type of data you are storing. Example model declaration:: # people.py from bulbs.model import Node from bulbs.property import String, Integer class Person(Node): element_type = "person" name = String(nullable=False) age = Integer() Example usage:: >>> from people import Person >>> from bulbs.neo4jserver import Graph >>> g = Graph() # Add a "people" proxy to the Graph object for the Person model: >>> g.add_proxy("people", Person) # Use it to create a Person node, which also saves it in the database: >>> james = g.people.create(name="James") >>> james.eid 3 >>> james.name 'James' # Get the node (again) from the database by its element ID: >>> james = g.people.get(james.eid) # Update the node and save it in the database: >>> james.age = 34 >>> james.save() # Lookup people using the Person model's primary index: >>> nodes = g.people.index.lookup(name="James") """ #: The mode for saving attributes to the database. #: If set to STRICT, only defined Properties are saved. #: If set to NORMAL, all attributes are saved. #: Defaults to NORMAL. __mode__ = NORMAL #: A dict containing the database Property instances. _properties = None element_type = None @classmethod
[docs] def get_element_type(cls, config): """ Returns the element type. :param config: Config object. :type config: bulbs.config.Config :rtype: str """ element_type = getattr(cls, config.type_var) return element_type
@classmethod[docs] def get_element_key(cls, config): """ Returns the element key. :param config: Config object. :type config: bulbs.config.Config :rtype: str """ return cls.get_element_type(config)
@classmethod[docs] def get_index_name(cls, config): """ Returns the index name. :param config: Config object. :type config: bulbs.config.Config :rtype: str """ return cls.get_element_type(config)
@classmethod[docs] def get_proxy_class(cls): """ Returns the proxy class. :param config: Config object. :type config: bulbs.config.Config :rtype: class """ return NodeProxy
[docs] def save(self): """ Saves/updates the element's data in the database. :rtype: None """ data = self._get_property_data() self.__check__(data) index_name = self.get_index_name(self._client.config) keys = self.get_index_keys() self._client.update_indexed_vertex(self._id, data, index_name, keys) # # Override the _create and _update methods to cusomize behavior. #
def _create(self, _data, kwds): """ Creates a vertex in the database; called by the NodeProxy create() method. :param _data: Optional property data dict. :type _data: dict :param kwds: Optional property data keyword pairs. :type kwds: dict :rtype: None """ # bundle is an OrderedDict containing data, index_name, and keys data, index_name, keys = self.get_bundle(_data, **kwds) self.__check__(data) resp = self._client.create_indexed_vertex(data, index_name, keys) result = resp.one() self._initialize(result) def _update(self, _id, _data, kwds): """ Updates a vertex in the database; called by NodeProxy update() method. :param _id: Element ID. :param _id: int or str :param _data: Optional property data dict. :type _data: dict :param kwds: Optional property data keyword pairs. :type kwds: dict :rtype: None """ data, index_name, keys = self.get_bundle(_data, **kwds) self.__check__(data) resp = self._client.update_indexed_vertex(_id, data, index_name, keys) result = resp.one() self._initialize(result)[docs] def _initialize(self, result): """ Initializes the element. Initialize all non-DB attributes here. :param result: Result object. :type result: Result :rtype: None ..note:: Called by _create, _update, and utils.initialize_element. """ Vertex._initialize(self,result) self._initialized = False self._set_property_data() self._initialized = True
[docs]class Relationship(Model, Edge): """ Abstract base class used for creating a Relationship Model. It's used to create classes that model domain objects, and it's not meant to be used directly. To use it, create a subclass specific to the type of data you are storing. Example usage for an edge between a blog entry node and its creating user:: # people.py from bulbs.model import Relationship from bulbs.properties import DateTime from bulbs.utils import current_timestamp class Knows(Relationship): label = "knows" created = DateTime(default=current_timestamp, nullable=False) Example usage:: >>> from people import Person, Knows >>> from bulbs.neo4jserver import Graph >>> g = Graph() # Add proxy interfaces to the Graph object for each custom Model >>> g.add_proxy("people", Person) >>> g.add_proxy("knows", Knows) # Create two Person nodes, which are automatically saved in the DB >>> james = g.people.create(name="James") >>> julie = g.people.create(name="Julie") # Create a "knows" relationship between James and Julie: >>> knows = g.knows.create(james,julie) >>> knows.timestamp # Get the people James knows (the outgoing vertices labeled "knows") >>> friends = james.outV('knows') """ label = None @classmethod
[docs] def get_label(cls, config): """ Returns the edge's label. :param config: Config object. :type config: bulbs.config.Config :rtype: str """ label = getattr(cls, config.label_var) return label
@classmethod[docs] def get_element_key(cls, config): """ Returns the element key. :param config: Config object. :type config: bulbs.config.Config :rtype: str """ return cls.get_label(config)
@classmethod[docs] def get_index_name(cls, config): """ Returns the index name. :param config: Config object. :type config: bulbs.config.Config :rtype: str """ return cls.get_label(config)
@classmethod[docs] def get_proxy_class(cls): """ Returns the proxy class. :param config: Config object. :type config: bulbs.config.Config :rtype: class """ return RelationshipProxy
[docs] def save(self): """ Saves/updates the element's data in the database. :rtype: None """ data = self._get_property_data() self.__check__(data) index_name = self.get_index_name(self._client.config) keys = self.get_index_keys() self._client.update_indexed_edge(self._id, data, index_name, keys) # # Override the _create and _update methods to customize behavior. #
def _create(self, outV, inV, _data, kwds): """ Creates an edge in the DB; called by RelatinshipProxy create() method. :param _data: Optional property data dict. :type _data: dict :param kwds: Optional property data keyword pairs. :type kwds: dict :rtype: None """ label = self.get_label(self._client.config) outV, inV = coerce_vertices(outV, inV) data, index_name, keys = self.get_bundle(_data, **kwds) self.__check__(data) resp = self._client.create_indexed_edge(outV, label, inV, data, index_name, keys) result = resp.one() self._initialize(result) def _update(self, _id, _data, kwds): """ Updates an edge in DB; called by RelationshipProxy update() method. :param _id: Element ID. :param _id: int or str :param _data: Optional property data dict. :type _data: dict :param kwds: Optional property data keyword pairs. :type kwds: dict :rtype: None """ data, index_name, keys = self.get_bundle(_data, **kwds) self.__check__(data) resp = self._client.update_indexed_edge(_id, data, index_name, keys) result = resp.one() self._initialize(result)[docs] def _initialize(self,result): """ Initializes the element. Initialize all non-DB attributes here. :param result: Result object. :type result: Result :rtype: None ..note:: Called by _create, _update, and utils.initialize_element. """ Edge._initialize(self,result) self._initialized = False self._set_property_data() self._initialized = True
[docs]class NodeProxy(VertexProxy):
[docs] def create(self, _data=None, **kwds): """ Adds a vertex to the database and returns it. :param _data: Optional property data dict. :type _data: dict :param kwds: Optional property data keyword pairs. :type kwds: dict :rtype: Node """ node = self.element_class(self.client) node._create(_data, kwds) return node
[docs] def update(self, _id, _data=None, **kwds): """ Updates an element in the graph DB and returns it. :param _id: The vertex ID. :type _id: int or str :param _data: Optional property data dict. :type _data: dict :param kwds: Optional property data keyword pairs. :type kwds: dict :rtype: Node """ node = self.element_class(self.client) node._update(_id, _data, kwds) return node
[docs] def get_all(self): """ Returns all the elements for the model type. :rtype: Node generator """ config = self.client.config type_var = config.type_var element_type = self.element_class.get_element_type(config) return self.index.lookup(type_var,element_type)
[docs] def get_property_keys(self): """ Returns a list of all the Property keys. :rtype: list """ return self.element_class._properties.keys()
[docs]class RelationshipProxy(EdgeProxy):
[docs] def create(self, outV, inV, _data=None, **kwds): """ Creates an edge in the database and returns it. :param outV: The outgoing vertex. :type outV: Vertex or int :param inV: The incoming vertex. :type inV: Vertex or int :param _data: Optional property data dict. :type _data: dict :param kwds: Optional property data keyword pairs. :type kwds: dict :rtype: Relationship """ relationship = self.element_class(self.client) relationship._create(outV, inV, _data, kwds) return relationship
[docs] def update(self, _id, _data=None, **kwds): """ Updates an edge in the database and returns it. :param _id: The edge ID. :type _id: int or str :param _data: Optional property data dict. :type _data: dict :param kwds: Optional property data keyword pairs. :type kwds: dict :rtype: Relationship """ relationship = self.element_class(self.client) relationship._update(_id, _data, kwds) return relationship
[docs] def get_all(self): """ Returns all the relationships for the label. :rtype: Relationship generator """ # TODO: find a blueprints method that returns all edges for a given # label because you many not want to index edges config = self.client.config label_var = config.label_var label = self.element_class.get_label(config) return self.index.lookup(label_var,label)
[docs] def get_property_keys(self): """ Returns a list of all the Property keys. :rtype: list """ return self.element_class._properties.keys()