from __future__ import unicode_literals
import copy
import warnings
from goblin.exceptions import ValidationError
from .strategy import Strategy, SaveAlways, SaveOnce
from .validators import pass_all_validator
DEBUG = False
[docs]class BaseValueManager(object):
"""
Value managers are used to manage values pulled from the database and
track state changes.
These are useful for save strategies.
"""
def __init__(self, graph_property, value, strategy=SaveAlways):
"""
Initialize the value manager.
:param graph_property: The graph property to manage
:type graph_property: goblin.properties.base.GraphProperty
:param value: The initial value of the column
:type value: Object
:param strategy: The callable condition to compare against. If none
given, it won't be used
:type strategy: goblin.properties.strategy.Strategy
"""
self._create_private_fields()
self.graph_property = graph_property
self._previous_value = copy.copy(value)
self.value = value
self.strategy = strategy
if not issubclass(self.strategy, Strategy):
warnings.warn("Condition is not derived from goblin.properties.strategy.Strategy and will be ignored!",
category=SyntaxWarning)
def __repr__(self):
if DEBUG:
return "%s(property=%s, value=%s, previous_value=%s, strategy=%s)" % (self.__class__.__name__,
self.graph_property,
self.value,
self.previous_value,
self.strategy)
else:
return repr(self.value)
def _create_private_fields(self):
self._previous_value = None
@property
def previous_value(self):
return self._previous_value
@previous_value.setter
def previous_value(self, val):
self._previous_value = copy.copy(val)
@property
def deleted(self):
"""
Indicates whether or not this value has been deleted.
:rtype: bool
"""
return self.value is None and self.previous_value is not None
@property
def changed(self):
"""
Indicates whether or not this value has changed.
:rtype: bool
"""
try:
return self.strategy.condition(
self.previous_value, self.value,
has_changed=(self.value != self.previous_value),
graph_property=self.graph_property)
except:
return self.value != self.previous_value
[docs] def getval(self):
"""Return the current value."""
return self.value
[docs] def setval(self, val):
"""
Updates the current value.
:param val: The new value
:type val: mixed
"""
self.value = val
[docs] def delval(self):
"""Delete a given value"""
self.value = None
[docs] def get_property(self):
"""
Returns a value-managed property attributes
:rtype: property
"""
_get = lambda slf: self.getval()
_set = lambda slf, val: self.setval(val)
_del = lambda slf: self.delval()
if self.graph_property.can_delete:
return property(_get, _set, _del)
else:
return property(_get, _set)
[docs]class GraphProperty(object):
"""Base class for graph property types"""
data_type = "Object"
value_manager = BaseValueManager
validator = pass_all_validator
instance_counter = 0
def __init__(self, description=None, primary_key=False, index=False,
index_ext=None, db_field=None, choices=None, default=None,
required=False, save_strategy=SaveAlways, unique=None,
db_field_prefix=''):
"""
Initialize this graph property with the given information.
:param description: description of this field
:type description: basestring | str
:param primary_key: Indicates whether or not this is primary key
:type primary_key: bool
:param index: Indicates whether or not this field is indexed
:type index: bool
:param db_field: The property this field will map to in the database
:type db_field: basestring | str
:param choices: A dict of possible choices where the key is the value
to store, and the value is the user-friendly value
:type choices: tuple(tuple(Object, Object)) |
list[list[Object, Object]] | None
:param default: Value or callable with no args to set default value
:type default: Callable | Number
:param required: Whether or not this field is required
:type required: bool
:param save_strategy: Strategy used when saving the value of the column
:type save_strategy: strategy.Strategy
:param unique: Uniqueness constraint left in for backwards
compatibility -- used by Spec system.
:type unique: bool
:param db_field_prefix: The property prefix associated with the Model.
:type db_field_prefix: basestring | None
"""
self.description = description
self.primary_key = primary_key
self.index = index
self.index_ext = index_ext
self.db_field = db_field
self.db_field_prefix = db_field_prefix
self.default = default
self.required = required
self.save_strategy = save_strategy
self.unique = unique
self.choices = choices
# the graph property name in the model definition
self.property_name = None
# self.value = None
# keep track of instantiation order
self.position = GraphProperty.instance_counter
GraphProperty.instance_counter += 1
def __repr__(self):
return "{}(name={}, pos={}, default={}, db_field_name={})".format(
self.__class__.__name__, self.property_name, self.position,
self.default, self.db_field_name)
@classmethod
[docs] def get_value_from_choices(cls, value, choices):
""" Returns the key for the choices tuple of tuples
Note if you are using classes, they must implement the __in__ and
__eq__ for the logical comparison.
:param value: The raw value to test if it exists in the valid choices.
Could be the key or the value in the dict
:type value: Object
:rtype: Object
"""
if not choices:
return None
for key, val in choices:
if value in (key, val):
return key
return None
[docs] def validate(self, value):
"""
Returns a cleaned and validated value. Raises a ValidationError if
there's a problem
:rtype: Object
"""
if self.choices:
orig_value = value
value = GraphProperty.get_value_from_choices(value, self.choices)
if value is None:
raise ValidationError(
'{} - Value `{}` is not in available choices'.format(
self.db_field_name, orig_value))
if value is None:
if self.has_default:
return self.validator(self.get_default())
elif self.required:
raise ValidationError(
'{} - None values are not allowed'.format(
self.db_field_name))
else:
return None
return self.validator(value)
[docs] def to_python(self, value):
"""
Converts data from the database into python values raises a
ValidationError if the value can't be converted
:rtype: Object
"""
return value
[docs] def to_database(self, value):
"""
Converts python value into database value
:rtype: Object
"""
if value is None and self.has_default:
return self.get_default()
return value
@property
def has_default(self):
"""
Indicates whether or not this graph property has a default value.
:rtype: bool
"""
return self.default is not None
@property
def can_delete(self):
return not self.primary_key
[docs] def should_save(self, first_save=False):
"""
Indicates whether or not the property should be saved based on it's
save strategy.
:rtype: bool
"""
return self.get_save_strategy().condition(
previous_value=self.value_manager.previous_value,
value=self.value_manager.value,
has_changed=self.value_manager.changed, first_save=first_save,
graph_property=self)
[docs] def get_save_strategy(self):
"""
Returns the save strategy attached to this graph property.
:rtype: Callable
"""
return self.save_strategy or (SaveAlways if not self.primary_key
else SaveOnce)
[docs] def get_default(self):
"""
Returns the default value for this graph property if one is available.
:rtype: Object | None
"""
if self.has_default:
if callable(self.default):
return self.default()
else:
return self.default
[docs] def set_property_name(self, name):
"""
Sets the graph property name during document class construction.
This value will be ignored if db_field is set in __init__
:param name: The name of this graph property
:type name: str
"""
self.property_name = name
[docs] def set_db_field_prefix(self, prefix, override=False):
"""
Sets the graph property name prefix during document class construction.
"""
if not override and not self.db_field_prefix:
self.db_field_prefix = prefix.rstrip('_') + '_'
if self.db_field_prefix == '_':
self.db_field_prefix = ''
@property
def has_db_field_prefix(self):
"""
Determines if a field prefix has already been defined.
"""
if self.db_field_prefix not in [None, '']:
return True
return False
@property
def db_field_name(self):
"""
Returns the name of the goblin name of this graph property
:rtype: basestring | str
"""
return (self.db_field_prefix or '') + (self.db_field or
self.property_name or '')