Source code for malibu.design.brine

# -*- coding: utf-8 -*-
import json
import time
import types
import uuid
from difflib import SequenceMatcher

__doc__ = """
malibu.design.brine
-------------------

Brine is a play on Python's pickle module, which is used for
serializing data. Brine is used for serialization as well, but
into JSON, not a binary structure.
"""

# Declare a set of method types that should be filtered for.
METHOD_TYPES = [types.MethodType, types.FunctionType, types.LambdaType]


[docs]def fuzzy_ratio(a, b): """ Compares two values using the SequenceMatcher from difflib. Used for ~approximated~ fuzzy search. :param str a: lhs string :param str b: rhs string :rtype: int :returns: Integer ration of a <=> b """ return SequenceMatcher(None, a, b).ratio()
[docs]class BrineObject(object): """ This object is for use as a base class for other data. Essentially, it will expose a set of members that can be set and then squashed down to a JSON object through a call to to_json. It can also be used as a meta-class for the base of a caching object model or other neat things. """ @classmethod
[docs] def by_json(cls, data, read_only=False, **kw): """ Creates a new instance and calls from_json on the instance. Will take kwargs and pass to the underlying instance initializer. :param class cls: Class method is running on :param str data: JSON string to create object from :param bool read_only: Set object read-only :param dict **kw: Object initializer options :rtype: cls :returns: new BrineObject subclass instance """ inst = cls(**kw) inst.from_json(data) if read_only: inst.read_only() return inst
@classmethod
[docs] def by_dict(cls, data, read_only=False, **kw): """ Creates a new instance with fields from the data parameter as long as they match what is in _fields. Also does recursion on nested Brine objects. :param class cls: BrineObject subclass :param dict data: Dictionary to use for fields :param bool read_only: Set object read-only :param dict **kw: BrineObject initializer options :rtype: BrineObject :returns: BrineObject subclass instance :raises TypeError: If data is not dict type """ if not isinstance(data, dict): raise TypeError("Data parameter must be a dict.") o = cls(**kw) o.from_dict(data) if read_only: o.read_only() return o
def __init__(self, *args, **kw): # Do this because MRO. super(BrineObject, self).__init__() # Disable custom __setattr__ for the meantime. self._initialized = False self._read_only = False # For now, lets make this simple and treat fields with no special # syntax (underlines, mainly) as our schema. self._special_fields = ["timestamp", "uuid"] self._fields = [] for field in dir(self): if field.startswith("_"): continue # Also, make sure this isn't a function. if type(getattr(self, field)) in METHOD_TYPES: continue self._fields.append(field) if kw.get("timestamp", False): self.timestamp = int(time.time()) else: self._special_fields.remove("timestamp") if kw.get("uuid", False): self.uuid = str(uuid.uuid4()) else: self._special_fields.remove("uuid") self._initialized = True def __setattr__(self, attr, value): """ Allows setting local values, obviously. Mainly ensures that nested BrineObjects do *not* get clobbered, only modified. :param str attr: Local instance attribute to set :param object value: Value of instance attribute :rtype: None :returns: None :raises AttributeError: Immutable or non-existent field :raises TypeError: Overwriting function type with object """ # I wish this didn't have to be a special case. if attr == "_initialized": self.__dict__[attr] = value return # Check that init has finished. if not getattr(self, "_initialized", False): self.__dict__[attr] = value return if attr in self._special_fields: raise AttributeError("Field {} is immutable.".format(attr)) elif attr not in self._fields: if attr.startswith("_"): # Untracked instance variable if attr in ["_fields", "_special_fields"]: # These fields can NOT be overwritten raise AttributeError("Field {} is immutable.".format(attr)) else: raise AttributeError("Field {} does not exist.".format(attr)) elif attr in self._fields and self._read_only: raise AttributeError("Tracked fields are read-only.") # Verify that the set *will not* overwrite a method or Brine object. _attr_cur = getattr(self, attr, None) if type(_attr_cur) in METHOD_TYPES: raise TypeError("Function {} can not be overwritten.".format(attr)) elif isinstance(_attr_cur, BrineObject): raise AttributeError("Brine object {} can't be clobbered.".format( attr)) # Set the variable in the dictionary. self.__dict__[attr] = value
[docs] def as_dict(self): """ Returns the dictionary representation of the fields in this object. :rtype: dict :returns: Current object in dictionary form """ obj = {} for val in self._fields + self._special_fields: if not hasattr(self, val): continue # Also, make sure this isn't a function. if type(getattr(self, val)) in METHOD_TYPES: continue attr = getattr(self, val) if isinstance(attr, BrineObject): obj.update({val: attr.as_dict()}) else: obj.update({val: getattr(self, val)}) return obj
[docs] def to_json(self): """ Converts the object into JSON form. Simple, right? :rtype: str :returns: Current object in JSON string form. """ return json.dumps(self.as_dict())
[docs] def from_json(self, data): """ Converts the JSON data back into an object, then loads the data into the model instance. NOTE: This changes the current model *in-place*! :param str data: JSON string to import :rtype: None :returns: None """ obj = json.loads(data) if not isinstance(obj, dict): raise TypeError("Expected JSON serialized dictionary, not %s" % ( type(obj))) for k, v in obj.items(): # We need to make sure the data is sanitized a little bit. if k.startswith("_") and k not in self._special_fields: continue if k in self._fields: fval = getattr(self, k, None) if isinstance(fval, BrineObject): fval.from_json(json.dumps(v)) else: setattr(self, k, v)
[docs] def from_dict(self, data): """ Creates a new instance with fields from the data parameter as long as they match what is in _fields. Also does recursion on nested Brine objects. NOTE: Modifies the BrineObject *in-place*! If there are recursive objects also provided in the dictionary that are defined on the original object, they will also be modified *in-place*! Keys prefixed by an underscore will be inserted into the object, but will not be tracked in _fields or _special_fields. WARNING: This silently ignores "bad" fields. :param dict data: Dictionary to use for fields :rtype: None :returns: None :raises TypeError: If data is not dict type """ if not isinstance(data, dict): raise TypeError("Data parameter must be a dict.") for k, v in data.items(): if k.startswith("_"): # Untracked instance variable if k in ["_fields", "_special_fields"]: # These vars can NOT be overwritten continue else: setattr(self, k, v) continue if k not in self._fields + self._special_fields: continue if isinstance(getattr(self, k, None), BrineObject): cl = getattr(self, k) cl.from_dict(v) continue if type(getattr(self, k)) in METHOD_TYPES: continue setattr(self, k, v)
[docs] def read_only(self): """ Set object as read-only. After an object is set read-only, it can not be unset as read-only. :rtype: None :returns: None """ self._read_only = True
[docs]class CachingBrineObject(BrineObject): """ This is a magical class that performs the same function as the BrineObject, but it also adds object caching, searching, and fuzzy searching on the cache. Also provided is cached field invalidation / "dirtying". """ # Ratio for fuzzy search. Closer to 1.0 means stricter results. _FUZZ_RATIO = 0.535 @classmethod def _initialize_cache(cls): """ Initialize a class-level cache to store Json models for cache and searching purposes. """ if not hasattr(cls, "_CachingBrineObject__cache"): cls.__cache = [] @classmethod @classmethod
[docs] def search(cls, ignore_case=False, **kw): """ Searches through the cache to find objects with field that match those given by the **kw. Note that this can return more than one object. :param bool ignore_case: Should search ignore case? :param dict **kw: Fields to search :rtype: list :returns: List of matching CachingBrineObjects """ result = [] cls._initialize_cache() for k, v in kw.items(): for obj in cls.__cache: ob_value = getattr(obj, k, None) if ignore_case: if isinstance(v, str) and isinstance(ob_value, str): r = (v.lower() == ob_value.lower()) else: r = (v == ob_value) if r: if obj in result: continue else: result.append(obj) else: continue return result
def __init__(self, *args, **kw): # Call the parent initializer. super(CachingBrineObject, self).__init__(self, *args, **kw) self._initialized = False # Make sure the cache is initialized. self._initialize_cache() # The "dirty" cache list is just a list of fields that have been # updated. self.__dirty = [] # Throw this object into the cache. self.__cache.append(self) # Let the attribute handler know we're done loading. self._initialized = True def __setattr__(self, attr, value): """ Sets local fields and determines if the cache needs to be marked dirty for that set and ensures that the value can actually be set. :param str attr: Local instance attribute to set :param object value: Value of instance attribute :rtype: None :returns: None :raises AttributeError: Immutable of non-existent field :raises TypeError: Overwriting function type with object """ # I wish this didn't have to be a special case. if attr == "_initialized": self.__dict__[attr] = value return # Check that init has finished. if not getattr(self, "_initialized", False): self.__dict__[attr] = value return # Check various conditions used to determine if a variable has been # dirtied or can be set. if attr in self._fields: if self._read_only: raise AttributeError("Tracked fields are read-only.") if attr not in self.__dirty: self.__dirty.append(attr) elif attr in self._special_fields: raise AttributeError("Special field {} is immutable.".format(attr)) elif attr in ["_fields", "_special_fields"]: raise AttributeError("Field {} is immutable.".format(attr)) elif attr not in self.__dict__: if not attr.startswith("_"): # Untracked instance variable raise AttributeError("Field {} does not exist.".format(attr)) # Verify that the set *will not* overwrite a method. _attr_cur = getattr(self, attr, None) if type(_attr_cur) in METHOD_TYPES: raise TypeError("Function {} can not be overwritten.".format(attr)) elif isinstance(_attr_cur, BrineObject): raise AttributeError("Brine object {} can't be clobbered.".format( attr)) # Set the variable in the dictionary. self.__dict__[attr] = value
[docs] def uncache(self): """ Removes the object from the state cache forcibly. :rtype: None :returns: None """ self.__cache.remove(self)
[docs] def unmark(self, *fields): """ Unmarks some field as dirty. Should only be called after the upstream is updated or only if you know what you're doing! :param list *fields: Fields to unmark :rtype: None :returns: None """ for field in fields: if field not in self.__dirty: continue self.__dirty.remove(field)
[docs] def dirty_dict(self): """ Dumps a dictionary of dirty fields. :rtype: dict :returns: Dictionary of all *dirty* values """ obj = {} for val in self.__dirty: if not hasattr(self, val): continue # Also, make sure this isn't a function. if type(getattr(self, val)) in METHOD_TYPES: continue obj.update({val: getattr(self, val)}) return obj
[docs] def dirty_json(self): """ Dumps the dirty dictionary as JSON. :rtype: str :returns: JSON dictionary of dirty values """ return json.dumps(self.dirty_dict())