Source code for zktools.node

# ***** BEGIN LICENSE BLOCK *****
# Version: MPL 1.1/GPL 2.0/LGPL 2.1
#
# The contents of this file are subject to the Mozilla Public License Version
# 1.1 (the "License"); you may not use this file except in compliance with
# the License. You may obtain a copy of the License at
# http://www.mozilla.org/MPL/
#
# Software distributed under the License is distributed on an "AS IS" basis,
# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
# for the specific language governing rights and limitations under the
# License.
#
# The Original Code is Mozilla zktools.
#
# The Initial Developer of the Original Code is Mozilla Foundation.
# Portions created by the Initial Developer are Copyright (C) 2011
# the Initial Developer. All Rights Reserved.
#
# Contributor(s):
# Ben Bangert (bbangert@mozilla.com)
#
# Alternatively, the contents of this file may be used under the terms of
# either the GNU General Public License Version 2 or later (the "GPL"), or
# the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
# in which case the provisions of the GPL or the LGPL are applicable instead
# of those above. If you wish to allow use of your version of this file only
# under the terms of either the GPL or the LGPL, and not to allow others to
# use your version of this file under the terms of the MPL, indicate your
# decision by deleting the provisions above and replace them with the notice
# and other provisions required by the GPL or the LGPL. If you do not delete
# the provisions above, a recipient may use your version of this file under
# the terms of any one of the MPL, the GPL or the LGPL.
#
# ***** END LICENSE BLOCK *****
"""Zookeeper Nodes

This module provides a :class:`ZkNode` object which can load itself from
a Zookeeper path, and serialize itself back.

"""
import datetime
import decimal
import json
import re
import threading

import zookeeper

ZOO_OPEN_ACL_UNSAFE = {"perms": 0x1f, "scheme": "world", "id": "anyone"}


CONVERSIONS = {
    re.compile(r'^\d+\.\d+$'): decimal.Decimal,
    re.compile(r'^\d+$'): int,
    re.compile(r'^true$', re.IGNORECASE): lambda x: True,
    re.compile(r'^false$', re.IGNORECASE): lambda x: False,
    re.compile(r'^None$', re.IGNORECASE): lambda x: None,
    re.compile(r'^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d+Z$'):
       lambda x: datetime.datetime.strptime(x, '%Y-%m-%dT%H:%M:%S.%fZ'),
    re.compile(r'^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d+Z$'):
       lambda x: datetime.datetime.strptime(x, '%Y-%m-%d %H:%M:%S.%fZ'),
    re.compile(r'^\d{4}-\d{2}-\d{2}$'):
       lambda x: datetime.datetime.strptime(x, '%Y-%m-%d'),
}

JSON_REGEX = re.compile(r'^[\{\[].*[\}\]]$')


def _load_value(value, use_json=False):
    """Convert a saved value to the best Python match"""
    for regex, convert in CONVERSIONS.iteritems():
        if regex.match(value):
            return convert(value)
    if use_json and JSON_REGEX.match(value):
        try:
            return json.loads(value)
        except ValueError:
            return value
    return value


def _save_value(value, use_json=False):
    """Convert a Python object to the best string repr"""
    # Float is all we care about, as we lose float precision
    # when calling str on it
    if isinstance(value, float):
        return repr(value)
    elif isinstance(value, datetime.datetime):
        return value.strftime('%Y-%m-%dT%H:%M:%S.%fZ')
    elif isinstance(value, datetime.date):
        return value.strftime('%Y-%m-%d')
    elif use_json and isinstance(value, (dict, list)):
        return json.dumps(value)
    else:
        return str(value)


[docs]class ZkNode(object): """Zookeeper Node This object provides access to a single node for updating the value that can also track changes to the value from Zookeeper. The value of the node is coerced into an appropriate Python object when loaded, current supported conversions:: Numbers with decimals -> Decimal Numbers without decimals -> Int true/false -> Bool none -> None ISO 8601 Date and Datetime -> date or datetime And optionally, with use_json:: JSON string -> dict/list .. note:: The JSON determination is extremely lax, if its a string that starts and ends with brackets or curley marks, its assumed to be a JSON object and will be coerced if possible. If coercion fails, the string will be returned as is. Example:: from zktools.connection import ZkConnection from zktools.configuration import ZkNode conn = ZkConnection() node = ZkNode(conn, '/some/config/node') # prints out the current value, defaults to None print node.value # Set the value in zookeeper node.value = 483.24 The default behavior is to track changes to the node, so that the ``value`` attribute always reflects the node's value in Zookeeper. .. warning:: **Do not delete nodes that are in use**, there is purposely no code to handle such situations as it creates overly complex scenarios both for ZkNode to handle, and for application code using it to deal with. """
[docs] def __init__(self, connection, path, default=None, use_json=False): """Create a Zookeeper Node Creating a ZkNode by default attempts to load the value, and if its not found will automatically create a blank string as the value. In the event the node is deleted once this object is deleted once its being tracked, the ``deleted`` attribute will be ``True``. :param connection: zookeeper connection object :type connection: ZkConnection instance :param path: Path to the Zookeeper node :type path: str :param default: A default value if the node is being created :param use_json: Whether values that look like a JSON object should be deserialized, and dicts/lists saved as JSON. :type use_json: bool """ self._zk = connection self._path = path self._cv = threading.Condition() self._use_json = use_json self._value = None self._reload = False with self._cv: if not connection.exists(path, self._created_watcher): self._zk.create(self._path, _save_value(default, use_json=use_json), [ZOO_OPEN_ACL_UNSAFE], 0) # Wait for the node to actually be created self._cv.wait() self._load()
def _created_watcher(self, handle, type, state, path): """Watch for our node to be created before continuing""" with self._cv: if type == zookeeper.CREATED_EVENT: self._cv.notify_all() def _node_watcher(self, handle, type, state, path): """Watch a node for updates""" with self._cv: if type == zookeeper.CHANGED_EVENT: data = self._zk.get(self._path, self._node_watcher)[0] self._value = _load_value(data, use_json=self._use_json) elif type in (zookeeper.EXPIRED_SESSION_STATE, zookeeper.AUTH_FAILED_STATE): self._reload = True self._cv.notify_all() def _load(self): """Load data from the node, and coerce as necessary""" with self._cv: data = self._zk.get(self._path, self._node_watcher)[0] self._value = _load_value(data, use_json=self._use_json) @property def value(self): """Returns the current value If the Zookeeper session expired, it will be reconnected and the value reloaded. """ if self._reload: self._load() self._reload = False return self._value @value.setter
[docs] def value(self, value): """Set the value with a new one :param value: The value of the node :type value: Any str'able object """ with self._cv: self._zk.set( self._path, _save_value(value, use_json=self._use_json)) # Now wait to see that it triggered our change event self._cv.wait()