Просмотр исходного кода

[desktop] Import kazoo-2.0 into ext-py

Erick Tryzelaar 11 лет назад
Родитель
Сommit
6dac09c3f2
95 измененных файлов с 14091 добавлено и 0 удалено
  1. 548 0
      desktop/core/ext-py/kazoo-2.0/CHANGES.rst
  2. 81 0
      desktop/core/ext-py/kazoo-2.0/CONTRIBUTING.rst
  3. 176 0
      desktop/core/ext-py/kazoo-2.0/LICENSE
  4. 12 0
      desktop/core/ext-py/kazoo-2.0/MANIFEST.in
  5. 604 0
      desktop/core/ext-py/kazoo-2.0/PKG-INFO
  6. 27 0
      desktop/core/ext-py/kazoo-2.0/README.rst
  7. 5 0
      desktop/core/ext-py/kazoo-2.0/debian/changelog
  8. 1 0
      desktop/core/ext-py/kazoo-2.0/debian/compat
  9. 50 0
      desktop/core/ext-py/kazoo-2.0/debian/control
  10. 13 0
      desktop/core/ext-py/kazoo-2.0/debian/copyright
  11. 1 0
      desktop/core/ext-py/kazoo-2.0/debian/docs
  12. 1 0
      desktop/core/ext-py/kazoo-2.0/debian/python-kazoo.install
  13. 18 0
      desktop/core/ext-py/kazoo-2.0/debian/rules
  14. 1 0
      desktop/core/ext-py/kazoo-2.0/debian/source/format
  15. 153 0
      desktop/core/ext-py/kazoo-2.0/docs/Makefile
  16. 28 0
      desktop/core/ext-py/kazoo-2.0/docs/api.rst
  17. 35 0
      desktop/core/ext-py/kazoo-2.0/docs/api/client.rst
  18. 74 0
      desktop/core/ext-py/kazoo-2.0/docs/api/exceptions.rst
  19. 18 0
      desktop/core/ext-py/kazoo-2.0/docs/api/handlers/gevent.rst
  20. 20 0
      desktop/core/ext-py/kazoo-2.0/docs/api/handlers/threading.rst
  21. 33 0
      desktop/core/ext-py/kazoo-2.0/docs/api/interfaces.rst
  22. 24 0
      desktop/core/ext-py/kazoo-2.0/docs/api/protocol/states.rst
  23. 19 0
      desktop/core/ext-py/kazoo-2.0/docs/api/recipe/barrier.rst
  24. 19 0
      desktop/core/ext-py/kazoo-2.0/docs/api/recipe/counter.rst
  25. 14 0
      desktop/core/ext-py/kazoo-2.0/docs/api/recipe/election.rst
  26. 19 0
      desktop/core/ext-py/kazoo-2.0/docs/api/recipe/lock.rst
  27. 16 0
      desktop/core/ext-py/kazoo-2.0/docs/api/recipe/partitioner.rst
  28. 25 0
      desktop/core/ext-py/kazoo-2.0/docs/api/recipe/party.rst
  29. 29 0
      desktop/core/ext-py/kazoo-2.0/docs/api/recipe/queue.rst
  30. 29 0
      desktop/core/ext-py/kazoo-2.0/docs/api/recipe/watchers.rst
  31. 24 0
      desktop/core/ext-py/kazoo-2.0/docs/api/security.rst
  32. 12 0
      desktop/core/ext-py/kazoo-2.0/docs/api/testing.rst
  33. 114 0
      desktop/core/ext-py/kazoo-2.0/docs/async_usage.rst
  34. 465 0
      desktop/core/ext-py/kazoo-2.0/docs/basic_usage.rst
  35. 1 0
      desktop/core/ext-py/kazoo-2.0/docs/changelog.rst
  36. 257 0
      desktop/core/ext-py/kazoo-2.0/docs/conf.py
  37. 12 0
      desktop/core/ext-py/kazoo-2.0/docs/glossary.rst
  38. 45 0
      desktop/core/ext-py/kazoo-2.0/docs/implementation.rst
  39. 106 0
      desktop/core/ext-py/kazoo-2.0/docs/index.rst
  40. 14 0
      desktop/core/ext-py/kazoo-2.0/docs/install.rst
  41. 190 0
      desktop/core/ext-py/kazoo-2.0/docs/make.bat
  42. 81 0
      desktop/core/ext-py/kazoo-2.0/docs/testing.rst
  43. 1 0
      desktop/core/ext-py/kazoo-2.0/kazoo/__init__.py
  44. 1412 0
      desktop/core/ext-py/kazoo-2.0/kazoo/client.py
  45. 199 0
      desktop/core/ext-py/kazoo-2.0/kazoo/exceptions.py
  46. 1 0
      desktop/core/ext-py/kazoo-2.0/kazoo/handlers/__init__.py
  47. 161 0
      desktop/core/ext-py/kazoo-2.0/kazoo/handlers/gevent.py
  48. 287 0
      desktop/core/ext-py/kazoo-2.0/kazoo/handlers/threading.py
  49. 93 0
      desktop/core/ext-py/kazoo-2.0/kazoo/handlers/utils.py
  50. 26 0
      desktop/core/ext-py/kazoo-2.0/kazoo/hosts.py
  51. 203 0
      desktop/core/ext-py/kazoo-2.0/kazoo/interfaces.py
  52. 2 0
      desktop/core/ext-py/kazoo-2.0/kazoo/loggingsupport.py
  53. 1 0
      desktop/core/ext-py/kazoo-2.0/kazoo/protocol/__init__.py
  54. 623 0
      desktop/core/ext-py/kazoo-2.0/kazoo/protocol/connection.py
  55. 54 0
      desktop/core/ext-py/kazoo-2.0/kazoo/protocol/paths.py
  56. 396 0
      desktop/core/ext-py/kazoo-2.0/kazoo/protocol/serialization.py
  57. 237 0
      desktop/core/ext-py/kazoo-2.0/kazoo/protocol/states.py
  58. 1 0
      desktop/core/ext-py/kazoo-2.0/kazoo/recipe/__init__.py
  59. 214 0
      desktop/core/ext-py/kazoo-2.0/kazoo/recipe/barrier.py
  60. 94 0
      desktop/core/ext-py/kazoo-2.0/kazoo/recipe/counter.py
  61. 79 0
      desktop/core/ext-py/kazoo-2.0/kazoo/recipe/election.py
  62. 520 0
      desktop/core/ext-py/kazoo-2.0/kazoo/recipe/lock.py
  63. 377 0
      desktop/core/ext-py/kazoo-2.0/kazoo/recipe/partitioner.py
  64. 118 0
      desktop/core/ext-py/kazoo-2.0/kazoo/recipe/party.py
  65. 321 0
      desktop/core/ext-py/kazoo-2.0/kazoo/recipe/queue.py
  66. 419 0
      desktop/core/ext-py/kazoo-2.0/kazoo/recipe/watchers.py
  67. 150 0
      desktop/core/ext-py/kazoo-2.0/kazoo/retry.py
  68. 138 0
      desktop/core/ext-py/kazoo-2.0/kazoo/security.py
  69. 5 0
      desktop/core/ext-py/kazoo-2.0/kazoo/testing/__init__.py
  70. 283 0
      desktop/core/ext-py/kazoo-2.0/kazoo/testing/common.py
  71. 180 0
      desktop/core/ext-py/kazoo-2.0/kazoo/testing/harness.py
  72. 0 0
      desktop/core/ext-py/kazoo-2.0/kazoo/tests/__init__.py
  73. 157 0
      desktop/core/ext-py/kazoo-2.0/kazoo/tests/test_barrier.py
  74. 29 0
      desktop/core/ext-py/kazoo-2.0/kazoo/tests/test_build.py
  75. 1098 0
      desktop/core/ext-py/kazoo-2.0/kazoo/tests/test_client.py
  76. 319 0
      desktop/core/ext-py/kazoo-2.0/kazoo/tests/test_connection.py
  77. 35 0
      desktop/core/ext-py/kazoo-2.0/kazoo/tests/test_counter.py
  78. 139 0
      desktop/core/ext-py/kazoo-2.0/kazoo/tests/test_election.py
  79. 22 0
      desktop/core/ext-py/kazoo-2.0/kazoo/tests/test_exceptions.py
  80. 160 0
      desktop/core/ext-py/kazoo-2.0/kazoo/tests/test_gevent_handler.py
  81. 517 0
      desktop/core/ext-py/kazoo-2.0/kazoo/tests/test_lock.py
  82. 92 0
      desktop/core/ext-py/kazoo-2.0/kazoo/tests/test_partitioner.py
  83. 84 0
      desktop/core/ext-py/kazoo-2.0/kazoo/tests/test_party.py
  84. 98 0
      desktop/core/ext-py/kazoo-2.0/kazoo/tests/test_paths.py
  85. 179 0
      desktop/core/ext-py/kazoo-2.0/kazoo/tests/test_queue.py
  86. 77 0
      desktop/core/ext-py/kazoo-2.0/kazoo/tests/test_retry.py
  87. 40 0
      desktop/core/ext-py/kazoo-2.0/kazoo/tests/test_security.py
  88. 326 0
      desktop/core/ext-py/kazoo-2.0/kazoo/tests/test_threading_handler.py
  89. 489 0
      desktop/core/ext-py/kazoo-2.0/kazoo/tests/test_watchers.py
  90. 126 0
      desktop/core/ext-py/kazoo-2.0/kazoo/tests/util.py
  91. 3 0
      desktop/core/ext-py/kazoo-2.0/requirements.txt
  92. 1 0
      desktop/core/ext-py/kazoo-2.0/requirements_gevent.txt
  93. 4 0
      desktop/core/ext-py/kazoo-2.0/requirements_sphinx.txt
  94. 14 0
      desktop/core/ext-py/kazoo-2.0/setup.cfg
  95. 73 0
      desktop/core/ext-py/kazoo-2.0/setup.py

+ 548 - 0
desktop/core/ext-py/kazoo-2.0/CHANGES.rst

@@ -0,0 +1,548 @@
+Changelog
+=========
+
+2.0 (2014-06-19)
+----------------
+
+Documentation
+*************
+
+- Extend support to Python 3.4, deprecating Python 3.2.
+- Issue #198: Mention Zake as a sophisticated kazoo mock testing library.
+- Issue #181: Add documentation on basic logging setup.
+
+2.0b1 (2014-04-24)
+------------------
+
+API Changes
+***********
+
+- Null or None data is no longer treated as "". Pull req #165, patch by
+  Raul Gutierrez S. This will affect how you should treat null data in a
+  znode vs. an empty string.
+- Passing acl=[] to create() now works properly instead of an InvalidACLError
+  as it returned before. Patch by Raul Gutierrez S in PR #164.
+- Removed the dependency on zope.interface. The classes in the interfaces
+  module are left for documentation purposes only (issue #131).
+
+Features
+********
+
+- Logging levels have been reduced.
+
+  - Logging previously at the ``logging.DEBUG`` level is now logged at
+    the ``kazoo.loggingsupport.BLATHER`` level (5).
+
+  - Some low-level logging previously at the ``logging.INFO`` level is
+    now logged at the ``logging.DEBUG`` level.
+
+- Issue #133: Introduce a new environment variable `ZOOKEEPER_PORT_OFFSET`
+  for the testing support, to run the testing cluster on a different range.
+
+Bug Handling
+************
+
+- When authenticating via add_auth() the auth data will be saved to ensure that
+  the authentication happens on reconnect (as is the case when feeding auth
+  data via KazooClient's constructor). PR #172, patch by Raul Gutierrez S.
+- Change gevent import to remove deprecation warning when newer gevent is
+  used. PR #191, patch by Hiroaki Kawai.
+- Lock recipe was failing to use the client's sleep_func causing issues with
+  gevent. Issue #150.
+- Calling a DataWatch or ChildrenWatch instance twice (decorator) now throws
+  an exception as only a single function can be associated with a single
+  watcher. Issue #154.
+- Another fix for atexit handling so that when disposing of connections the
+  atexit handler is removed. PR #190, patch by Devaev Maxim.
+- Fix atexit handling for kazoo threading handler, PR #183. Patch by
+  Brian Wickman.
+- Partitioner should handle a suspended connection properly and restore
+  an allocated state if it was allocated previously. Patch by Manish Tomar.
+- Issue #167: Closing a client that was never started throws a type error.
+  Patch by Joshua Harlow.
+- Passing dictionaries to KazooClient.__init__() wasn't actually working
+  properly. Patch by Ryan Uber.
+- Issue #119: Handler timeout takes the max of the random interval or
+  the read timeout to ensure a negative number isn't used for the read
+  timeout.
+- Fix ordering of exception catches in lock.acquire as it was capturing a
+  parent exception before the child. Patch by ReneSac.
+- Fix issue with client.stop() not always setting the client state to
+  KeeperState.CLOSED. Patch by Jyrki Pulliainen in PR #174.
+- Issue #169: Fixed pipes leaking into child processes.
+
+Documentation
+*************
+
+- Add section on contributing recipes, add maintainer/status information for
+  existing recipes.
+- Add note about alternate use of DataWatch.
+
+1.3.1 (2013-09-25)
+------------------
+
+Bug Handling
+************
+
+- #118, #125, #128: Fix unknown variable in KazooClient `command_retry`
+  argument handling.
+
+- #126: Fix `KazooRetry.copy` to correctly copy sleep function.
+
+- #118: Correct session/socket timeout conversion (int vs. float).
+
+Documentation
+*************
+
+- #121: Add a note about `kazoo.recipe.queue.LockingQueue` requiring a
+  Zookeeper 3.4+ server.
+
+
+1.3 (2013-09-05)
+----------------
+
+Features
+********
+
+- #115: Limit the backends we use for SLF4J during tests.
+
+- #112: Add IPv6 support. Patch by Dan Kruchinin.
+
+1.2.1 (2013-08-01)
+------------------
+
+Bug Handling
+************
+
+- Issue #108: Circular import fail when importing kazoo.recipe.watchers
+  directly has now been resolved. Watchers and partitioner properly import
+  the KazooState from kazoo.protocol.states rather than kazoo.client.
+- Issue #109: Partials not usable properly as a datawatch call can now be
+  used. All funcs will be called with 3 args and fall back to 2 args if
+  there's an argument error.
+- Issue #106, #107: `client.create_async` didn't strip change root from the
+  returned path.
+
+1.2 (2013-07-24)
+----------------
+
+Features
+********
+
+- KazooClient can now be stopped more reliably even if its in the middle
+  of a long retry sleep. This utilizes the new interrupt feature of
+  KazooRetry which lets the sleep be broken down into chunks and an
+  interrupt function called to determine if the retry should fail early.
+
+- Issue #62, #92, #89, #101, #102: Allow KazooRetry to have a
+  max deadline, transition properly when connection fails to LOST, and
+  setup separate connection retry behavior from client command retry
+  behavior. Patches by Mike Lundy.
+
+- Issue #100: Make it easier to see exception context in threading and
+  connection modules.
+
+- Issue #85: Increase information density of logs and don't prevent
+  dynamic reconfiguration of log levels at runtime.
+
+- Data-watchers for the same node are no longer 'stacked'. That is, if
+  a get and an exists call occur for the same node with the same watch
+  function, then it will be registered only once. This change results in
+  Kazoo behaving per Zookeeper client spec regarding repeat watch use.
+
+Bug Handling
+************
+
+- Issue #53: Throw a warning upon starting if the chroot path doesn't exist
+  so that it's more obvious when the chroot should be created before
+  performing more operations.
+
+- Kazoo previously would let the same function be registered as a data-watch
+  or child-watch multiple times, and then call it multiple times upon being
+  triggered. This was non-compliant Zookeeper client behavior, the same
+  watch can now only be registered once for the same znode path per Zookeeper
+  client documentation.
+
+- Issue #105: Avoid rare import lock problems by moving module imports in
+  client.py to the module scope.
+
+- Issue #103: Allow prefix-less sequential znodes.
+
+- Issue #98: Extend testing ZK harness to work with different file locations
+  on some versions of Debian/Ubuntu.
+
+- Issue #97: Update some docstrings to reflect current state of handlers.
+
+- Issue #62, #92, #89, #101, #102: Allow KazooRetry to have a
+  max deadline, transition properly when connection fails to LOST, and
+  setup separate connection retry behavior from client command retry
+  behavior. Patches by Mike Lundy.
+
+API Changes
+***********
+
+- The `kazoo.testing.harness.KazooTestHarness` class directly inherits from
+  `unittest.TestCase` and you need to ensure to call its `__init__` method.
+
+- DataWatch no longer takes any parameters besides for the optional function
+  during instantiation. The additional options are now implicitly True, with
+  the user being left to ignore events as they choose. See the DataWatch
+  API docs for more information.
+
+- Issue #99: Better exception raised when the writer fails to close. A
+  WriterNotClosedException that inherits from KazooException is now raised
+  when the writer fails to close in time.
+
+1.1 (2013-06-08)
+----------------
+
+Features
+********
+
+- Issue #93: Add timeout option to lock/semaphore acquire methods.
+
+- Issue #79 / #90: Add ability to pass the WatchedEvent to DataWatch and
+  ChildWatch functions.
+
+- Respect large client timeout values when closing the connection.
+
+- Add a `max_leases` consistency check to the semaphore recipe.
+
+- Issue #76: Extend testing helpers to allow customization of the Java
+  classpath by specifying the new `ZOOKEEPER_CLASSPATH` environment variable.
+
+- Issue #65: Allow non-blocking semaphore acquisition.
+
+Bug Handling
+************
+
+- Issue #96: Provide Windows compatibility in testing harness.
+
+- Issue #95: Handle errors deserializing connection response.
+
+- Issue #94: Clean up stray bytes in connection pipe.
+
+- Issue #87 / #88: Allow re-acquiring lock after cancel.
+
+- Issue #77: Use timeout in initial socket connection.
+
+- Issue #69: Only ensure path once in lock and semaphore recipes.
+
+- Issue #68: Closing the connection causes exceptions to be raised by watchers
+  which assume the connection won't be closed when running commands.
+
+- Issue #66: Require ping reply before sending another ping, otherwise the
+  connection will be considered dead and a ConnectionDropped will be raised
+  to trigger a reconnect.
+
+- Issue #63: Watchers weren't reset on lost connection.
+
+- Issue #58: DataWatcher failed to re-register for changes after non-existent
+  node was created then deleted.
+
+API Changes
+***********
+
+- KazooClient.create_async now supports the makepath argument.
+
+- KazooClient.ensure_path now has an async version, ensure_path_async.
+
+1.0 (2013-03-26)
+----------------
+
+Features
+********
+
+- Added a LockingQueue recipe. The queue first locks an item and removes it
+  from the queue only after the consume() method is called. This enables other
+  nodes to retake the item if an error occurs on the first node.
+
+Bug Handling
+************
+
+- Issue #50: Avoid problems with sleep function in mixed gevent/threading
+  setup.
+
+- Issue #56: Avoid issues with watch callbacks evaluating to false.
+
+1.0b1 (2013-02-24)
+------------------
+
+Features
+********
+
+- Refactored the internal connection handler to use a single thread. It now
+  uses a deque and pipe to signal the ZK thread that there's a new command to
+  send, so that the ZK thread can send it, or retrieve a response.
+  Processing ZK requests and responses serially in a single thread eliminates
+  the need for a bunch of the locking, the peekable queue and two threads
+  working on the same underlying socket.
+
+- Issue #48: Added documentation for the `retry` helper module.
+
+- Issue #55: Fix `os.pipe` file descriptor leak and introduce a
+  `KazooClient.close` method. The method is particular useful in tests, where
+  multiple KazooClients are created and closed in the same process.
+
+Bug Handling
+************
+
+- Issue #46: Avoid TypeError in GeneratorContextManager on process shutdown.
+
+- Issue #43: Let DataWatch return node data if allow_missing_node is used.
+
+0.9 (2013-01-07)
+----------------
+
+API Changes
+***********
+
+- When a retry operation ultimately fails, it now raises a
+  `kazoo.retry.RetryFailedError` exception, instead of a general `Exception`
+  instance. `RetryFailedError` also inherits from the base `KazooException`.
+
+Features
+********
+
+- Improvements to Debian packaging rules.
+
+Bug Handling
+************
+
+- Issue #39 / #41: Handle connection dropped errors during session writes.
+  Ensure client connection is re-established to a new ZK node if available.
+
+- Issue #38: Set `CLOEXEC` flag on all sockets when available.
+
+- Issue #37 / #40: Handle timeout errors during `select` calls on sockets.
+
+- Issue #36: Correctly set `ConnectionHandler.writer_stopped` even if an
+  exception is raised inside the writer, like a retry operation failing.
+
+0.8 (2012-10-26)
+----------------
+
+API Changes
+***********
+
+- The `KazooClient.__init__` took as `watcher` argument as its second keyword
+  argument. The argument had no effect anymore since version 0.5 and was
+  removed.
+
+Bug Handling
+************
+
+- Issue #35: `KazooClient.__init__` didn't pass on `retry_max_delay` to the
+  retry helper.
+
+- Issue #34: Be more careful while handling socket connection errors.
+
+0.7 (2012-10-15)
+----------------
+
+Features
+********
+
+- DataWatch now has a `allow_missing_node` setting that allows a watch to be
+  set on a node that doesn't exist when the DataWatch is created.
+- Add new Queue recipe, with optional priority support.
+- Add new Counter recipe.
+- Added debian packaging rules.
+
+Bug Handling
+************
+
+- Issue #31 fixed: Only catch KazooExceptions in catch-all calls.
+- Issue #15 fixed again: Force sleep delay to be a float to appease gevent.
+- Issue #29 fixed: DataWatch and ChildrenWatch properly re-register their
+  watches on server disconnect.
+
+0.6 (2012-09-27)
+----------------
+
+API Changes
+***********
+
+- Node paths are assumed to be Unicode objects. Under Python 2 pure-ascii
+  strings will also be accepted. Node values are considered bytes. The byte
+  type is an alias for `str` under Python 2.
+- New KeeperState.CONNECTED_RO state for Zookeeper servers connected in
+  read-only mode.
+- New NotReadOnlyCallError exception when issuing a write change against a
+  server thats currently read-only.
+
+Features
+********
+
+- Add support for Python 3.2, 3.3 and PyPy (only for the threading handler).
+- Handles connecting to Zookeeper 3.4+ read-only servers.
+- Automatic background scanning for a Read/Write server when connected to a
+  server in read-only mode.
+- Add new Semaphore recipe.
+- Add a new `retry_max_delay` argument to the client and by default limit the
+  retry delay to at most an hour regardless of exponential backoff settings.
+- Add new `randomize_hosts` argument to `KazooClient`, allowing one to disable
+  host randomization.
+
+Bug Handling
+************
+
+- Fix bug with locks not handling intermediary lock contenders disappearing.
+- Fix bug with set_data type check failing to catch unicode values.
+- Fix bug with gevent 0.13.x backport of peekable queue.
+- Fix PatientChildrenWatch to use handler specific sleep function.
+
+0.5 (2012-09-06)
+----------------
+
+Skipping a version to reflect the magnitude of the change. Kazoo is now a pure
+Python client with no C bindings. This release should run without a problem
+on alternate Python implementations such as PyPy and Jython. Porting to Python
+3 in the future should also be much easier.
+
+Documentation
+*************
+
+- Docs have been restructured to handle the new classes and locations of the
+  methods from the pure Python refactor.
+
+Bug Handling
+************
+
+This change may introduce new bugs, however there is no longer the possibility
+of a complete Python segfault due to errors in the C library and/or the C
+binding.
+
+- Possible segfaults from the C lib are gone.
+- Password mangling due to the C lib is gone.
+- The party recipes didn't set their participating flag to False after
+  leaving.
+
+Features
+********
+
+- New `client.command` and `client.server_version` API, exposing Zookeeper's
+  four letter commands and giving access to structured version information.
+- Added 'include_data' option for get_children to include the node's Stat
+  object.
+- Substantial increase in logging data with debug mode. All correspondence with
+  the Zookeeper server can now be seen to help in debugging.
+
+API Changes
+***********
+
+- The testing helpers have been moved from `testing.__init__` into a
+  `testing.harness` module. The official API's of `KazooTestCase` and
+  `KazooTestHarness` can still be directly imported from `testing`.
+- The kazoo.handlers.util module was removed.
+- Backwards compatible exception class aliases are provided for now in kazoo
+  exceptions for the prior C exception names.
+- Unicode strings now work fine for node names and are properly converted to
+  and from unicode objects.
+- The data value argument for the create and create_async methods of the
+  client was made optional and defaults to an empty byte string. The data
+  value must be a byte string. Unicode values are no longer allowed and
+  will raise a TypeError.
+
+
+0.3 (2012-08-23)
+----------------
+
+API Changes
+***********
+
+- Handler interface now has an rlock_object for use by recipes.
+
+Bug Handling
+************
+
+- Fixed password bug with updated zc-zookeeper-static release, which retains
+  null bytes in the password properly.
+- Fixed reconnect hammering, so that the reconnection follows retry jitter and
+  retry backoff's.
+- Fixed possible bug with using a threading.Condition in the set partitioner.
+  Set partitioner uses new rlock_object handler API to get an appropriate RLock
+  for gevent.
+- Issue #17 fixed: Wrap timeout exceptions with staticmethod so they can be
+  used directly as intended. Patch by Bob Van Zant.
+- Fixed bug with client reconnection looping indefinitely using an expired
+  session id.
+
+0.2 (2012-08-12)
+----------------
+
+Documentation
+*************
+
+- Fixed doc references to start_async using an AsyncResult object, it uses
+  an Event object.
+
+Bug Handling
+************
+
+- Issue #16 fixed: gevent zookeeper logging failed to handle a monkey patched
+  logging setup. Logging is now setup such that a greenlet is used for logging
+  messages under gevent, and the thread one is used otherwise.
+- Fixed bug similar to #14 for ChildrenWatch on the session listener.
+- Issue #14 fixed: DataWatch had inconsistent handling of the node it was
+  watching not existing. DataWatch also properly spawns its _get_data function
+  to avoid blocking session events.
+- Issue #15 fixed: sleep_func for SequentialGeventHandler was not set on the
+  class appropriately leading to additional arguments being passed to
+  gevent.sleep.
+- Issue #9 fixed: Threads/greenlets didn't gracefully shut down. Handler now
+  has a start/stop that is used by the client when calling start and stop that
+  shuts down the handler workers. This addresses errors and warnings that could
+  be emitted upon process shutdown regarding a clean exit of the workers.
+- Issue #12 fixed: gevent 0.13 doesn't use the same start_new_thread as gevent
+  1.0 which resulted in a fully monkey-patched environment halting due to the
+  wrong thread. Updated to use the older kazoo method of getting the real thread
+  module object.
+
+API Changes
+***********
+
+- The KazooClient handler is now officially exposed as KazooClient.handler
+  so that the appropriate sync objects can be used by end-users.
+- Refactored ChildrenWatcher used by SetPartitioner into a publicly exposed
+  PatientChildrenWatch under recipe.watchers.
+
+Deprecations
+************
+
+- connect/connect_async has been renamed to start/start_async to better match
+  the stop to indicate connection handling. The prior names are aliased for
+  the time being.
+
+Recipes
+*******
+
+- Added Barrier and DoubleBarrier implementation.
+
+0.2b1 (2012-07-27)
+------------------
+
+Bug Handling
+************
+
+- ZOOKEEPER-1318: SystemError is caught and rethrown as the proper invalid
+  state exception in older zookeeper python bindings where this issue is still
+  valid.
+- ZOOKEEPER-1431: Install the latest zc-zookeeper-static library or use the
+  packaged ubuntu one for ubuntu 12.04 or later.
+- ZOOKEEPER-553: State handling isn't checked via this method, we track it in
+  a simpler manner with the watcher to ensure we know the right state.
+
+Features
+********
+
+- Exponential backoff with jitter for retrying commands.
+- Gevent 0.13 and 1.0b support.
+- Lock, Party, SetPartitioner, and Election recipe implementations.
+- Data and Children watching API's.
+- State transition handling with listener registering to handle session state
+  changes (choose to fatal the app on session expiration, etc.)
+- Zookeeper logging stream redirected into Python logging channel under the
+  name 'Zookeeper'.
+- Base client library with handler support for threading and gevent async
+  environments.

+ 81 - 0
desktop/core/ext-py/kazoo-2.0/CONTRIBUTING.rst

@@ -0,0 +1,81 @@
+=================
+How to contribute
+=================
+
+We gladly accept outside contributions. We use our
+`Github issue tracker <https://github.com/python-zk/kazoo/issues>`_
+for both discussions and talking about new features or bugs. You can
+also fork the project and sent us a pull request. If you have a more
+general topic to discuss, the
+`user@zookeeper.apache.org <https://zookeeper.apache.org/lists.html>`_
+mailing list is a good place to do so. You can sometimes find us on
+IRC in the
+`#zookeeper channel on freenode <https://zookeeper.apache.org/irc.html>`_.
+
+
+Development
+===========
+
+If you want to work on the code and sent us a
+`pull request <https://help.github.com/articles/using-pull-requests>`_,
+first fork the repository on github to your own account. Then clone
+your new repository and run the build scripts::
+
+    git clone git@github.com:<username>/kazoo.git
+    cd kazoo
+    make
+    make zookeeper
+
+You need to have some supported version of Python installed and have
+it available as ``python`` in your shell. To run Zookeeper you also
+need a Java runtime (JRE or JDK) version 6 or 7.
+
+You can run all the tests by calling::
+
+    make test
+
+Or to run individual tests::
+
+    export ZOOKEEPER_PATH=/<path to current folder>/bin/zookeeper/
+    bin/nosetests -s -d kazoo.tests.test_client:TestClient.test_create
+
+The nose test runner allows you to filter by test module, class or
+individual test method.
+
+If you made changes to the documentation, you can build it locally::
+
+    make html
+
+And then open ``./docs/_build/html/index.html`` in a web browser to
+verify the correct rendering.
+
+
+Submitting changes
+==================
+
+We appreciate getting changes sent as pull requests via github. We have
+travis-ci set up, which will run all tests on all supported version
+combinations for submitted pull requests, which makes it easy to see
+if new code breaks tests on some weird version combination.
+
+If you introduce new functionality, please also add documentation and
+a short entry in the top-level ``CHANGES.rst`` file.
+
+
+Adding Recipes
+==============
+
+New recipes are welcome, however they should include the status/maintainer
+RST information so its clear who is maintaining the recipe. This does mean
+that if you submit a recipe for inclusion with Kazoo, you should be ready
+to support/maintain it, and address bugs that may be found.
+
+Ideally a recipe should have at least two maintainers.
+
+Legal
+=====
+
+Currently we don't have any legal contributor agreement, so code
+ownership stays with the original authors. The project is licensed
+under the
+`Apache License Version 2 <https://github.com/python-zk/kazoo/blob/master/LICENSE>`_.

+ 176 - 0
desktop/core/ext-py/kazoo-2.0/LICENSE

@@ -0,0 +1,176 @@
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+

+ 12 - 0
desktop/core/ext-py/kazoo-2.0/MANIFEST.in

@@ -0,0 +1,12 @@
+include CHANGES.rst
+include CONTRIBUTING.rst
+include README.rst
+include LICENSE
+include MANIFEST.in
+exclude .gitignore
+exclude .travis.yml
+exclude Makefile
+exclude run_failure.py
+recursive-include kazoo *
+recursive-exclude sw *
+global-exclude *pyc *pyo

+ 604 - 0
desktop/core/ext-py/kazoo-2.0/PKG-INFO

@@ -0,0 +1,604 @@
+Metadata-Version: 1.1
+Name: kazoo
+Version: 2.0
+Summary: Higher Level Zookeeper Client
+Home-page: https://kazoo.readthedocs.org
+Author: Kazoo team
+Author-email: python-zk@googlegroups.com
+License: Apache 2.0
+Description: =====
+        Kazoo
+        =====
+        
+        ``kazoo`` implements a higher level API to `Apache Zookeeper`_ for Python
+        clients.
+        
+        See `the full docs`_ for more information.
+        
+        License
+        =======
+        
+        ``kazoo`` is offered under the Apache License 2.0.
+        
+        Authors
+        =======
+        
+        ``kazoo`` started under the `Nimbus Project`_ and through collaboration with
+        the open-source community has been merged with code from `Mozilla`_ and the
+        `Zope Corporation`_. It has since gathered an active community of over two
+        dozen contributors.
+        
+        .. _Apache Zookeeper: http://zookeeper.apache.org/
+        .. _the full docs: http://kazoo.rtfd.org/
+        .. _Nimbus Project: http://www.nimbusproject.org/
+        .. _Zope Corporation: http://zope.com/
+        .. _Mozilla: http://www.mozilla.org/
+        
+        
+        Changelog
+        =========
+        
+        2.0 (2014-06-19)
+        ----------------
+        
+        Documentation
+        *************
+        
+        - Extend support to Python 3.4, deprecating Python 3.2.
+        - Issue #198: Mention Zake as a sophisticated kazoo mock testing library.
+        - Issue #181: Add documentation on basic logging setup.
+        
+        2.0b1 (2014-04-24)
+        ------------------
+        
+        API Changes
+        ***********
+        
+        - Null or None data is no longer treated as "". Pull req #165, patch by
+          Raul Gutierrez S. This will affect how you should treat null data in a
+          znode vs. an empty string.
+        - Passing acl=[] to create() now works properly instead of an InvalidACLError
+          as it returned before. Patch by Raul Gutierrez S in PR #164.
+        - Removed the dependency on zope.interface. The classes in the interfaces
+          module are left for documentation purposes only (issue #131).
+        
+        Features
+        ********
+        
+        - Logging levels have been reduced.
+        
+          - Logging previously at the ``logging.DEBUG`` level is now logged at
+            the ``kazoo.loggingsupport.BLATHER`` level (5).
+        
+          - Some low-level logging previously at the ``logging.INFO`` level is
+            now logged at the ``logging.DEBUG`` level.
+        
+        - Issue #133: Introduce a new environment variable `ZOOKEEPER_PORT_OFFSET`
+          for the testing support, to run the testing cluster on a different range.
+        
+        Bug Handling
+        ************
+        
+        - When authenticating via add_auth() the auth data will be saved to ensure that
+          the authentication happens on reconnect (as is the case when feeding auth
+          data via KazooClient's constructor). PR #172, patch by Raul Gutierrez S.
+        - Change gevent import to remove deprecation warning when newer gevent is
+          used. PR #191, patch by Hiroaki Kawai.
+        - Lock recipe was failing to use the client's sleep_func causing issues with
+          gevent. Issue #150.
+        - Calling a DataWatch or ChildrenWatch instance twice (decorator) now throws
+          an exception as only a single function can be associated with a single
+          watcher. Issue #154.
+        - Another fix for atexit handling so that when disposing of connections the
+          atexit handler is removed. PR #190, patch by Devaev Maxim.
+        - Fix atexit handling for kazoo threading handler, PR #183. Patch by
+          Brian Wickman.
+        - Partitioner should handle a suspended connection properly and restore
+          an allocated state if it was allocated previously. Patch by Manish Tomar.
+        - Issue #167: Closing a client that was never started throws a type error.
+          Patch by Joshua Harlow.
+        - Passing dictionaries to KazooClient.__init__() wasn't actually working
+          properly. Patch by Ryan Uber.
+        - Issue #119: Handler timeout takes the max of the random interval or
+          the read timeout to ensure a negative number isn't used for the read
+          timeout.
+        - Fix ordering of exception catches in lock.acquire as it was capturing a
+          parent exception before the child. Patch by ReneSac.
+        - Fix issue with client.stop() not always setting the client state to
+          KeeperState.CLOSED. Patch by Jyrki Pulliainen in PR #174.
+        - Issue #169: Fixed pipes leaking into child processes.
+        
+        Documentation
+        *************
+        
+        - Add section on contributing recipes, add maintainer/status information for
+          existing recipes.
+        - Add note about alternate use of DataWatch.
+        
+        1.3.1 (2013-09-25)
+        ------------------
+        
+        Bug Handling
+        ************
+        
+        - #118, #125, #128: Fix unknown variable in KazooClient `command_retry`
+          argument handling.
+        
+        - #126: Fix `KazooRetry.copy` to correctly copy sleep function.
+        
+        - #118: Correct session/socket timeout conversion (int vs. float).
+        
+        Documentation
+        *************
+        
+        - #121: Add a note about `kazoo.recipe.queue.LockingQueue` requiring a
+          Zookeeper 3.4+ server.
+        
+        
+        1.3 (2013-09-05)
+        ----------------
+        
+        Features
+        ********
+        
+        - #115: Limit the backends we use for SLF4J during tests.
+        
+        - #112: Add IPv6 support. Patch by Dan Kruchinin.
+        
+        1.2.1 (2013-08-01)
+        ------------------
+        
+        Bug Handling
+        ************
+        
+        - Issue #108: Circular import fail when importing kazoo.recipe.watchers
+          directly has now been resolved. Watchers and partitioner properly import
+          the KazooState from kazoo.protocol.states rather than kazoo.client.
+        - Issue #109: Partials not usable properly as a datawatch call can now be
+          used. All funcs will be called with 3 args and fall back to 2 args if
+          there's an argument error.
+        - Issue #106, #107: `client.create_async` didn't strip change root from the
+          returned path.
+        
+        1.2 (2013-07-24)
+        ----------------
+        
+        Features
+        ********
+        
+        - KazooClient can now be stopped more reliably even if its in the middle
+          of a long retry sleep. This utilizes the new interrupt feature of
+          KazooRetry which lets the sleep be broken down into chunks and an
+          interrupt function called to determine if the retry should fail early.
+        
+        - Issue #62, #92, #89, #101, #102: Allow KazooRetry to have a
+          max deadline, transition properly when connection fails to LOST, and
+          setup separate connection retry behavior from client command retry
+          behavior. Patches by Mike Lundy.
+        
+        - Issue #100: Make it easier to see exception context in threading and
+          connection modules.
+        
+        - Issue #85: Increase information density of logs and don't prevent
+          dynamic reconfiguration of log levels at runtime.
+        
+        - Data-watchers for the same node are no longer 'stacked'. That is, if
+          a get and an exists call occur for the same node with the same watch
+          function, then it will be registered only once. This change results in
+          Kazoo behaving per Zookeeper client spec regarding repeat watch use.
+        
+        Bug Handling
+        ************
+        
+        - Issue #53: Throw a warning upon starting if the chroot path doesn't exist
+          so that it's more obvious when the chroot should be created before
+          performing more operations.
+        
+        - Kazoo previously would let the same function be registered as a data-watch
+          or child-watch multiple times, and then call it multiple times upon being
+          triggered. This was non-compliant Zookeeper client behavior, the same
+          watch can now only be registered once for the same znode path per Zookeeper
+          client documentation.
+        
+        - Issue #105: Avoid rare import lock problems by moving module imports in
+          client.py to the module scope.
+        
+        - Issue #103: Allow prefix-less sequential znodes.
+        
+        - Issue #98: Extend testing ZK harness to work with different file locations
+          on some versions of Debian/Ubuntu.
+        
+        - Issue #97: Update some docstrings to reflect current state of handlers.
+        
+        - Issue #62, #92, #89, #101, #102: Allow KazooRetry to have a
+          max deadline, transition properly when connection fails to LOST, and
+          setup separate connection retry behavior from client command retry
+          behavior. Patches by Mike Lundy.
+        
+        API Changes
+        ***********
+        
+        - The `kazoo.testing.harness.KazooTestHarness` class directly inherits from
+          `unittest.TestCase` and you need to ensure to call its `__init__` method.
+        
+        - DataWatch no longer takes any parameters besides for the optional function
+          during instantiation. The additional options are now implicitly True, with
+          the user being left to ignore events as they choose. See the DataWatch
+          API docs for more information.
+        
+        - Issue #99: Better exception raised when the writer fails to close. A
+          WriterNotClosedException that inherits from KazooException is now raised
+          when the writer fails to close in time.
+        
+        1.1 (2013-06-08)
+        ----------------
+        
+        Features
+        ********
+        
+        - Issue #93: Add timeout option to lock/semaphore acquire methods.
+        
+        - Issue #79 / #90: Add ability to pass the WatchedEvent to DataWatch and
+          ChildWatch functions.
+        
+        - Respect large client timeout values when closing the connection.
+        
+        - Add a `max_leases` consistency check to the semaphore recipe.
+        
+        - Issue #76: Extend testing helpers to allow customization of the Java
+          classpath by specifying the new `ZOOKEEPER_CLASSPATH` environment variable.
+        
+        - Issue #65: Allow non-blocking semaphore acquisition.
+        
+        Bug Handling
+        ************
+        
+        - Issue #96: Provide Windows compatibility in testing harness.
+        
+        - Issue #95: Handle errors deserializing connection response.
+        
+        - Issue #94: Clean up stray bytes in connection pipe.
+        
+        - Issue #87 / #88: Allow re-acquiring lock after cancel.
+        
+        - Issue #77: Use timeout in initial socket connection.
+        
+        - Issue #69: Only ensure path once in lock and semaphore recipes.
+        
+        - Issue #68: Closing the connection causes exceptions to be raised by watchers
+          which assume the connection won't be closed when running commands.
+        
+        - Issue #66: Require ping reply before sending another ping, otherwise the
+          connection will be considered dead and a ConnectionDropped will be raised
+          to trigger a reconnect.
+        
+        - Issue #63: Watchers weren't reset on lost connection.
+        
+        - Issue #58: DataWatcher failed to re-register for changes after non-existent
+          node was created then deleted.
+        
+        API Changes
+        ***********
+        
+        - KazooClient.create_async now supports the makepath argument.
+        
+        - KazooClient.ensure_path now has an async version, ensure_path_async.
+        
+        1.0 (2013-03-26)
+        ----------------
+        
+        Features
+        ********
+        
+        - Added a LockingQueue recipe. The queue first locks an item and removes it
+          from the queue only after the consume() method is called. This enables other
+          nodes to retake the item if an error occurs on the first node.
+        
+        Bug Handling
+        ************
+        
+        - Issue #50: Avoid problems with sleep function in mixed gevent/threading
+          setup.
+        
+        - Issue #56: Avoid issues with watch callbacks evaluating to false.
+        
+        1.0b1 (2013-02-24)
+        ------------------
+        
+        Features
+        ********
+        
+        - Refactored the internal connection handler to use a single thread. It now
+          uses a deque and pipe to signal the ZK thread that there's a new command to
+          send, so that the ZK thread can send it, or retrieve a response.
+          Processing ZK requests and responses serially in a single thread eliminates
+          the need for a bunch of the locking, the peekable queue and two threads
+          working on the same underlying socket.
+        
+        - Issue #48: Added documentation for the `retry` helper module.
+        
+        - Issue #55: Fix `os.pipe` file descriptor leak and introduce a
+          `KazooClient.close` method. The method is particular useful in tests, where
+          multiple KazooClients are created and closed in the same process.
+        
+        Bug Handling
+        ************
+        
+        - Issue #46: Avoid TypeError in GeneratorContextManager on process shutdown.
+        
+        - Issue #43: Let DataWatch return node data if allow_missing_node is used.
+        
+        0.9 (2013-01-07)
+        ----------------
+        
+        API Changes
+        ***********
+        
+        - When a retry operation ultimately fails, it now raises a
+          `kazoo.retry.RetryFailedError` exception, instead of a general `Exception`
+          instance. `RetryFailedError` also inherits from the base `KazooException`.
+        
+        Features
+        ********
+        
+        - Improvements to Debian packaging rules.
+        
+        Bug Handling
+        ************
+        
+        - Issue #39 / #41: Handle connection dropped errors during session writes.
+          Ensure client connection is re-established to a new ZK node if available.
+        
+        - Issue #38: Set `CLOEXEC` flag on all sockets when available.
+        
+        - Issue #37 / #40: Handle timeout errors during `select` calls on sockets.
+        
+        - Issue #36: Correctly set `ConnectionHandler.writer_stopped` even if an
+          exception is raised inside the writer, like a retry operation failing.
+        
+        0.8 (2012-10-26)
+        ----------------
+        
+        API Changes
+        ***********
+        
+        - The `KazooClient.__init__` took as `watcher` argument as its second keyword
+          argument. The argument had no effect anymore since version 0.5 and was
+          removed.
+        
+        Bug Handling
+        ************
+        
+        - Issue #35: `KazooClient.__init__` didn't pass on `retry_max_delay` to the
+          retry helper.
+        
+        - Issue #34: Be more careful while handling socket connection errors.
+        
+        0.7 (2012-10-15)
+        ----------------
+        
+        Features
+        ********
+        
+        - DataWatch now has a `allow_missing_node` setting that allows a watch to be
+          set on a node that doesn't exist when the DataWatch is created.
+        - Add new Queue recipe, with optional priority support.
+        - Add new Counter recipe.
+        - Added debian packaging rules.
+        
+        Bug Handling
+        ************
+        
+        - Issue #31 fixed: Only catch KazooExceptions in catch-all calls.
+        - Issue #15 fixed again: Force sleep delay to be a float to appease gevent.
+        - Issue #29 fixed: DataWatch and ChildrenWatch properly re-register their
+          watches on server disconnect.
+        
+        0.6 (2012-09-27)
+        ----------------
+        
+        API Changes
+        ***********
+        
+        - Node paths are assumed to be Unicode objects. Under Python 2 pure-ascii
+          strings will also be accepted. Node values are considered bytes. The byte
+          type is an alias for `str` under Python 2.
+        - New KeeperState.CONNECTED_RO state for Zookeeper servers connected in
+          read-only mode.
+        - New NotReadOnlyCallError exception when issuing a write change against a
+          server thats currently read-only.
+        
+        Features
+        ********
+        
+        - Add support for Python 3.2, 3.3 and PyPy (only for the threading handler).
+        - Handles connecting to Zookeeper 3.4+ read-only servers.
+        - Automatic background scanning for a Read/Write server when connected to a
+          server in read-only mode.
+        - Add new Semaphore recipe.
+        - Add a new `retry_max_delay` argument to the client and by default limit the
+          retry delay to at most an hour regardless of exponential backoff settings.
+        - Add new `randomize_hosts` argument to `KazooClient`, allowing one to disable
+          host randomization.
+        
+        Bug Handling
+        ************
+        
+        - Fix bug with locks not handling intermediary lock contenders disappearing.
+        - Fix bug with set_data type check failing to catch unicode values.
+        - Fix bug with gevent 0.13.x backport of peekable queue.
+        - Fix PatientChildrenWatch to use handler specific sleep function.
+        
+        0.5 (2012-09-06)
+        ----------------
+        
+        Skipping a version to reflect the magnitude of the change. Kazoo is now a pure
+        Python client with no C bindings. This release should run without a problem
+        on alternate Python implementations such as PyPy and Jython. Porting to Python
+        3 in the future should also be much easier.
+        
+        Documentation
+        *************
+        
+        - Docs have been restructured to handle the new classes and locations of the
+          methods from the pure Python refactor.
+        
+        Bug Handling
+        ************
+        
+        This change may introduce new bugs, however there is no longer the possibility
+        of a complete Python segfault due to errors in the C library and/or the C
+        binding.
+        
+        - Possible segfaults from the C lib are gone.
+        - Password mangling due to the C lib is gone.
+        - The party recipes didn't set their participating flag to False after
+          leaving.
+        
+        Features
+        ********
+        
+        - New `client.command` and `client.server_version` API, exposing Zookeeper's
+          four letter commands and giving access to structured version information.
+        - Added 'include_data' option for get_children to include the node's Stat
+          object.
+        - Substantial increase in logging data with debug mode. All correspondence with
+          the Zookeeper server can now be seen to help in debugging.
+        
+        API Changes
+        ***********
+        
+        - The testing helpers have been moved from `testing.__init__` into a
+          `testing.harness` module. The official API's of `KazooTestCase` and
+          `KazooTestHarness` can still be directly imported from `testing`.
+        - The kazoo.handlers.util module was removed.
+        - Backwards compatible exception class aliases are provided for now in kazoo
+          exceptions for the prior C exception names.
+        - Unicode strings now work fine for node names and are properly converted to
+          and from unicode objects.
+        - The data value argument for the create and create_async methods of the
+          client was made optional and defaults to an empty byte string. The data
+          value must be a byte string. Unicode values are no longer allowed and
+          will raise a TypeError.
+        
+        
+        0.3 (2012-08-23)
+        ----------------
+        
+        API Changes
+        ***********
+        
+        - Handler interface now has an rlock_object for use by recipes.
+        
+        Bug Handling
+        ************
+        
+        - Fixed password bug with updated zc-zookeeper-static release, which retains
+          null bytes in the password properly.
+        - Fixed reconnect hammering, so that the reconnection follows retry jitter and
+          retry backoff's.
+        - Fixed possible bug with using a threading.Condition in the set partitioner.
+          Set partitioner uses new rlock_object handler API to get an appropriate RLock
+          for gevent.
+        - Issue #17 fixed: Wrap timeout exceptions with staticmethod so they can be
+          used directly as intended. Patch by Bob Van Zant.
+        - Fixed bug with client reconnection looping indefinitely using an expired
+          session id.
+        
+        0.2 (2012-08-12)
+        ----------------
+        
+        Documentation
+        *************
+        
+        - Fixed doc references to start_async using an AsyncResult object, it uses
+          an Event object.
+        
+        Bug Handling
+        ************
+        
+        - Issue #16 fixed: gevent zookeeper logging failed to handle a monkey patched
+          logging setup. Logging is now setup such that a greenlet is used for logging
+          messages under gevent, and the thread one is used otherwise.
+        - Fixed bug similar to #14 for ChildrenWatch on the session listener.
+        - Issue #14 fixed: DataWatch had inconsistent handling of the node it was
+          watching not existing. DataWatch also properly spawns its _get_data function
+          to avoid blocking session events.
+        - Issue #15 fixed: sleep_func for SequentialGeventHandler was not set on the
+          class appropriately leading to additional arguments being passed to
+          gevent.sleep.
+        - Issue #9 fixed: Threads/greenlets didn't gracefully shut down. Handler now
+          has a start/stop that is used by the client when calling start and stop that
+          shuts down the handler workers. This addresses errors and warnings that could
+          be emitted upon process shutdown regarding a clean exit of the workers.
+        - Issue #12 fixed: gevent 0.13 doesn't use the same start_new_thread as gevent
+          1.0 which resulted in a fully monkey-patched environment halting due to the
+          wrong thread. Updated to use the older kazoo method of getting the real thread
+          module object.
+        
+        API Changes
+        ***********
+        
+        - The KazooClient handler is now officially exposed as KazooClient.handler
+          so that the appropriate sync objects can be used by end-users.
+        - Refactored ChildrenWatcher used by SetPartitioner into a publicly exposed
+          PatientChildrenWatch under recipe.watchers.
+        
+        Deprecations
+        ************
+        
+        - connect/connect_async has been renamed to start/start_async to better match
+          the stop to indicate connection handling. The prior names are aliased for
+          the time being.
+        
+        Recipes
+        *******
+        
+        - Added Barrier and DoubleBarrier implementation.
+        
+        0.2b1 (2012-07-27)
+        ------------------
+        
+        Bug Handling
+        ************
+        
+        - ZOOKEEPER-1318: SystemError is caught and rethrown as the proper invalid
+          state exception in older zookeeper python bindings where this issue is still
+          valid.
+        - ZOOKEEPER-1431: Install the latest zc-zookeeper-static library or use the
+          packaged ubuntu one for ubuntu 12.04 or later.
+        - ZOOKEEPER-553: State handling isn't checked via this method, we track it in
+          a simpler manner with the watcher to ensure we know the right state.
+        
+        Features
+        ********
+        
+        - Exponential backoff with jitter for retrying commands.
+        - Gevent 0.13 and 1.0b support.
+        - Lock, Party, SetPartitioner, and Election recipe implementations.
+        - Data and Children watching API's.
+        - State transition handling with listener registering to handle session state
+          changes (choose to fatal the app on session expiration, etc.)
+        - Zookeeper logging stream redirected into Python logging channel under the
+          name 'Zookeeper'.
+        - Base client library with handler support for threading and gevent async
+          environments.
+        
+Keywords: zookeeper lock leader configuration
+Platform: UNKNOWN
+Classifier: Development Status :: 5 - Production/Stable
+Classifier: License :: OSI Approved :: Apache Software License
+Classifier: Intended Audience :: Developers
+Classifier: Operating System :: OS Independent
+Classifier: Programming Language :: Python
+Classifier: Programming Language :: Python :: 2
+Classifier: Programming Language :: Python :: 2.6
+Classifier: Programming Language :: Python :: 2.7
+Classifier: Programming Language :: Python :: 3
+Classifier: Programming Language :: Python :: 3.3
+Classifier: Programming Language :: Python :: 3.4
+Classifier: Programming Language :: Python :: Implementation :: CPython
+Classifier: Programming Language :: Python :: Implementation :: PyPy
+Classifier: Topic :: Communications
+Classifier: Topic :: System :: Distributed Computing
+Classifier: Topic :: System :: Networking

+ 27 - 0
desktop/core/ext-py/kazoo-2.0/README.rst

@@ -0,0 +1,27 @@
+=====
+Kazoo
+=====
+
+``kazoo`` implements a higher level API to `Apache Zookeeper`_ for Python
+clients.
+
+See `the full docs`_ for more information.
+
+License
+=======
+
+``kazoo`` is offered under the Apache License 2.0.
+
+Authors
+=======
+
+``kazoo`` started under the `Nimbus Project`_ and through collaboration with
+the open-source community has been merged with code from `Mozilla`_ and the
+`Zope Corporation`_. It has since gathered an active community of over two
+dozen contributors.
+
+.. _Apache Zookeeper: http://zookeeper.apache.org/
+.. _the full docs: http://kazoo.rtfd.org/
+.. _Nimbus Project: http://www.nimbusproject.org/
+.. _Zope Corporation: http://zope.com/
+.. _Mozilla: http://www.mozilla.org/

+ 5 - 0
desktop/core/ext-py/kazoo-2.0/debian/changelog

@@ -0,0 +1,5 @@
+kazoo (0+git20130102) unstable; urgency=low
+
+  * Initial package.
+
+ -- Neil Williams <neil@spladug.net>  Fri, 02 Jan 2013 23:20:03 -0800

+ 1 - 0
desktop/core/ext-py/kazoo-2.0/debian/compat

@@ -0,0 +1 @@
+8

+ 50 - 0
desktop/core/ext-py/kazoo-2.0/debian/control

@@ -0,0 +1,50 @@
+Source: kazoo
+Section: python
+Priority: optional
+Maintainer: Neil Williams <neil@spladug.net>
+Build-Depends: python-setuptools (>= 0.6b3),
+               python-all (>= 2.6.6-3),
+               debhelper (>= 8.0.0),
+               python-sphinx (>= 1.0.7+dfsg) | python3-sphinx,
+Standards-Version: 3.9.3
+Homepage: https://kazoo.readthedocs.org
+X-Python-Version: >= 2.6
+
+Package: python-kazoo
+Architecture: all
+Depends: ${python:Depends}, ${misc:Depends}
+Description: higher level API to Apache Zookeeper for Python clients
+ Kazoo features:
+ .
+  * Support for gevent 0.13 and gevent 1.0
+  * Unified asynchronous API for use with greenlets or threads
+  * Lock, Party, Election, and Partitioner recipe implementations (more
+    implementations are in development)
+  * Data and Children Watchers
+  * Integrated testing helpers for Zookeeper clusters
+  * Simplified Zookeeper connection state tracking
+  * Pure-Python based implementation of the wire protocol, avoiding all the
+    memory leaks, lacking features, and debugging madness of the C library
+ .
+ Kazoo is heavily inspired by Netflix Curator simplifications and helpers.
+
+Package: python-kazoo-doc
+Architecture: all
+Section: doc
+Depends: ${misc:Depends}, ${sphinxdoc:Depends}
+Description: API to Apache Zookeeper for Python clients. - API documentation
+ Kazoo features:
+ .
+  * Support for gevent 0.13 and gevent 1.0
+  * Unified asynchronous API for use with greenlets or threads
+  * Lock, Party, Election, and Partitioner recipe implementations (more
+    implementations are in development)
+  * Data and Children Watchers
+  * Integrated testing helpers for Zookeeper clusters
+  * Simplified Zookeeper connection state tracking
+  * Pure-Python based implementation of the wire protocol, avoiding all the
+    memory leaks, lacking features, and debugging madness of the C library
+ .
+ Kazoo is heavily inspired by Netflix Curator simplifications and helpers.
+ .
+ This package contains the API documentation.

+ 13 - 0
desktop/core/ext-py/kazoo-2.0/debian/copyright

@@ -0,0 +1,13 @@
+Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
+Upstream-Name: kazoo
+Source: https://github.com/python-zk/kazoo
+
+Files: *
+Copyright: 2012 Kazoo Team
+License: Apache-2.0
+ See /usr/share/common-licenses/Apache-2.0
+
+Files: debian/*
+Copyright: 2012 Neil Williams <neil@spladug.net>
+License: Apache-2.0
+ See /usr/share/common-licenses/Apache-2.0

+ 1 - 0
desktop/core/ext-py/kazoo-2.0/debian/docs

@@ -0,0 +1 @@
+README.rst

+ 1 - 0
desktop/core/ext-py/kazoo-2.0/debian/python-kazoo.install

@@ -0,0 +1 @@
+usr/lib/python2*

+ 18 - 0
desktop/core/ext-py/kazoo-2.0/debian/rules

@@ -0,0 +1,18 @@
+#!/usr/bin/make -f
+
+%:
+	dh $@ --with python2,sphinxdoc --buildsystem=python_distutils
+
+.PHONY: override_dh_installchangelogs
+override_dh_installchangelogs:
+	dh_installchangelogs CHANGES.rst
+
+.PHONY: override_dh_auto_build
+override_dh_auto_build:
+	sphinx-build -b html docs build/html
+	dh_auto_build
+
+.PHONY: override_dh_clean
+override_dh_clean:
+	rm -rf build
+	dh_clean

+ 1 - 0
desktop/core/ext-py/kazoo-2.0/debian/source/format

@@ -0,0 +1 @@
+3.0 (quilt)

+ 153 - 0
desktop/core/ext-py/kazoo-2.0/docs/Makefile

@@ -0,0 +1,153 @@
+# Makefile for Sphinx documentation
+#
+
+# You can set these variables from the command line.
+SPHINXOPTS    =
+SPHINXBUILD   = ../bin/sphinx-build
+PAPER         =
+BUILDDIR      = _build
+
+# Internal variables.
+PAPEROPT_a4     = -D latex_paper_size=a4
+PAPEROPT_letter = -D latex_paper_size=letter
+ALLSPHINXOPTS   = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
+# the i18n builder cannot share the environment and doctrees with the others
+I18NSPHINXOPTS  = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
+
+.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext
+
+help:
+	@echo "Please use \`make <target>' where <target> is one of"
+	@echo "  html       to make standalone HTML files"
+	@echo "  dirhtml    to make HTML files named index.html in directories"
+	@echo "  singlehtml to make a single large HTML file"
+	@echo "  pickle     to make pickle files"
+	@echo "  json       to make JSON files"
+	@echo "  htmlhelp   to make HTML files and a HTML help project"
+	@echo "  qthelp     to make HTML files and a qthelp project"
+	@echo "  devhelp    to make HTML files and a Devhelp project"
+	@echo "  epub       to make an epub"
+	@echo "  latex      to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
+	@echo "  latexpdf   to make LaTeX files and run them through pdflatex"
+	@echo "  text       to make text files"
+	@echo "  man        to make manual pages"
+	@echo "  texinfo    to make Texinfo files"
+	@echo "  info       to make Texinfo files and run them through makeinfo"
+	@echo "  gettext    to make PO message catalogs"
+	@echo "  changes    to make an overview of all changed/added/deprecated items"
+	@echo "  linkcheck  to check all external links for integrity"
+	@echo "  doctest    to run all doctests embedded in the documentation (if enabled)"
+
+clean:
+	-rm -rf $(BUILDDIR)/*
+
+html:
+	$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
+	@echo
+	@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
+
+dirhtml:
+	$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
+	@echo
+	@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
+
+singlehtml:
+	$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
+	@echo
+	@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
+
+pickle:
+	$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
+	@echo
+	@echo "Build finished; now you can process the pickle files."
+
+json:
+	$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
+	@echo
+	@echo "Build finished; now you can process the JSON files."
+
+htmlhelp:
+	$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
+	@echo
+	@echo "Build finished; now you can run HTML Help Workshop with the" \
+	      ".hhp project file in $(BUILDDIR)/htmlhelp."
+
+qthelp:
+	$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
+	@echo
+	@echo "Build finished; now you can run "qcollectiongenerator" with the" \
+	      ".qhcp project file in $(BUILDDIR)/qthelp, like this:"
+	@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/kazoo.qhcp"
+	@echo "To view the help file:"
+	@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/kazoo.qhc"
+
+devhelp:
+	$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
+	@echo
+	@echo "Build finished."
+	@echo "To view the help file:"
+	@echo "# mkdir -p $$HOME/.local/share/devhelp/kazoo"
+	@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/kazoo"
+	@echo "# devhelp"
+
+epub:
+	$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
+	@echo
+	@echo "Build finished. The epub file is in $(BUILDDIR)/epub."
+
+latex:
+	$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
+	@echo
+	@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
+	@echo "Run \`make' in that directory to run these through (pdf)latex" \
+	      "(use \`make latexpdf' here to do that automatically)."
+
+latexpdf:
+	$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
+	@echo "Running LaTeX files through pdflatex..."
+	$(MAKE) -C $(BUILDDIR)/latex all-pdf
+	@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
+
+text:
+	$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
+	@echo
+	@echo "Build finished. The text files are in $(BUILDDIR)/text."
+
+man:
+	$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
+	@echo
+	@echo "Build finished. The manual pages are in $(BUILDDIR)/man."
+
+texinfo:
+	$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
+	@echo
+	@echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
+	@echo "Run \`make' in that directory to run these through makeinfo" \
+	      "(use \`make info' here to do that automatically)."
+
+info:
+	$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
+	@echo "Running Texinfo files through makeinfo..."
+	make -C $(BUILDDIR)/texinfo info
+	@echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
+
+gettext:
+	$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
+	@echo
+	@echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
+
+changes:
+	$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
+	@echo
+	@echo "The overview file is in $(BUILDDIR)/changes."
+
+linkcheck:
+	$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
+	@echo
+	@echo "Link check complete; look for any errors in the above output " \
+	      "or in $(BUILDDIR)/linkcheck/output.txt."
+
+doctest:
+	$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
+	@echo "Testing of doctests in the sources finished, look at the " \
+	      "results in $(BUILDDIR)/doctest/output.txt."

+ 28 - 0
desktop/core/ext-py/kazoo-2.0/docs/api.rst

@@ -0,0 +1,28 @@
+API Documentation
+=================
+
+Comprehensive reference material for every public API exposed by
+`kazoo` is available within this chapter. The API documentation is
+organized alphabetically by module name.
+
+.. toctree::
+   :maxdepth: 1
+
+   api/client
+   api/exceptions
+   api/handlers/gevent
+   api/handlers/threading
+   api/handlers/utils
+   api/interfaces
+   api/protocol/states
+   api/recipe/barrier
+   api/recipe/counter
+   api/recipe/election
+   api/recipe/lock
+   api/recipe/partitioner
+   api/recipe/party
+   api/recipe/queue
+   api/recipe/watchers
+   api/retry
+   api/security
+   api/testing

+ 35 - 0
desktop/core/ext-py/kazoo-2.0/docs/api/client.rst

@@ -0,0 +1,35 @@
+.. _client_module:
+
+:mod:`kazoo.client`
+----------------------------
+
+.. automodule:: kazoo.client
+
+Public API
+++++++++++
+
+    .. autoclass:: KazooClient()
+        :members:
+        :member-order: bysource
+
+        .. automethod:: __init__
+
+        .. attribute:: handler
+
+            The :class:`~kazoo.interfaces.IHandler` strategy used by this
+            client. Gives access to appropriate synchronization objects.
+
+        .. method:: retry(func, *args, **kwargs)
+
+            Runs the given function with the provided arguments, retrying if it
+            fails because the ZooKeeper connection is lost,
+            see :ref:`retrying_commands`.
+
+        .. attribute:: state
+
+            A :class:`~kazoo.protocol.states.KazooState` attribute indicating
+            the current higher-level connection state.
+
+    .. autoclass:: TransactionRequest
+        :members:
+        :member-order: bysource

+ 74 - 0
desktop/core/ext-py/kazoo-2.0/docs/api/exceptions.rst

@@ -0,0 +1,74 @@
+.. _exceptions_module:
+
+:mod:`kazoo.exceptions`
+-----------------------
+
+.. automodule:: kazoo.exceptions
+
+Public API
+++++++++++
+
+.. autoexception:: KazooException
+
+.. autoexception:: ZookeeperError
+
+.. autoexception:: AuthFailedError
+
+.. autoexception:: BadVersionError
+
+.. autoexception:: ConfigurationError
+
+.. autoexception:: InvalidACLError
+
+.. autoexception:: LockTimeout
+
+.. autoexception:: NoChildrenForEphemeralsError
+
+.. autoexception:: NodeExistsError
+
+.. autoexception:: NoNodeError
+
+.. autoexception:: NotEmptyError
+
+Private API
++++++++++++
+
+.. autoexception:: APIError
+
+.. autoexception:: BadArgumentsError
+
+.. autoexception:: CancelledError
+
+.. autoexception:: ConnectionDropped
+
+.. autoexception:: ConnectionClosedError
+
+.. autoexception:: ConnectionLoss
+
+.. autoexception:: DataInconsistency
+
+.. autoexception:: MarshallingError
+
+.. autoexception:: NoAuthError
+
+.. autoexception:: NotReadOnlyCallError
+
+.. autoexception:: InvalidCallbackError
+
+.. autoexception:: OperationTimeoutError
+
+.. autoexception:: RolledBackError
+
+.. autoexception:: RuntimeInconsistency
+
+.. autoexception:: SessionExpiredError
+
+.. autoexception:: SessionMovedError
+
+.. autoexception:: SystemZookeeperError
+
+.. autoexception:: UnimplementedError
+
+.. autoexception:: WriterNotClosedException
+
+.. autoexception:: ZookeeperStoppedError

+ 18 - 0
desktop/core/ext-py/kazoo-2.0/docs/api/handlers/gevent.rst

@@ -0,0 +1,18 @@
+.. _gevent_handler_module:
+
+:mod:`kazoo.handlers.gevent`
+----------------------------
+
+.. automodule:: kazoo.handlers.gevent
+
+Public API
+++++++++++
+
+    .. autoclass:: SequentialGeventHandler
+        :members:
+
+Private API
++++++++++++
+
+  .. autoclass:: AsyncResult
+     :members:

+ 20 - 0
desktop/core/ext-py/kazoo-2.0/docs/api/handlers/threading.rst

@@ -0,0 +1,20 @@
+.. _thread_handler_module:
+
+:mod:`kazoo.handlers.threading`
+-------------------------------
+
+.. automodule:: kazoo.handlers.threading
+
+Public API
+++++++++++
+
+    .. autoclass:: SequentialThreadingHandler
+        :members:
+
+Private API
++++++++++++
+
+  .. autoclass:: AsyncResult
+     :members:
+
+  .. autoexception:: TimeoutError

+ 33 - 0
desktop/core/ext-py/kazoo-2.0/docs/api/interfaces.rst

@@ -0,0 +1,33 @@
+.. _interfaces_module:
+
+:mod:`kazoo.interfaces`
+----------------------------
+
+.. automodule:: kazoo.interfaces
+
+Public API
+++++++++++
+
+:class:`IHandler` implementations should be created by the developer to be
+passed into :class:`~kazoo.client.KazooClient` during instantiation for the
+preferred callback handling.
+
+If the developer needs to use objects implementing the :class:`IAsyncResult`
+interface, the :meth:`IHandler.async_result` method must be used instead of
+instantiating one directly.
+
+    .. autoclass:: IHandler
+     :members:
+
+Private API
++++++++++++
+
+The :class:`IAsyncResult` documents the proper implementation for providing
+a value that results from a Zookeeper completion callback. Since the
+:class:`~kazoo.client.KazooClient` returns an :class:`IAsyncResult` object
+instead of taking a completion callback for async functions, developers
+wishing to have their own callback called should use the
+:meth:`IAsyncResult.rawlink` method.
+
+  .. autoclass:: IAsyncResult
+     :members:

+ 24 - 0
desktop/core/ext-py/kazoo-2.0/docs/api/protocol/states.rst

@@ -0,0 +1,24 @@
+.. _states_module:
+
+:mod:`kazoo.protocol.states`
+----------------------------
+
+.. automodule:: kazoo.protocol.states
+
+Public API
+++++++++++
+
+    .. autoclass:: EventType
+
+    .. autoclass:: KazooState
+
+    .. autoclass:: KeeperState
+
+    .. autoclass:: WatchedEvent
+
+    .. autoclass:: ZnodeStat
+
+Private API
++++++++++++
+
+    .. autoclass:: Callback

+ 19 - 0
desktop/core/ext-py/kazoo-2.0/docs/api/recipe/barrier.rst

@@ -0,0 +1,19 @@
+.. _barrier_module:
+
+:mod:`kazoo.recipe.barrier`
+----------------------------
+
+.. automodule:: kazoo.recipe.barrier
+
+Public API
+++++++++++
+
+    .. autoclass:: Barrier
+        :members:
+
+        .. automethod:: __init__
+
+    .. autoclass:: DoubleBarrier
+        :members:
+
+        .. automethod:: __init__

+ 19 - 0
desktop/core/ext-py/kazoo-2.0/docs/api/recipe/counter.rst

@@ -0,0 +1,19 @@
+.. _counter_module:
+
+:mod:`kazoo.recipe.counter`
+---------------------------
+
+.. automodule:: kazoo.recipe.counter
+
+.. versionadded:: 0.7
+    The Counter class.
+
+Public API
+++++++++++
+
+    .. autoclass:: Counter
+        :members:
+
+        .. automethod:: __init__
+        .. automethod:: __add__
+        .. automethod:: __sub__

+ 14 - 0
desktop/core/ext-py/kazoo-2.0/docs/api/recipe/election.rst

@@ -0,0 +1,14 @@
+.. _election_module:
+
+:mod:`kazoo.recipe.election`
+----------------------------
+
+.. automodule:: kazoo.recipe.election
+
+Public API
+++++++++++
+
+    .. autoclass:: Election
+        :members:
+
+        .. automethod:: __init__

+ 19 - 0
desktop/core/ext-py/kazoo-2.0/docs/api/recipe/lock.rst

@@ -0,0 +1,19 @@
+.. _lock_module:
+
+:mod:`kazoo.recipe.lock`
+----------------------------
+
+.. automodule:: kazoo.recipe.lock
+
+Public API
+++++++++++
+
+    .. autoclass:: Lock
+        :members:
+
+        .. automethod:: __init__
+
+    .. autoclass:: Semaphore
+        :members:
+
+        .. automethod:: __init__

+ 16 - 0
desktop/core/ext-py/kazoo-2.0/docs/api/recipe/partitioner.rst

@@ -0,0 +1,16 @@
+.. _partitioner_module:
+
+:mod:`kazoo.recipe.partitioner`
+-------------------------------
+
+.. automodule:: kazoo.recipe.partitioner
+
+Public API
+++++++++++
+
+    .. autoclass:: SetPartitioner
+        :members:
+
+        .. automethod:: __init__
+
+    .. autoclass:: PartitionState

+ 25 - 0
desktop/core/ext-py/kazoo-2.0/docs/api/recipe/party.rst

@@ -0,0 +1,25 @@
+.. _party_module:
+
+:mod:`kazoo.recipe.party`
+-------------------------
+
+.. automodule:: kazoo.recipe.party
+
+Public API
+++++++++++
+
+    .. autoclass:: Party
+        :members:
+        :inherited-members:
+
+        .. automethod:: __init__
+        .. automethod:: __iter__
+        .. automethod:: __len__
+
+    .. autoclass:: ShallowParty
+        :members:
+        :inherited-members:
+
+        .. automethod:: __init__
+        .. automethod:: __iter__
+        .. automethod:: __len__

+ 29 - 0
desktop/core/ext-py/kazoo-2.0/docs/api/recipe/queue.rst

@@ -0,0 +1,29 @@
+.. _queue_module:
+
+:mod:`kazoo.recipe.queue`
+-------------------------
+
+.. automodule:: kazoo.recipe.queue
+
+.. versionadded:: 0.6
+    The Queue class.
+
+.. versionadded:: 1.0
+    The LockingQueue class.
+
+Public API
+++++++++++
+
+    .. autoclass:: Queue
+        :members:
+        :inherited-members:
+
+        .. automethod:: __init__
+        .. automethod:: __len__
+
+    .. autoclass:: LockingQueue
+        :members:
+        :inherited-members:
+
+        .. automethod:: __init__
+        .. automethod:: __len__

+ 29 - 0
desktop/core/ext-py/kazoo-2.0/docs/api/recipe/watchers.rst

@@ -0,0 +1,29 @@
+.. _watchers_module:
+
+:mod:`kazoo.recipe.watchers`
+----------------------------
+
+.. automodule:: kazoo.recipe.watchers
+
+Public API
+++++++++++
+
+    .. autoclass:: DataWatch
+        :members:
+
+        .. automethod:: __init__
+
+        .. automethod:: __call__
+
+
+    .. autoclass:: ChildrenWatch
+        :members:
+
+        .. automethod:: __init__
+
+        .. automethod:: __call__
+
+    .. autoclass:: PatientChildrenWatch
+        :members:
+
+        .. automethod:: __init__

+ 24 - 0
desktop/core/ext-py/kazoo-2.0/docs/api/security.rst

@@ -0,0 +1,24 @@
+.. _security_module:
+
+:mod:`kazoo.security`
+----------------------------
+
+.. automodule:: kazoo.security
+
+Public API
+++++++++++
+
+    .. autoclass:: ACL
+
+    .. autoclass:: Id
+
+    .. autofunction:: make_digest_acl
+
+Private API
++++++++++++
+
+    .. autofunction:: make_acl
+
+    .. autofunction:: make_digest_acl_credential
+
+    .. autoclass: ACLPermission

+ 12 - 0
desktop/core/ext-py/kazoo-2.0/docs/api/testing.rst

@@ -0,0 +1,12 @@
+.. _testing_harness_module:
+
+:mod:`kazoo.testing.harness`
+----------------------------
+
+.. automodule:: kazoo.testing.harness
+
+Public API
+++++++++++
+
+    .. autoclass:: KazooTestHarness
+    .. autoclass:: KazooTestCase

+ 114 - 0
desktop/core/ext-py/kazoo-2.0/docs/async_usage.rst

@@ -0,0 +1,114 @@
+.. _async_usage:
+
+==================
+Asynchronous Usage
+==================
+
+The asynchronous Kazoo API relies on the
+:class:`~kazoo.interfaces.IAsyncResult` object which is returned by all the
+asynchronous methods. Callbacks can be added with the
+:meth:`~kazoo.interfaces.IAsyncResult.rawlink` method which works in a
+consistent manner whether threads or an asynchronous framework like gevent is
+used.
+
+Kazoo utilizes a pluggable :class:`~kazoo.interfaces.IHandler` interface which
+abstracts the callback system to ensure it works consistently.
+
+Connection Handling
+===================
+
+Creating a connection:
+
+.. code-block:: python
+
+    from kazoo.client import KazooClient
+    from kazoo.handlers.gevent import SequentialGeventHandler
+
+    zk = KazooClient(handler=SequentialGeventHandler())
+
+    # returns immediately
+    event = zk.start_async()
+
+    # Wait for 30 seconds and see if we're connected
+    event.wait(timeout=30)
+
+    if not zk.connected:
+        # Not connected, stop trying to connect
+        zk.stop()
+        raise Exception("Unable to connect.")
+
+In this example, the `wait` method is used on the event object returned by the
+:meth:`~kazoo.client.KazooClient.start_async` method. A timeout is **always**
+used because its possible that we might never connect and that should be
+handled gracefully.
+
+The :class:`~kazoo.handlers.gevent.SequentialGeventHandler` is used when you
+want to use gevent. Kazoo doesn't rely on gevents monkey patching and requires
+that you pass in the appropriate handler, the default handler is
+:class:`~kazoo.handlers.threading.SequentialThreadingHandler`.
+
+
+Asynchronous Callbacks
+======================
+
+All kazoo `_async` methods except for
+:meth:`~kazoo.client.KazooClient.start_async` return an
+:class:`~kazoo.interfaces.IAsyncResult` instance. These instances allow
+you to see when a result is ready, or chain one or more callback
+functions to the result that will be called when it's ready.
+
+The callback function will be passed the
+:class:`~kazoo.interfaces.IAsyncResult` instance and should call the
+:meth:`~kazoo.interfaces.IAsyncResult.get` method on it to retrieve
+the value. This call could result in an exception being raised
+if the asynchronous function encountered an error. It should be caught
+and handled appropriately.
+
+Example:
+
+.. code-block:: python
+
+    import sys
+
+    from kazoo.exceptions import ConnectionLossException
+    from kazoo.exceptions import NoAuthException
+
+    def my_callback(async_obj):
+        try:
+            children = async_obj.get()
+            do_something(children)
+        except (ConnectionLossException, NoAuthException):
+            sys.exit(1)
+
+    # Both these statements return immediately, the second sets a callback
+    # that will be run when get_children_async has its return value
+    async_obj = zk.get_children_async("/some/node")
+    async_obj.rawlink(my_callback)
+
+Zookeeper CRUD
+==============
+
+The following CRUD methods all work the same as their synchronous counterparts
+except that they return an :class:`~kazoo.interfaces.IAsyncResult` object.
+
+Creating Method:
+
+* :meth:`~kazoo.client.KazooClient.create_async`
+
+Reading Methods:
+
+* :meth:`~kazoo.client.KazooClient.exists_async`
+* :meth:`~kazoo.client.KazooClient.get_async`
+* :meth:`~kazoo.client.KazooClient.get_children_async`
+
+Updating Methods:
+
+* :meth:`~kazoo.client.KazooClient.set_async`
+
+Deleting Methods:
+
+* :meth:`~kazoo.client.KazooClient.delete_async`
+
+The :meth:`~kazoo.client.KazooClient.ensure_path` has no asynchronous
+counterpart at the moment nor can the
+:meth:`~kazoo.client.KazooClient.delete_async` method do recursive deletes.

+ 465 - 0
desktop/core/ext-py/kazoo-2.0/docs/basic_usage.rst

@@ -0,0 +1,465 @@
+.. _basic_usage:
+
+===========
+Basic Usage
+===========
+
+
+Connection Handling
+===================
+
+To begin using Kazoo, a :class:`~kazoo.client.KazooClient` object must be
+created and a connection established:
+
+.. code-block:: python
+
+    from kazoo.client import KazooClient
+
+    zk = KazooClient(hosts='127.0.0.1:2181')
+    zk.start()
+
+By default, the client will connect to a local Zookeeper server on the default
+port (2181). You should make sure Zookeeper is actually running there first,
+or the ``start`` command will be waiting until its default timeout.
+
+Once connected, the client will attempt to stay connected regardless of
+intermittent connection loss or Zookeeper session expiration. The client can be
+instructed to drop a connection by calling `stop`:
+
+.. code-block:: python
+
+    zk.stop()
+
+
+Logging Setup
+-------------
+
+If logging is not setup for your application, you can get following message:
+
+.. code-block:: python
+    
+    No handlers could be found for logger "kazoo.client"
+
+To avoid this issue you can at the very minimum do the following:
+
+.. code-block:: python
+    
+    import logging
+    logging.basicConfig()
+
+Read `Python's logging tutorial <https://docs.python.org/howto/logging.html>`_
+for more details.
+
+
+Listening for Connection Events
+-------------------------------
+
+It can be useful to know when the connection has been dropped, restored, or
+when the Zookeeper session has expired. To simplify this process Kazoo uses a
+state system and lets you register listener functions to be called when the
+state changes.
+
+.. code-block:: python
+
+    from kazoo.client import KazooState
+
+    def my_listener(state):
+        if state == KazooState.LOST:
+            # Register somewhere that the session was lost
+        elif state == KazooState.SUSPENDED:
+            # Handle being disconnected from Zookeeper
+        else:
+            # Handle being connected/reconnected to Zookeeper
+
+    zk.add_listener(my_listener)
+
+When using the :class:`kazoo.recipe.lock.Lock` or creating ephemeral nodes, its
+highly recommended to add a state listener so that your program can properly
+deal with connection interruptions or a Zookeeper session loss.
+
+Understanding Kazoo States
+--------------------------
+
+The :class:`~kazoo.protocol.states.KazooState` object represents several states
+the client transitions through. The current state of the client can always be
+determined by viewing the :attr:`~kazoo.client.KazooClient.state` property. The
+possible states are:
+
+- LOST
+- CONNECTED
+- SUSPENDED
+
+When a :class:`~kazoo.client.KazooClient` instance is first created, it is in
+the `LOST` state. After a connection is established it transitions to the
+`CONNECTED` state. If any connection issues come up or if it needs to connect
+to a different Zookeeper cluster node, it will transition to `SUSPENDED` to let
+you know that commands cannot currently be run. The connection will also be
+lost if the Zookeeper node is no longer part of the quorum, resulting in a
+`SUSPENDED` state.
+
+Upon re-establishing a connection the client could transition to `LOST` if the
+session has expired, or `CONNECTED` if the session is still valid.
+
+.. note::
+
+    These states should be monitored using a listener as described previously
+    so that the client behaves properly depending on the state of the
+    connection.
+
+When a connection transitions to `SUSPENDED`, if the client is performing an
+action that requires agreement with other systems (using the Lock recipe for
+example), it should pause what it's doing. When the connection has been
+re-established the client can continue depending on if the state is `LOST` or
+transitions directly to `CONNECTED` again.
+
+When a connection transitions to `LOST`, any ephemeral nodes that have been
+created will be removed by Zookeeper. This affects all recipes that create
+ephemeral nodes, such as the Lock recipe. Lock's will need to be re-acquired
+after the state transitions to `CONNECTED` again. This transition occurs when
+a session expires or when you stop the clients connection.
+
+**Valid State Transitions**
+
+- *LOST -> CONNECTED*
+
+  New connection, or previously lost one becoming connected.
+- *CONNECTED -> SUSPENDED*
+
+  Connection loss to server occurred on a connection.
+- *CONNECTED -> LOST*
+
+  Only occurs if invalid authentication credentials are provided after the
+  connection was established.
+- *SUSPENDED -> LOST*
+
+  Connection resumed to server, but then lost as the session was expired.
+- *SUSPENDED -> CONNECTED*
+
+  Connection that was lost has been restored.
+
+Read-Only Connections
+---------------------
+
+.. versionadded:: 0.6
+
+Zookeeper 3.4 and above `supports a read-only mode
+<http://wiki.apache.org/hadoop/ZooKeeper/GSoCReadOnlyMode>`_. This mode
+must be turned on for the servers in the Zookeeper cluster for the
+client to utilize it. To use this mode with Kazoo, the
+:class:`~kazoo.client.KazooClient` should be called with the
+`read_only` option set to `True`. This will let the client connect to
+a Zookeeper node that has gone read-only, and the client will continue
+to scan for other nodes that are read-write.
+
+.. code-block:: python
+
+    from kazoo.client import KazooClient
+
+    zk = KazooClient(hosts='127.0.0.1:2181', read_only=True)
+    zk.start()
+
+A new attribute on :class:`~kazoo.protocol.states.KeeperState` has been
+added, `CONNECTED_RO`. The connection states above are still valid,
+however upon `CONNECTED`, you will need to check the clients non-
+simplified state to see if the connection is `CONNECTED_RO`. For
+example:
+
+.. code-block:: python
+
+    from kazoo.client import KazooState
+    from kazoo.client import KeeperState
+
+    @zk.add_listener
+    def watch_for_ro(state):
+        if state == KazooState.CONNECTED:
+            if zk.client_state == KeeperState.CONNECTED_RO:
+                print("Read only mode!")
+            else:
+                print("Read/Write mode!")
+
+It's important to note that a `KazooState` is passed in to the listener
+but the read-only information is only available by comparing the
+non-simplified client state to the `KeeperState` object.
+
+.. warning::
+
+    A client using read-only mode should not use any of the recipes.
+
+
+Zookeeper CRUD
+==============
+
+Zookeeper includes several functions for creating, reading, updating, and
+deleting Zookeeper nodes (called znodes or nodes here). Kazoo adds several
+convenience methods and a more Pythonic API.
+
+Creating Nodes
+--------------
+
+Methods:
+
+* :meth:`~kazoo.client.KazooClient.ensure_path`
+* :meth:`~kazoo.client.KazooClient.create`
+
+:meth:`~kazoo.client.KazooClient.ensure_path` will recursively create the node
+and any nodes in the path necessary along the way, but can not set the data for
+the node, only the ACL.
+
+:meth:`~kazoo.client.KazooClient.create` creates a node and can set the data on
+the node along with a watch function. It requires the path to it to exist
+first, unless the `makepath` option is set to `True`.
+
+.. code-block:: python
+
+    # Ensure a path, create if necessary
+    zk.ensure_path("/my/favorite")
+
+    # Create a node with data
+    zk.create("/my/favorite/node", b"a value")
+
+Reading Data
+------------
+
+Methods:
+
+* :meth:`~kazoo.client.KazooClient.exists`
+* :meth:`~kazoo.client.KazooClient.get`
+* :meth:`~kazoo.client.KazooClient.get_children`
+
+:meth:`~kazoo.client.KazooClient.exists` checks to see if a node exists.
+
+:meth:`~kazoo.client.KazooClient.get` fetches the data of the node along with
+detailed node information in a :class:`~kazoo.protocol.states.ZnodeStat`
+structure.
+
+:meth:`~kazoo.client.KazooClient.get_children` gets a list of the children of
+a given node.
+
+.. code-block:: python
+
+    # Determine if a node exists
+    if zk.exists("/my/favorite"):
+        # Do something
+
+    # Print the version of a node and its data
+    data, stat = zk.get("/my/favorite")
+    print("Version: %s, data: %s" % (stat.version, data.decode("utf-8")))
+
+    # List the children
+    children = zk.get_children("/my/favorite")
+    print("There are %s children with names %s" % (len(children), children))
+
+Updating Data
+-------------
+
+Methods:
+
+* :meth:`~kazoo.client.KazooClient.set`
+
+:meth:`~kazoo.client.KazooClient.set` updates the data for a given node. A
+version for the node can be supplied, which will be required to match before
+updating the data, or a :exc:`~kazoo.exceptions.BadVersionError` will be
+raised instead of updating.
+
+.. code-block:: python
+
+    zk.set("/my/favorite", b"some data")
+
+Deleting Nodes
+--------------
+
+Methods:
+
+* :meth:`~kazoo.client.KazooClient.delete`
+
+:meth:`~kazoo.client.KazooClient.delete` deletes a node, and can optionally
+recursively delete all children of the node as well. A version can be
+supplied when deleting a node which will be required to match the version of
+the node before deleting it or a :exc:`~kazoo.exceptions.BadVersionError`
+will be raised instead of deleting.
+
+.. code-block:: python
+
+    zk.delete("/my/favorite/node", recursive=True)
+
+.. _retrying_commands:
+
+Retrying Commands
+=================
+
+Connections to Zookeeper may get interrupted if the Zookeeper server goes down
+or becomes unreachable. By default, kazoo does not retry commands, so these
+failures will result in an exception being raised. To assist with failures
+kazoo comes with a :meth:`~kazoo.client.KazooClient.retry` helper that will
+retry a function should one of the Zookeeper connection exceptions get raised.
+
+Example:
+
+.. code-block:: python
+
+    result = zk.retry(zk.get, "/path/to/node")
+
+Some commands may have unique behavior that doesn't warrant automatic retries
+on a per command basis. For example, if one creates a node a connection might
+be lost before the command returns successfully but the node actually got
+created. This results in a :exc:`kazoo.exceptions.NodeExistsError` being
+raised when it runs again. A similar unique situation arises when a node is
+created with ephemeral and sequence options set,
+`documented here on the Zookeeper site
+<http://zookeeper.apache.org/doc/trunk/recipes.html#sc_recipes_errorHandlingNote>`_.
+
+Since the :meth:`~kazoo.client.KazooClient.retry` method takes a function to
+call and its arguments, a function that runs multiple Zookeeper commands could
+be passed to it so that the entire function will be retried if the connection
+is lost.
+
+This snippet from the lock implementation shows how it uses retry to re-run the
+function acquiring a lock, and checks to see if it was already created to
+handle this condition:
+
+.. code-block:: python
+
+    # kazoo.recipe.lock snippet
+
+    def acquire(self):
+        """Acquire the mutex, blocking until it is obtained"""
+        try:
+            self.client.retry(self._inner_acquire)
+            self.is_acquired = True
+        except KazooException:
+            # if we did ultimately fail, attempt to clean up
+            self._best_effort_cleanup()
+            self.cancelled = False
+            raise
+
+    def _inner_acquire(self):
+        self.wake_event.clear()
+
+        # make sure our election parent node exists
+        if not self.assured_path:
+            self.client.ensure_path(self.path)
+
+        node = None
+        if self.create_tried:
+            node = self._find_node()
+        else:
+            self.create_tried = True
+
+        if not node:
+            node = self.client.create(self.create_path, self.data,
+                ephemeral=True, sequence=True)
+            # strip off path to node
+            node = node[len(self.path) + 1:]
+
+`create_tried` records whether it has tried to create the node already in the
+event the connection is lost before the node name is returned.
+
+Custom Retries
+--------------
+
+Sometimes you may wish to have specific retry policies for a command or
+set of commands that differs from the
+:meth:`~kazoo.client.KazooClient.retry` method. You can manually create
+a :class:`~kazoo.retry.KazooRetry` instance with the specific retry
+policy you prefer:
+
+.. code-block:: python
+
+    from kazoo.retry import KazooRetry
+
+    kr = KazooRetry(max_tries=3, ignore_expire=False)
+    result = kr(client.get, "/some/path")
+
+This will retry the ``client.get`` command up to 3 times, and raise a
+session expiration if it occurs. You can also make an instance with the
+default behavior that ignores session expiration during a retry.
+
+Watchers
+========
+
+Kazoo can set watch functions on a node that can be triggered either when the
+node has changed or when the children of the node change. This change to the
+node or children can also be the node or its children being deleted.
+
+Watchers can be set in two different ways, the first is the style that
+Zookeeper supports by default for one-time watch events. These watch functions
+will be called once by kazoo, and do *not* receive session events, unlike the
+native Zookeeper watches. Using this style requires the watch function to be
+passed to one of these methods:
+
+* :meth:`~kazoo.client.KazooClient.get`
+* :meth:`~kazoo.client.KazooClient.get_children`
+* :meth:`~kazoo.client.KazooClient.exists`
+
+A watch function passed to :meth:`~kazoo.client.KazooClient.get` or
+:meth:`~kazoo.client.KazooClient.exists` will be called when the data on the
+node changes or the node itself is deleted. It will be passed a
+:class:`~kazoo.protocol.states.WatchedEvent` instance.
+
+.. code-block:: python
+
+    def my_func(event):
+        # check to see what the children are now
+
+    # Call my_func when the children change
+    children = zk.get_children("/my/favorite/node", watch=my_func)
+
+Kazoo includes a higher level API that watches for data and children
+modifications that's easier to use as it doesn't require re-setting the watch
+every time the event is triggered. It also passes in the data and
+:class:`~kazoo.protocol.states.ZnodeStat` when watching a node or the list of
+children when watching a nodes children. Watch functions registered with this
+API will be called immediately and every time there's a change, or until the
+function returns False. If `allow_session_lost` is set to `True`, then the
+function will no longer be called if the session is lost.
+
+The following methods provide this functionality:
+
+* :class:`~kazoo.recipe.watchers.ChildrenWatch`
+* :class:`~kazoo.recipe.watchers.DataWatch`
+
+These classes are available directly on the :class:`~kazoo.client.KazooClient`
+instance and don't require the client object to be passed in when used in this
+manner. The instance returned by instantiating either of the classes can be
+called directly allowing them to be used as decorators:
+
+.. code-block:: python
+
+    @zk.ChildrenWatch("/my/favorite/node")
+    def watch_children(children):
+        print("Children are now: %s" % children)
+    # Above function called immediately, and from then on
+
+    @zk.DataWatch("/my/favorite")
+    def watch_node(data, stat):
+        print("Version: %s, data: %s" % (stat.version, data.decode("utf-8")))
+
+Transactions
+============
+
+.. versionadded:: 0.6
+
+Zookeeper 3.4 and above supports the sending of multiple commands at
+once that will be committed as a single atomic unit. Either they will
+all succeed or they will all fail. The result of a transaction will be
+a list of the success/failure results for each command in the
+transaction.
+
+.. code-block:: python
+
+    transaction = zk.transaction()
+    transaction.check('/node/a', version=3)
+    transaction.create('/node/b', b"a value")
+    results = transaction.commit()
+
+The :meth:`~kazoo.client.KazooClient.transaction` method returns a
+:class:`~kazoo.client.TransactionRequest` instance. It's methods may be
+called to queue commands to be completed in the transaction. When the
+transaction is ready to be sent, the
+:meth:`~kazoo.client.TransactionRequest.commit` method on it is called.
+
+In the example above, there's a command not available unless a
+transaction is being used, `check`. This can check nodes for a specific
+version, which could be used to make the transaction fail if a node
+doesn't match a version that it should be at. In this case the node
+`/node/a` must be at version 3 or `/node/b` will not be created.

+ 1 - 0
desktop/core/ext-py/kazoo-2.0/docs/changelog.rst

@@ -0,0 +1 @@
+.. include:: ../CHANGES.rst

+ 257 - 0
desktop/core/ext-py/kazoo-2.0/docs/conf.py

@@ -0,0 +1,257 @@
+# -*- coding: utf-8 -*-
+#
+# kazoo documentation build configuration file, created by
+# sphinx-quickstart on Fri Nov 11 13:23:01 2011.
+#
+# This file is execfile()d with the current directory set to its containing dir.
+#
+# Note that not all possible configuration values are present in this
+# autogenerated file.
+#
+# All configuration values have a default; values that are commented out
+# serve to show the default.
+import os
+import sys
+
+
+class Mock(object):
+    def __init__(self, *args):
+        pass
+
+    def __getattr__(self, name):
+        return Mock
+
+MOCK_MODULES = ['zookeeper']
+for mod_name in MOCK_MODULES:
+    sys.modules[mod_name] = Mock()
+
+# If extensions (or modules to document with autodoc) are in another directory,
+# add these directories to sys.path here. If the directory is relative to the
+# documentation root, use os.path.abspath to make it absolute, like shown here.
+sys.path.insert(0, os.path.abspath('..'))
+
+# -- General configuration -----------------------------------------------------
+
+# If your documentation needs a minimal Sphinx version, state it here.
+#needs_sphinx = '1.0'
+
+# Add any Sphinx extension module names here, as strings. They can be extensions
+# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
+extensions = [
+    'sphinx.ext.autodoc',
+    'sphinx.ext.doctest',
+    'sphinx.ext.viewcode',
+]
+
+# Add any paths that contain templates here, relative to this directory.
+templates_path = ['_templates']
+
+# The suffix of source filenames.
+source_suffix = '.rst'
+
+# The encoding of source files.
+#source_encoding = 'utf-8-sig'
+
+# The master toctree document.
+master_doc = 'index'
+
+# General information about the project.
+project = u'kazoo'
+copyright = u'2011-2014, Kazoo team'
+
+# The version info for the project you're documenting, acts as replacement for
+# |version| and |release|, also used in various other places throughout the
+# built documents.
+#
+# The short X.Y version.
+version = '2.0'
+# The full version, including alpha/beta/rc tags.
+release = '2.0'
+
+# The language for content autogenerated by Sphinx. Refer to documentation
+# for a list of supported languages.
+#language = None
+
+# There are two options for replacing |today|: either, you set today to some
+# non-false value, then it is used:
+#today = ''
+# Else, today_fmt is used as the format for a strftime call.
+#today_fmt = '%B %d, %Y'
+
+# List of patterns, relative to source directory, that match files and
+# directories to ignore when looking for source files.
+exclude_patterns = ['_build']
+
+# The reST default role (used for this markup: `text`) to use for all documents.
+#default_role = None
+
+# If true, '()' will be appended to :func: etc. cross-reference text.
+#add_function_parentheses = True
+
+# If true, the current module name will be prepended to all description
+# unit titles (such as .. function::).
+#add_module_names = True
+
+# If true, sectionauthor and moduleauthor directives will be shown in the
+# output. They are ignored by default.
+#show_authors = False
+
+# The name of the Pygments (syntax highlighting) style to use.
+pygments_style = 'sphinx'
+
+# A list of ignored prefixes for module index sorting.
+#modindex_common_prefix = []
+
+
+# -- Options for HTML output ---------------------------------------------------
+
+# The theme to use for HTML and HTML Help pages.  See the documentation for
+# a list of builtin themes.
+html_theme = 'default'
+
+# Theme options are theme-specific and customize the look and feel of a theme
+# further.  For a list of options available for each theme, see the
+# documentation.
+#html_theme_options = {}
+
+# Add any paths that contain custom themes here, relative to this directory.
+#html_theme_path = []
+
+# The name for this set of Sphinx documents.  If None, it defaults to
+# "<project> v<release> documentation".
+#html_title = None
+
+# A shorter title for the navigation bar.  Default is the same as html_title.
+#html_short_title = None
+
+# The name of an image file (relative to this directory) to place at the top
+# of the sidebar.
+#html_logo = None
+
+# The name of an image file (within the static path) to use as favicon of the
+# docs.  This file should be a Windows icon file (.ico) being 16x16 or 32x32
+# pixels large.
+#html_favicon = None
+
+# Add any paths that contain custom static files (such as style sheets) here,
+# relative to this directory. They are copied after the builtin static files,
+# so a file named "default.css" will overwrite the builtin "default.css".
+html_static_path = []
+
+# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
+# using the given strftime format.
+#html_last_updated_fmt = '%b %d, %Y'
+
+# If true, SmartyPants will be used to convert quotes and dashes to
+# typographically correct entities.
+#html_use_smartypants = True
+
+# Custom sidebar templates, maps document names to template names.
+#html_sidebars = {}
+
+# Additional templates that should be rendered to pages, maps page names to
+# template names.
+#html_additional_pages = {}
+
+# If false, no module index is generated.
+#html_domain_indices = True
+
+# If false, no index is generated.
+#html_use_index = True
+
+# If true, the index is split into individual pages for each letter.
+#html_split_index = False
+
+# If true, links to the reST sources are added to the pages.
+#html_show_sourcelink = True
+
+# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
+#html_show_sphinx = True
+
+# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
+#html_show_copyright = True
+
+# If true, an OpenSearch description file will be output, and all pages will
+# contain a <link> tag referring to it.  The value of this option must be the
+# base URL from which the finished HTML is served.
+#html_use_opensearch = ''
+
+# This is the file name suffix for HTML files (e.g. ".xhtml").
+#html_file_suffix = None
+
+# Output file base name for HTML help builder.
+htmlhelp_basename = 'kazoodoc'
+
+
+# -- Options for LaTeX output --------------------------------------------------
+
+latex_elements = {
+# The paper size ('letterpaper' or 'a4paper').
+#'papersize': 'letterpaper',
+
+# The font size ('10pt', '11pt' or '12pt').
+#'pointsize': '10pt',
+
+# Additional stuff for the LaTeX preamble.
+#'preamble': '',
+}
+
+# Grouping the document tree into LaTeX files. List of tuples
+# (source start file, target name, title, author, documentclass [howto/manual]).
+latex_documents = [
+  ('index', 'kazoo.tex', u'kazoo Documentation',
+   u'Various Authors', 'manual'),
+]
+
+# The name of an image file (relative to this directory) to place at the top of
+# the title page.
+#latex_logo = None
+
+# For "manual" documents, if this is true, then toplevel headings are parts,
+# not chapters.
+#latex_use_parts = False
+
+# If true, show page references after internal links.
+#latex_show_pagerefs = False
+
+# If true, show URL addresses after external links.
+#latex_show_urls = False
+
+# Documents to append as an appendix to all manuals.
+#latex_appendices = []
+
+# If false, no module index is generated.
+#latex_domain_indices = True
+
+
+# -- Options for manual page output --------------------------------------------
+
+# One entry per manual page. List of tuples
+# (source start file, name, description, authors, manual section).
+man_pages = [
+    ('index', 'kazoo', u'kazoo Documentation',
+     [u'Various Authors'], 1)
+]
+
+# If true, show URL addresses after external links.
+#man_show_urls = False
+
+
+# -- Options for Texinfo output ------------------------------------------------
+
+# Grouping the document tree into Texinfo files. List of tuples
+# (source start file, target name, title, author,
+#  dir menu entry, description, category)
+texinfo_documents = [
+  ('index', 'kazoo', u'kazoo Documentation', u'Various Authors',
+   'kazoo', 'One line description of project.', 'Miscellaneous'),
+]
+
+# Documents to append as an appendix to all manuals.
+#texinfo_appendices = []
+
+# If false, no module index is generated.
+#texinfo_domain_indices = True
+
+# How to display URL addresses: 'footnote', 'no', or 'inline'.
+#texinfo_show_urls = 'footnote'

+ 12 - 0
desktop/core/ext-py/kazoo-2.0/docs/glossary.rst

@@ -0,0 +1,12 @@
+.. _glossary:
+
+Glossary
+========
+
+
+.. glossary::
+
+    Zookeeper
+        `Apache Zookeeper <http://zookeeper.apache.org/>`_ is a centralized
+        service for maintaining configuration information, naming, providing
+        distributed synchronization, and providing group services.

+ 45 - 0
desktop/core/ext-py/kazoo-2.0/docs/implementation.rst

@@ -0,0 +1,45 @@
+.. _implementation_details:
+
+======================
+Implementation Details
+======================
+
+Up to version 0.3 kazoo used the Python bindings to the Zookeeper C library.
+Unfortunately those bindings are fairly buggy and required a fair share of
+weird workarounds to interface with the native OS thread used in those
+bindings.
+
+Starting with version 0.4 kazoo implements the entire Zookeeper wire protocol
+itself in pure Python. Doing so removed the need for the workarounds and made
+it much easier to implement the features missing in the C bindings.
+
+Handlers
+========
+
+Both the Kazoo handlers run 3 separate queues to help alleviate deadlock issues
+and ensure consistent execution order regardless of environment. The
+:class:`~kazoo.handlers.gevent.SequentialGeventHandler` runs a separate
+greenlet for each queue that processes the callbacks queued in order. The
+:class:`~kazoo.handlers.threading.SequentialThreadingHandler` runs a separate
+thread for each queue that processes the callbacks queued in order (thus the
+naming scheme which notes they are sequential in anticipation that there could
+be handlers shipped in the future which don't make this guarantee).
+
+Callbacks are queued by type, the 3 types being:
+
+1. Session events (State changes, registered listener functions)
+2. Watch events (Watch callbacks, DataWatch, and ChildrenWatch functions)
+3. Completion callbacks (Functions chained to
+   :class:`~kazoo.interfaces.IAsyncResult` objects)
+
+This ensures that calls can be made to Zookeeper from any callback **except for
+a state listener** without worrying that critical session events will be
+blocked.
+
+.. warning::
+
+    Its important to remember that if you write code that blocks in one of
+    these functions then no queued functions of that type will be executed
+    until the code stops blocking. If your code might block, it should run
+    itself in a separate greenlet/thread so that the other callbacks can
+    run.

+ 106 - 0
desktop/core/ext-py/kazoo-2.0/docs/index.rst

@@ -0,0 +1,106 @@
+=====
+kazoo
+=====
+
+Kazoo is a Python library designed to make working with :term:`Zookeeper` a
+more hassle-free experience that is less prone to errors.
+
+Kazoo features:
+
+* A wide range of recipe implementations, like Lock, Election or Queue
+* Data and Children Watchers
+* Simplified Zookeeper connection state tracking
+* Unified asynchronous API for use with greenlets or threads
+* Support for gevent 0.13 and gevent 1.0
+* Support for Zookeeper 3.3 and 3.4 servers
+* Integrated testing helpers for Zookeeper clusters
+* Pure-Python based implementation of the wire protocol, avoiding all the
+  memory leaks, lacking features, and debugging madness of the C library
+
+Kazoo is heavily inspired by `Netflix Curator`_ simplifications and helpers.
+
+.. note::
+
+    You should be familiar with Zookeeper and have read the `Zookeeper
+    Programmers Guide`_ before using `kazoo`.
+
+Reference Docs
+==============
+
+.. toctree::
+   :maxdepth: 1
+
+   install
+   basic_usage
+   async_usage
+   implementation
+   testing
+   api
+   Changelog <changelog>
+   Contributing <contributing>
+
+Why
+===
+
+Using :term:`Zookeeper` in a safe manner can be difficult due to the variety of
+edge-cases in :term:`Zookeeper` and other bugs that have been present in the
+Python C binding. Due to how the C library utilizes a separate C thread for
+:term:`Zookeeper` communication some libraries like `gevent`_ also don't work
+properly by default.
+
+By utilizing a pure Python implementation, Kazoo handles all of these
+cases and provides a new asynchronous API which is consistent when
+using threads or `gevent`_ greenlets.
+
+Source Code
+===========
+
+All source code is available on `github under kazoo <https://github.com/python-
+zk/kazoo>`_.
+
+Bugs/Support
+============
+
+Bugs and support issues should be reported on the `kazoo github issue tracker
+<https://github.com/python-zk/kazoo/issues>`_.
+
+The developers of ``kazoo`` can frequently be found on the Freenode IRC
+network in the #zookeeper channel.
+
+For general discussions, please use the
+`python-zk <https://groups.google.com/forum/#!forum/python-zk>`_ mailing list
+hosted on Google Groups.
+
+Indices and tables
+==================
+
+* :ref:`genindex`
+* :ref:`modindex`
+* :ref:`glossary`
+
+.. toctree::
+   :hidden:
+
+   glossary
+
+License
+=======
+
+``kazoo`` is offered under the Apache License 2.0.
+
+Authors
+=======
+
+``kazoo`` started under the `Nimbus Project`_ and through collaboration with
+the open-source community has been merged with code from `Mozilla`_ and the
+`Zope Corporation`_. It has since gathered an active community of over two
+dozen contributors.
+
+.. _Apache Zookeeper: http://zookeeper.apache.org/
+.. _Zookeeper Programmers Guide: http://zookeeper.apache.org/doc/trunk/zookeeperProgrammers.html
+.. _Zookeeper Recipes: http://zookeeper.apache.org/doc/current/recipes.html#sc_recoverableSharedLocks
+.. _Nimbus Project: http://www.nimbusproject.org/
+.. _Zope Corporation: http://zope.com/
+.. _Mozilla: http://www.mozilla.org/
+.. _Netflix Curator: https://github.com/Netflix/curator
+.. _gevent: http://gevent.org/

+ 14 - 0
desktop/core/ext-py/kazoo-2.0/docs/install.rst

@@ -0,0 +1,14 @@
+.. _install:
+
+==========
+Installing
+==========
+
+kazoo can be installed via ``pip`` or ``easy_install``:
+
+.. code-block:: bash
+
+    $ pip install kazoo
+
+Kazoo implements the Zookeeper protocol in pure Python, so you don't need
+any Python Zookeeper C bindings installed.

+ 190 - 0
desktop/core/ext-py/kazoo-2.0/docs/make.bat

@@ -0,0 +1,190 @@
+@ECHO OFF
+
+REM Command file for Sphinx documentation
+
+if "%SPHINXBUILD%" == "" (
+	set SPHINXBUILD=sphinx-build
+)
+set BUILDDIR=_build
+set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% .
+set I18NSPHINXOPTS=%SPHINXOPTS% .
+if NOT "%PAPER%" == "" (
+	set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS%
+	set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS%
+)
+
+if "%1" == "" goto help
+
+if "%1" == "help" (
+	:help
+	echo.Please use `make ^<target^>` where ^<target^> is one of
+	echo.  html       to make standalone HTML files
+	echo.  dirhtml    to make HTML files named index.html in directories
+	echo.  singlehtml to make a single large HTML file
+	echo.  pickle     to make pickle files
+	echo.  json       to make JSON files
+	echo.  htmlhelp   to make HTML files and a HTML help project
+	echo.  qthelp     to make HTML files and a qthelp project
+	echo.  devhelp    to make HTML files and a Devhelp project
+	echo.  epub       to make an epub
+	echo.  latex      to make LaTeX files, you can set PAPER=a4 or PAPER=letter
+	echo.  text       to make text files
+	echo.  man        to make manual pages
+	echo.  texinfo    to make Texinfo files
+	echo.  gettext    to make PO message catalogs
+	echo.  changes    to make an overview over all changed/added/deprecated items
+	echo.  linkcheck  to check all external links for integrity
+	echo.  doctest    to run all doctests embedded in the documentation if enabled
+	goto end
+)
+
+if "%1" == "clean" (
+	for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i
+	del /q /s %BUILDDIR%\*
+	goto end
+)
+
+if "%1" == "html" (
+	%SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html
+	if errorlevel 1 exit /b 1
+	echo.
+	echo.Build finished. The HTML pages are in %BUILDDIR%/html.
+	goto end
+)
+
+if "%1" == "dirhtml" (
+	%SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml
+	if errorlevel 1 exit /b 1
+	echo.
+	echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml.
+	goto end
+)
+
+if "%1" == "singlehtml" (
+	%SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml
+	if errorlevel 1 exit /b 1
+	echo.
+	echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml.
+	goto end
+)
+
+if "%1" == "pickle" (
+	%SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle
+	if errorlevel 1 exit /b 1
+	echo.
+	echo.Build finished; now you can process the pickle files.
+	goto end
+)
+
+if "%1" == "json" (
+	%SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json
+	if errorlevel 1 exit /b 1
+	echo.
+	echo.Build finished; now you can process the JSON files.
+	goto end
+)
+
+if "%1" == "htmlhelp" (
+	%SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp
+	if errorlevel 1 exit /b 1
+	echo.
+	echo.Build finished; now you can run HTML Help Workshop with the ^
+.hhp project file in %BUILDDIR%/htmlhelp.
+	goto end
+)
+
+if "%1" == "qthelp" (
+	%SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp
+	if errorlevel 1 exit /b 1
+	echo.
+	echo.Build finished; now you can run "qcollectiongenerator" with the ^
+.qhcp project file in %BUILDDIR%/qthelp, like this:
+	echo.^> qcollectiongenerator %BUILDDIR%\qthelp\kazoo.qhcp
+	echo.To view the help file:
+	echo.^> assistant -collectionFile %BUILDDIR%\qthelp\kazoo.ghc
+	goto end
+)
+
+if "%1" == "devhelp" (
+	%SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp
+	if errorlevel 1 exit /b 1
+	echo.
+	echo.Build finished.
+	goto end
+)
+
+if "%1" == "epub" (
+	%SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub
+	if errorlevel 1 exit /b 1
+	echo.
+	echo.Build finished. The epub file is in %BUILDDIR%/epub.
+	goto end
+)
+
+if "%1" == "latex" (
+	%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
+	if errorlevel 1 exit /b 1
+	echo.
+	echo.Build finished; the LaTeX files are in %BUILDDIR%/latex.
+	goto end
+)
+
+if "%1" == "text" (
+	%SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text
+	if errorlevel 1 exit /b 1
+	echo.
+	echo.Build finished. The text files are in %BUILDDIR%/text.
+	goto end
+)
+
+if "%1" == "man" (
+	%SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man
+	if errorlevel 1 exit /b 1
+	echo.
+	echo.Build finished. The manual pages are in %BUILDDIR%/man.
+	goto end
+)
+
+if "%1" == "texinfo" (
+	%SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo
+	if errorlevel 1 exit /b 1
+	echo.
+	echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo.
+	goto end
+)
+
+if "%1" == "gettext" (
+	%SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale
+	if errorlevel 1 exit /b 1
+	echo.
+	echo.Build finished. The message catalogs are in %BUILDDIR%/locale.
+	goto end
+)
+
+if "%1" == "changes" (
+	%SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes
+	if errorlevel 1 exit /b 1
+	echo.
+	echo.The overview file is in %BUILDDIR%/changes.
+	goto end
+)
+
+if "%1" == "linkcheck" (
+	%SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck
+	if errorlevel 1 exit /b 1
+	echo.
+	echo.Link check complete; look for any errors in the above output ^
+or in %BUILDDIR%/linkcheck/output.txt.
+	goto end
+)
+
+if "%1" == "doctest" (
+	%SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest
+	if errorlevel 1 exit /b 1
+	echo.
+	echo.Testing of doctests in the sources finished, look at the ^
+results in %BUILDDIR%/doctest/output.txt.
+	goto end
+)
+
+:end

+ 81 - 0
desktop/core/ext-py/kazoo-2.0/docs/testing.rst

@@ -0,0 +1,81 @@
+.. _testing:
+
+=======
+Testing
+=======
+
+Kazoo has several test harnesses used internally for its own tests that are
+exposed as public API's for use in your own tests for common Zookeeper cluster
+management and session testing. They can be mixed in with your own `unittest`
+or `nose` tests along with a `mock` object that allows you to force specific
+`KazooClient` commands to fail in various ways.
+
+The test harness needs to be able to find the Zookeeper Java libraries. You
+need to specify an environment variable called `ZOOKEEPER_PATH` and point it
+to their location, for example `/usr/share/java`. The directory should contain
+a `zookeeper-*.jar` and a `lib` directory containing at least a `log4j-*.jar`.
+
+If your Java setup is complex, you may also override our classpath mechanism
+completely by specifying an environment variable called `ZOOKEEPER_CLASSPATH`.
+If provided, it will be used unmodified as the Java classpath for Zookeeper.
+
+You can specify an optional `ZOOKEEPER_PORT_OFFSET` environment variable to
+influence the ports the cluster is using. By default the offset is 20000 and
+a cluster with three members will use ports 20000, 20010 and 20020.
+
+
+Kazoo Test Harness
+==================
+
+The :class:`~kazoo.testing.harness.KazooTestHarness` can be used directly or
+mixed in with your test code.
+
+Example:
+
+.. code-block:: python
+
+    from kazoo.testing import KazooTestHarness
+
+    class MyTest(KazooTestHarness):
+        def setUp(self):
+            self.setup_zookeeper()
+
+        def tearDown(self):
+            self.teardown_zookeeper()
+
+        def testmycode(self):
+            self.client.ensure_path('/test/path')
+            result = self.client.get('/test/path')
+            ...
+
+
+Kazoo Test Case
+===============
+
+The :class:`~kazoo.testing.harness.KazooTestCase` is complete test case that
+is equivalent to the mixin setup of
+:class:`~kazoo.testing.harness.KazooTestHarness`. An equivalent test to the
+one above:
+
+.. code-block:: python
+
+    from kazoo.testing import KazooTestCase
+
+    class MyTest(KazooTestCase):
+        def testmycode(self):
+            self.client.ensure_path('/test/path')
+            result = self.client.get('/test/path')
+            ...
+
+Zake
+====
+
+For those that do not need (or desire) to setup a Zookeeper cluster to test
+integration with kazoo there is also a library called
+`zake <https://pypi.python.org/pypi/zake/>`_. Contributions to
+`Zake's github repository <https://github.com/yahoo/Zake>`_ are welcome.
+
+Zake can be used to provide a *mock client* to layers of your application that
+interact with kazoo (using the same client interface) during testing to allow
+for introspection of what was stored, which watchers are active (and more)
+after your test of your application code has finished.

+ 1 - 0
desktop/core/ext-py/kazoo-2.0/kazoo/__init__.py

@@ -0,0 +1 @@
+#

+ 1412 - 0
desktop/core/ext-py/kazoo-2.0/kazoo/client.py

@@ -0,0 +1,1412 @@
+"""Kazoo Zookeeper Client"""
+import inspect
+import logging
+import os
+import re
+import warnings
+from collections import defaultdict, deque
+from functools import partial
+from os.path import split
+
+from kazoo.exceptions import (
+    AuthFailedError,
+    ConfigurationError,
+    ConnectionClosedError,
+    ConnectionLoss,
+    NoNodeError,
+    NodeExistsError,
+    SessionExpiredError,
+    WriterNotClosedException,
+)
+from kazoo.handlers.threading import SequentialThreadingHandler
+from kazoo.handlers.utils import capture_exceptions, wrap
+from kazoo.hosts import collect_hosts
+from kazoo.loggingsupport import BLATHER
+from kazoo.protocol.connection import ConnectionHandler
+from kazoo.protocol.paths import normpath
+from kazoo.protocol.paths import _prefix_root
+from kazoo.protocol.serialization import (
+    Auth,
+    CheckVersion,
+    CloseInstance,
+    Create,
+    Delete,
+    Exists,
+    GetChildren,
+    GetChildren2,
+    GetACL,
+    SetACL,
+    GetData,
+    SetData,
+    Sync,
+    Transaction
+)
+from kazoo.protocol.states import KazooState
+from kazoo.protocol.states import KeeperState
+from kazoo.retry import KazooRetry
+from kazoo.security import ACL
+from kazoo.security import OPEN_ACL_UNSAFE
+
+# convenience API
+from kazoo.recipe.barrier import Barrier
+from kazoo.recipe.barrier import DoubleBarrier
+from kazoo.recipe.counter import Counter
+from kazoo.recipe.election import Election
+from kazoo.recipe.lock import Lock
+from kazoo.recipe.lock import Semaphore
+from kazoo.recipe.partitioner import SetPartitioner
+from kazoo.recipe.party import Party
+from kazoo.recipe.party import ShallowParty
+from kazoo.recipe.queue import Queue
+from kazoo.recipe.queue import LockingQueue
+from kazoo.recipe.watchers import ChildrenWatch
+from kazoo.recipe.watchers import DataWatch
+
+try:  # pragma: nocover
+    basestring
+except NameError:  # pragma: nocover
+    basestring = str
+
+LOST_STATES = (KeeperState.EXPIRED_SESSION, KeeperState.AUTH_FAILED,
+               KeeperState.CLOSED)
+ENVI_VERSION = re.compile('[\w\s:.]*=([\d\.]*).*', re.DOTALL)
+log = logging.getLogger(__name__)
+
+
+_RETRY_COMPAT_DEFAULTS = dict(
+    max_retries=None,
+    retry_delay=0.1,
+    retry_backoff=2,
+    retry_jitter=0.8,
+    retry_max_delay=3600,
+)
+
+_RETRY_COMPAT_MAPPING = dict(
+    max_retries='max_tries',
+    retry_delay='delay',
+    retry_backoff='backoff',
+    retry_jitter='max_jitter',
+    retry_max_delay='max_delay',
+)
+
+
+class KazooClient(object):
+    """An Apache Zookeeper Python client supporting alternate callback
+    handlers and high-level functionality.
+
+    Watch functions registered with this class will not get session
+    events, unlike the default Zookeeper watches. They will also be
+    called with a single argument, a
+    :class:`~kazoo.protocol.states.WatchedEvent` instance.
+
+    """
+    def __init__(self, hosts='127.0.0.1:2181',
+                 timeout=10.0, client_id=None, handler=None,
+                 default_acl=None, auth_data=None, read_only=None,
+                 randomize_hosts=True, connection_retry=None,
+                 command_retry=None, logger=None, **kwargs):
+        """Create a :class:`KazooClient` instance. All time arguments
+        are in seconds.
+
+        :param hosts: Comma-separated list of hosts to connect to
+                      (e.g. 127.0.0.1:2181,127.0.0.1:2182,[::1]:2183).
+        :param timeout: The longest to wait for a Zookeeper connection.
+        :param client_id: A Zookeeper client id, used when
+                          re-establishing a prior session connection.
+        :param handler: An instance of a class implementing the
+                        :class:`~kazoo.interfaces.IHandler` interface
+                        for callback handling.
+        :param default_acl: A default ACL used on node creation.
+        :param auth_data:
+            A list of authentication credentials to use for the
+            connection. Should be a list of (scheme, credential)
+            tuples as :meth:`add_auth` takes.
+        :param read_only: Allow connections to read only servers.
+        :param randomize_hosts: By default randomize host selection.
+        :param connection_retry:
+            A :class:`kazoo.retry.KazooRetry` object to use for
+            retrying the connection to Zookeeper. Also can be a dict of
+            options which will be used for creating one.
+        :param command_retry:
+            A :class:`kazoo.retry.KazooRetry` object to use for
+            the :meth:`KazooClient.retry` method. Also can be a dict of
+            options which will be used for creating one.
+        :param logger: A custom logger to use instead of the module
+            global `log` instance.
+
+        Basic Example:
+
+        .. code-block:: python
+
+            zk = KazooClient()
+            zk.start()
+            children = zk.get_children('/')
+            zk.stop()
+
+        As a convenience all recipe classes are available as attributes
+        and get automatically bound to the client. For example::
+
+            zk = KazooClient()
+            zk.start()
+            lock = zk.Lock('/lock_path')
+
+        .. versionadded:: 0.6
+            The read_only option. Requires Zookeeper 3.4+
+
+        .. versionadded:: 0.6
+            The retry_max_delay option.
+
+        .. versionadded:: 0.6
+            The randomize_hosts option.
+
+        .. versionchanged:: 0.8
+            Removed the unused watcher argument (was second argument).
+
+        .. versionadded:: 1.2
+            The connection_retry, command_retry and logger options.
+
+        """
+        self.logger = logger or log
+
+        # Record the handler strategy used
+        self.handler = handler if handler else SequentialThreadingHandler()
+        if inspect.isclass(self.handler):
+            raise ConfigurationError("Handler must be an instance of a class, "
+                                     "not the class: %s" % self.handler)
+
+        self.auth_data = auth_data if auth_data else set([])
+        self.default_acl = default_acl
+        self.randomize_hosts = randomize_hosts
+        self.hosts = None
+        self.chroot = None
+        self.set_hosts(hosts)
+
+        # Curator like simplified state tracking, and listeners for
+        # state transitions
+        self._state = KeeperState.CLOSED
+        self.state = KazooState.LOST
+        self.state_listeners = set()
+
+        self._reset()
+        self.read_only = read_only
+
+        if client_id:
+            self._session_id = client_id[0]
+            self._session_passwd = client_id[1]
+        else:
+            self._reset_session()
+
+        # ZK uses milliseconds
+        self._session_timeout = int(timeout * 1000)
+
+        # We use events like twitter's client to track current and
+        # desired state (connected, and whether to shutdown)
+        self._live = self.handler.event_object()
+        self._writer_stopped = self.handler.event_object()
+        self._stopped = self.handler.event_object()
+        self._stopped.set()
+        self._writer_stopped.set()
+
+        self.retry = self._conn_retry = None
+
+        if type(connection_retry) is dict:
+            self._conn_retry = KazooRetry(**connection_retry)
+        elif type(connection_retry) is KazooRetry:
+            self._conn_retry = connection_retry
+
+        if type(command_retry) is dict:
+            self.retry = KazooRetry(**command_retry)
+        elif type(command_retry) is KazooRetry:
+            self.retry = command_retry
+
+
+        if type(self._conn_retry) is KazooRetry:
+            if self.handler.sleep_func != self._conn_retry.sleep_func:
+                raise ConfigurationError("Retry handler and event handler "
+                                         " must use the same sleep func")
+
+        if type(self.retry) is KazooRetry:
+            if self.handler.sleep_func != self.retry.sleep_func:
+                raise ConfigurationError("Command retry handler and event "
+                                         "handler must use the same sleep func")
+
+        if self.retry is None or self._conn_retry is None:
+            old_retry_keys = dict(_RETRY_COMPAT_DEFAULTS)
+            for key in old_retry_keys:
+                try:
+                    old_retry_keys[key] = kwargs.pop(key)
+                    warnings.warn('Passing retry configuration param %s to the'
+                            ' client directly is deprecated, please pass a'
+                            ' configured retry object (using param %s)' % (
+                                key, _RETRY_COMPAT_MAPPING[key]),
+                            DeprecationWarning, stacklevel=2)
+                except KeyError:
+                    pass
+
+            retry_keys = {}
+            for oldname, value in old_retry_keys.items():
+                retry_keys[_RETRY_COMPAT_MAPPING[oldname]] = value
+
+            if self._conn_retry is None:
+                self._conn_retry = KazooRetry(
+                    sleep_func=self.handler.sleep_func,
+                    **retry_keys)
+            if self.retry is None:
+                self.retry = KazooRetry(
+                    sleep_func=self.handler.sleep_func,
+                    **retry_keys)
+
+        self._conn_retry.interrupt = lambda: self._stopped.is_set()
+        self._connection = ConnectionHandler(self, self._conn_retry.copy(),
+            logger=self.logger)
+
+        # Every retry call should have its own copy of the retry helper
+        # to avoid shared retry counts
+        self._retry = self.retry
+        def _retry(*args, **kwargs):
+            return self._retry.copy()(*args, **kwargs)
+        self.retry = _retry
+
+        self.Barrier = partial(Barrier, self)
+        self.Counter = partial(Counter, self)
+        self.DoubleBarrier = partial(DoubleBarrier, self)
+        self.ChildrenWatch = partial(ChildrenWatch, self)
+        self.DataWatch = partial(DataWatch, self)
+        self.Election = partial(Election, self)
+        self.Lock = partial(Lock, self)
+        self.Party = partial(Party, self)
+        self.Queue = partial(Queue, self)
+        self.LockingQueue = partial(LockingQueue, self)
+        self.SetPartitioner = partial(SetPartitioner, self)
+        self.Semaphore = partial(Semaphore, self)
+        self.ShallowParty = partial(ShallowParty, self)
+
+         # If we got any unhandled keywords, complain like python would
+        if kwargs:
+            raise TypeError('__init__() got unexpected keyword arguments: %s'
+                            % (kwargs.keys(),))
+
+    def _reset(self):
+        """Resets a variety of client states for a new connection."""
+        self._queue = deque()
+        self._pending = deque()
+
+        self._reset_watchers()
+        self._reset_session()
+        self.last_zxid = 0
+        self._protocol_version = None
+
+    def _reset_watchers(self):
+        self._child_watchers = defaultdict(set)
+        self._data_watchers = defaultdict(set)
+
+    def _reset_session(self):
+        self._session_id = None
+        self._session_passwd = b'\x00' * 16
+
+    @property
+    def client_state(self):
+        """Returns the last Zookeeper client state
+
+        This is the non-simplified state information and is generally
+        not as useful as the simplified KazooState information.
+
+        """
+        return self._state
+
+    @property
+    def client_id(self):
+        """Returns the client id for this Zookeeper session if
+        connected.
+
+        :returns: client id which consists of the session id and
+                  password.
+        :rtype: tuple
+        """
+        if self._live.is_set():
+            return (self._session_id, self._session_passwd)
+        return None
+
+    @property
+    def connected(self):
+        """Returns whether the Zookeeper connection has been
+        established."""
+        return self._live.is_set()
+
+    def set_hosts(self, hosts, randomize_hosts=None):
+        """ sets the list of hosts used by this client.
+
+        This function accepts the same format hosts parameter as the init
+        function and sets the client to use the new hosts the next time it
+        needs to look up a set of hosts. This function does not affect the
+        current connected status.
+
+        It is not currently possible to change the chroot with this function,
+        setting a host list with a new chroot will raise a ConfigurationError.
+
+        :param hosts: see description in :meth:`KazooClient.__init__`
+        :param randomize_hosts: override client default for host randomization
+        :raises:
+            :exc:`ConfigurationError` if the hosts argument changes the chroot
+
+        .. versionadded:: 1.4
+
+        .. warning::
+
+            Using this function to point a client to a completely disparate
+            zookeeper server cluster has undefined behavior.
+
+        """
+
+        if randomize_hosts is None:
+            randomize_hosts = self.randomize_hosts
+
+        self.hosts, chroot = collect_hosts(hosts, randomize_hosts)
+
+        if chroot:
+            new_chroot = normpath(chroot)
+        else:
+            new_chroot = ''
+
+        if self.chroot is not None and new_chroot != self.chroot:
+            raise ConfigurationError("Changing chroot at runtime is not "
+                                     "currently supported")
+
+        self.chroot = new_chroot
+
+    def add_listener(self, listener):
+        """Add a function to be called for connection state changes.
+
+        This function will be called with a
+        :class:`~kazoo.protocol.states.KazooState` instance indicating
+        the new connection state on state transitions.
+
+        .. warning::
+
+            This function must not block. If its at all likely that it
+            might need data or a value that could result in blocking
+            than the :meth:`~kazoo.interfaces.IHandler.spawn` method
+            should be used so that the listener can return immediately.
+
+        """
+        if not (listener and callable(listener)):
+            raise ConfigurationError("listener must be callable")
+        self.state_listeners.add(listener)
+
+    def remove_listener(self, listener):
+        """Remove a listener function"""
+        self.state_listeners.discard(listener)
+
+    def _make_state_change(self, state):
+        # skip if state is current
+        if self.state == state:
+            return
+
+        self.state = state
+
+        # Create copy of listeners for iteration in case one needs to
+        # remove itself
+        for listener in list(self.state_listeners):
+            try:
+                remove = listener(state)
+                if remove is True:
+                    self.remove_listener(listener)
+            except Exception:
+                self.logger.exception("Error in connection state listener")
+
+    def _session_callback(self, state):
+        if state == self._state:
+            return
+
+        # Note that we don't check self.state == LOST since that's also
+        # the client's initial state
+        dead_state = self._state in LOST_STATES
+        self._state = state
+
+        # If we were previously closed or had an expired session, and
+        # are now connecting, don't bother with the rest of the
+        # transitions since they only apply after
+        # we've established a connection
+        if dead_state and state == KeeperState.CONNECTING:
+            self.logger.log(BLATHER, "Skipping state change")
+            return
+
+        if state in (KeeperState.CONNECTED, KeeperState.CONNECTED_RO):
+            self.logger.info("Zookeeper connection established, state: %s", state)
+            self._live.set()
+            self._make_state_change(KazooState.CONNECTED)
+        elif state in LOST_STATES:
+            self.logger.info("Zookeeper session lost, state: %s", state)
+            self._live.clear()
+            self._make_state_change(KazooState.LOST)
+            self._notify_pending(state)
+            self._reset()
+        else:
+            self.logger.info("Zookeeper connection lost")
+            # Connection lost
+            self._live.clear()
+            self._notify_pending(state)
+            self._make_state_change(KazooState.SUSPENDED)
+            self._reset_watchers()
+
+    def _notify_pending(self, state):
+        """Used to clear a pending response queue and request queue
+        during connection drops."""
+        if state == KeeperState.AUTH_FAILED:
+            exc = AuthFailedError()
+        elif state == KeeperState.EXPIRED_SESSION:
+            exc = SessionExpiredError()
+        else:
+            exc = ConnectionLoss()
+
+        while True:
+            try:
+                request, async_object, xid = self._pending.popleft()
+                if async_object:
+                    async_object.set_exception(exc)
+            except IndexError:
+                break
+
+        while True:
+            try:
+                request, async_object = self._queue.popleft()
+                if async_object:
+                    async_object.set_exception(exc)
+            except IndexError:
+                break
+
+    def _safe_close(self):
+        self.handler.stop()
+        timeout = self._session_timeout // 1000
+        if timeout < 10:
+            timeout = 10
+        if not self._connection.stop(timeout):
+            raise WriterNotClosedException(
+                "Writer still open from prior connection "
+                "and wouldn't close after %s seconds" % timeout)
+
+    def _call(self, request, async_object):
+        """Ensure there's an active connection and put the request in
+        the queue if there is.
+
+        Returns False if the call short circuits due to AUTH_FAILED,
+        CLOSED, EXPIRED_SESSION or CONNECTING state.
+
+        """
+
+        if self._state == KeeperState.AUTH_FAILED:
+            async_object.set_exception(AuthFailedError())
+            return False
+        elif self._state == KeeperState.CLOSED:
+            async_object.set_exception(ConnectionClosedError(
+                "Connection has been closed"))
+            return False
+        elif self._state in (KeeperState.EXPIRED_SESSION,
+                             KeeperState.CONNECTING):
+            async_object.set_exception(SessionExpiredError())
+            return False
+
+        self._queue.append((request, async_object))
+
+        # wake the connection, guarding against a race with close()
+        write_pipe = self._connection._write_pipe
+        if write_pipe is None:
+            async_object.set_exception(ConnectionClosedError(
+                "Connection has been closed"))
+        try:
+            os.write(write_pipe, b'\0')
+        except:
+            async_object.set_exception(ConnectionClosedError(
+                "Connection has been closed"))
+
+    def start(self, timeout=15):
+        """Initiate connection to ZK.
+
+        :param timeout: Time in seconds to wait for connection to
+                        succeed.
+        :raises: :attr:`~kazoo.interfaces.IHandler.timeout_exception`
+                 if the connection wasn't established within `timeout`
+                 seconds.
+
+        """
+        event = self.start_async()
+        event.wait(timeout=timeout)
+        if not self.connected:
+            # We time-out, ensure we are disconnected
+            self.stop()
+            raise self.handler.timeout_exception("Connection time-out")
+
+        if self.chroot and not self.exists("/"):
+            warnings.warn("No chroot path exists, the chroot path "
+                          "should be created before normal use.")
+
+    def start_async(self):
+        """Asynchronously initiate connection to ZK.
+
+        :returns: An event object that can be checked to see if the
+                  connection is alive.
+        :rtype: :class:`~threading.Event` compatible object.
+
+        """
+        # If we're already connected, ignore
+        if self._live.is_set():
+            return self._live
+
+        # Make sure we're safely closed
+        self._safe_close()
+
+        # We've been asked to connect, clear the stop and our writer
+        # thread indicator
+        self._stopped.clear()
+        self._writer_stopped.clear()
+
+        # Start the handler
+        self.handler.start()
+
+        # Start the connection
+        self._connection.start()
+        return self._live
+
+    def stop(self):
+        """Gracefully stop this Zookeeper session.
+
+        This method can be called while a reconnection attempt is in
+        progress, which will then be halted.
+
+        Once the connection is closed, its session becomes invalid. All
+        the ephemeral nodes in the ZooKeeper server associated with the
+        session will be removed. The watches left on those nodes (and
+        on their parents) will be triggered.
+
+        """
+        if self._stopped.is_set():
+            return
+
+        self._stopped.set()
+        self._queue.append((CloseInstance, None))
+        os.write(self._connection._write_pipe, b'\0')
+        self._safe_close()
+
+    def restart(self):
+        """Stop and restart the Zookeeper session."""
+        self.stop()
+        self.start()
+
+    def close(self):
+        """Free any resources held by the client.
+
+        This method should be called on a stopped client before it is
+        discarded. Not doing so may result in filehandles being leaked.
+
+        .. versionadded:: 1.0
+        """
+        self._connection.close()
+
+    def command(self, cmd=b'ruok'):
+        """Sent a management command to the current ZK server.
+
+        Examples are `ruok`, `envi` or `stat`.
+
+        :returns: An unstructured textual response.
+        :rtype: str
+
+        :raises:
+            :exc:`ConnectionLoss` if there is no connection open, or
+            possibly a :exc:`socket.error` if there's a problem with
+            the connection used just for this command.
+
+        .. versionadded:: 0.5
+
+        """
+        if not self._live.is_set():
+            raise ConnectionLoss("No connection to server")
+
+        peer = self._connection._socket.getpeername()
+        sock = self.handler.create_connection(
+            peer, timeout=self._session_timeout / 1000.0)
+        sock.sendall(cmd)
+        result = sock.recv(8192)
+        sock.close()
+        return result.decode('utf-8', 'replace')
+
+    def server_version(self):
+        """Get the version of the currently connected ZK server.
+
+        :returns: The server version, for example (3, 4, 3).
+        :rtype: tuple
+
+        .. versionadded:: 0.5
+
+        """
+        data = self.command(b'envi')
+        string = ENVI_VERSION.match(data).group(1)
+        return tuple([int(i) for i in string.split('.')])
+
+    def add_auth(self, scheme, credential):
+        """Send credentials to server.
+
+        :param scheme: authentication scheme (default supported:
+                       "digest").
+        :param credential: the credential -- value depends on scheme.
+
+        :returns: True if it was successful.
+        :rtype: bool
+
+        :raises:
+            :exc:`~kazoo.exceptions.AuthFailedError` if it failed though
+            the session state will be set to AUTH_FAILED as well.
+
+        """
+        return self.add_auth_async(scheme, credential).get()
+
+    def add_auth_async(self, scheme, credential):
+        """Asynchronously send credentials to server. Takes the same
+        arguments as :meth:`add_auth`.
+
+        :rtype: :class:`~kazoo.interfaces.IAsyncResult`
+
+        """
+        if not isinstance(scheme, basestring):
+            raise TypeError("Invalid type for scheme")
+        if not isinstance(credential, basestring):
+            raise TypeError("Invalid type for credential")
+
+        # we need this auth data to re-authenticate on reconnect
+        self.auth_data.add((scheme, credential))
+
+        async_result = self.handler.async_result()
+        self._call(Auth(0, scheme, credential), async_result)
+        return async_result
+
+    def unchroot(self, path):
+        """Strip the chroot if applicable from the path."""
+        if not self.chroot:
+            return path
+
+        if path.startswith(self.chroot):
+            return path[len(self.chroot):]
+        else:
+            return path
+
+    def sync_async(self, path):
+        """Asynchronous sync.
+
+        :rtype: :class:`~kazoo.interfaces.IAsyncResult`
+
+        """
+        async_result = self.handler.async_result()
+        self._call(Sync(_prefix_root(self.chroot, path)), async_result)
+        return async_result
+
+    def sync(self, path):
+        """Sync, blocks until response is acknowledged.
+
+        Flushes channel between process and leader.
+
+        :param path: path of node.
+        :returns: The node path that was synced.
+        :raises:
+            :exc:`~kazoo.exceptions.ZookeeperError` if the server
+            returns a non-zero error code.
+
+        .. versionadded:: 0.5
+
+        """
+        return self.sync_async(path).get()
+
+    def create(self, path, value=b"", acl=None, ephemeral=False,
+               sequence=False, makepath=False):
+        """Create a node with the given value as its data. Optionally
+        set an ACL on the node.
+
+        The ephemeral and sequence arguments determine the type of the
+        node.
+
+        An ephemeral node will be automatically removed by ZooKeeper
+        when the session associated with the creation of the node
+        expires.
+
+        A sequential node will be given the specified path plus a
+        suffix `i` where i is the current sequential number of the
+        node. The sequence number is always fixed length of 10 digits,
+        0 padded. Once such a node is created, the sequential number
+        will be incremented by one.
+
+        If a node with the same actual path already exists in
+        ZooKeeper, a NodeExistsError will be raised. Note that since a
+        different actual path is used for each invocation of creating
+        sequential nodes with the same path argument, the call will
+        never raise NodeExistsError.
+
+        If the parent node does not exist in ZooKeeper, a NoNodeError
+        will be raised. Setting the optional `makepath` argument to
+        `True` will create all missing parent nodes instead.
+
+        An ephemeral node cannot have children. If the parent node of
+        the given path is ephemeral, a NoChildrenForEphemeralsError
+        will be raised.
+
+        This operation, if successful, will trigger all the watches
+        left on the node of the given path by :meth:`exists` and
+        :meth:`get` API calls, and the watches left on the parent node
+        by :meth:`get_children` API calls.
+
+        The maximum allowable size of the node value is 1 MB. Values
+        larger than this will cause a ZookeeperError to be raised.
+
+        :param path: Path of node.
+        :param value: Initial bytes value of node.
+        :param acl: :class:`~kazoo.security.ACL` list.
+        :param ephemeral: Boolean indicating whether node is ephemeral
+                          (tied to this session).
+        :param sequence: Boolean indicating whether path is suffixed
+                         with a unique index.
+        :param makepath: Whether the path should be created if it
+                         doesn't exist.
+        :returns: Real path of the new node.
+        :rtype: str
+
+        :raises:
+            :exc:`~kazoo.exceptions.NodeExistsError` if the node
+            already exists.
+
+            :exc:`~kazoo.exceptions.NoNodeError` if parent nodes are
+            missing.
+
+            :exc:`~kazoo.exceptions.NoChildrenForEphemeralsError` if
+            the parent node is an ephemeral node.
+
+            :exc:`~kazoo.exceptions.ZookeeperError` if the provided
+            value is too large.
+
+            :exc:`~kazoo.exceptions.ZookeeperError` if the server
+            returns a non-zero error code.
+
+        """
+        acl = acl or self.default_acl
+        return self.create_async(path, value, acl=acl, ephemeral=ephemeral,
+            sequence=sequence, makepath=makepath).get()
+
+    def create_async(self, path, value=b"", acl=None, ephemeral=False,
+                     sequence=False, makepath=False):
+        """Asynchronously create a ZNode. Takes the same arguments as
+        :meth:`create`.
+
+        :rtype: :class:`~kazoo.interfaces.IAsyncResult`
+
+        .. versionadded:: 1.1
+            The makepath option.
+
+        """
+        if acl is None and self.default_acl:
+            acl = self.default_acl
+
+        if not isinstance(path, basestring):
+            raise TypeError("path must be a string")
+        if acl and (isinstance(acl, ACL) or
+                    not isinstance(acl, (tuple, list))):
+            raise TypeError("acl must be a tuple/list of ACL's")
+        if value is not None and not isinstance(value, bytes):
+            raise TypeError("value must be a byte string")
+        if not isinstance(ephemeral, bool):
+            raise TypeError("ephemeral must be a bool")
+        if not isinstance(sequence, bool):
+            raise TypeError("sequence must be a bool")
+        if not isinstance(makepath, bool):
+            raise TypeError("makepath must be a bool")
+
+        flags = 0
+        if ephemeral:
+            flags |= 1
+        if sequence:
+            flags |= 2
+        if acl is None:
+            acl = OPEN_ACL_UNSAFE
+
+        async_result = self.handler.async_result()
+
+        @capture_exceptions(async_result)
+        def do_create():
+            result = self._create_async_inner(path, value, acl, flags, trailing=sequence)
+            result.rawlink(create_completion)
+
+        @capture_exceptions(async_result)
+        def retry_completion(result):
+            result.get()
+            do_create()
+
+        @wrap(async_result)
+        def create_completion(result):
+            try:
+                return self.unchroot(result.get())
+            except NoNodeError:
+                if not makepath:
+                    raise
+                if sequence and path.endswith('/'):
+                    parent = path.rstrip('/')
+                else:
+                    parent, _ = split(path)
+                self.ensure_path_async(parent, acl).rawlink(retry_completion)
+
+        do_create()
+        return async_result
+
+    def _create_async_inner(self, path, value, acl, flags, trailing=False):
+        async_result = self.handler.async_result()
+        call_result = self._call(
+            Create(_prefix_root(self.chroot, path, trailing=trailing),
+                   value, acl, flags), async_result)
+        if call_result is False:
+            # We hit a short-circuit exit on the _call. Because we are
+            # not using the original async_result here, we bubble the
+            # exception upwards to the do_create function in
+            # KazooClient.create so that it gets set on the correct
+            # async_result object
+            raise async_result.exception
+        return async_result
+
+    def ensure_path(self, path, acl=None):
+        """Recursively create a path if it doesn't exist.
+
+        :param path: Path of node.
+        :param acl: Permissions for node.
+
+        """
+        return self.ensure_path_async(path, acl).get()
+
+    def ensure_path_async(self, path, acl=None):
+        """Recursively create a path asynchronously if it doesn't
+        exist. Takes the same arguments as :meth:`ensure_path`.
+
+        :rtype: :class:`~kazoo.interfaces.IAsyncResult`
+
+        .. versionadded:: 1.1
+
+        """
+        acl = acl or self.default_acl
+        async_result = self.handler.async_result()
+
+        @wrap(async_result)
+        def create_completion(result):
+            try:
+                return result.get()
+            except NodeExistsError:
+                return True
+
+        @capture_exceptions(async_result)
+        def prepare_completion(next_path, result):
+            result.get()
+            self.create_async(next_path, acl=acl).rawlink(create_completion)
+
+        @wrap(async_result)
+        def exists_completion(path, result):
+            if result.get():
+                return True
+            parent, node = split(path)
+            if node:
+                self.ensure_path_async(parent, acl=acl).rawlink(
+                    partial(prepare_completion, path))
+            else:
+                self.create_async(path, acl=acl).rawlink(create_completion)
+
+        self.exists_async(path).rawlink(partial(exists_completion, path))
+
+        return async_result
+
+    def exists(self, path, watch=None):
+        """Check if a node exists.
+
+        If a watch is provided, it will be left on the node with the
+        given path. The watch will be triggered by a successful
+        operation that creates/deletes the node or sets the data on the
+        node.
+
+        :param path: Path of node.
+        :param watch: Optional watch callback to set for future changes
+                      to this path.
+        :returns: ZnodeStat of the node if it exists, else None if the
+                  node does not exist.
+        :rtype: :class:`~kazoo.protocol.states.ZnodeStat` or `None`.
+
+        :raises:
+            :exc:`~kazoo.exceptions.ZookeeperError` if the server
+            returns a non-zero error code.
+
+        """
+        return self.exists_async(path, watch).get()
+
+    def exists_async(self, path, watch=None):
+        """Asynchronously check if a node exists. Takes the same
+        arguments as :meth:`exists`.
+
+        :rtype: :class:`~kazoo.interfaces.IAsyncResult`
+
+        """
+        if not isinstance(path, basestring):
+            raise TypeError("path must be a string")
+        if watch and not callable(watch):
+            raise TypeError("watch must be a callable")
+
+        async_result = self.handler.async_result()
+        self._call(Exists(_prefix_root(self.chroot, path), watch),
+                   async_result)
+        return async_result
+
+    def get(self, path, watch=None):
+        """Get the value of a node.
+
+        If a watch is provided, it will be left on the node with the
+        given path. The watch will be triggered by a successful
+        operation that sets data on the node, or deletes the node.
+
+        :param path: Path of node.
+        :param watch: Optional watch callback to set for future changes
+                      to this path.
+        :returns:
+            Tuple (value, :class:`~kazoo.protocol.states.ZnodeStat`) of
+            node.
+        :rtype: tuple
+
+        :raises:
+            :exc:`~kazoo.exceptions.NoNodeError` if the node doesn't
+            exist
+
+            :exc:`~kazoo.exceptions.ZookeeperError` if the server
+            returns a non-zero error code
+
+        """
+        return self.get_async(path, watch).get()
+
+    def get_async(self, path, watch=None):
+        """Asynchronously get the value of a node. Takes the same
+        arguments as :meth:`get`.
+
+        :rtype: :class:`~kazoo.interfaces.IAsyncResult`
+
+        """
+        if not isinstance(path, basestring):
+            raise TypeError("path must be a string")
+        if watch and not callable(watch):
+            raise TypeError("watch must be a callable")
+
+        async_result = self.handler.async_result()
+        self._call(GetData(_prefix_root(self.chroot, path), watch),
+                   async_result)
+        return async_result
+
+    def get_children(self, path, watch=None, include_data=False):
+        """Get a list of child nodes of a path.
+
+        If a watch is provided it will be left on the node with the
+        given path. The watch will be triggered by a successful
+        operation that deletes the node of the given path or
+        creates/deletes a child under the node.
+
+        The list of children returned is not sorted and no guarantee is
+        provided as to its natural or lexical order.
+
+        :param path: Path of node to list.
+        :param watch: Optional watch callback to set for future changes
+                      to this path.
+        :param include_data:
+            Include the :class:`~kazoo.protocol.states.ZnodeStat` of
+            the node in addition to the children. This option changes
+            the return value to be a tuple of (children, stat).
+
+        :returns: List of child node names, or tuple if `include_data`
+                  is `True`.
+        :rtype: list
+
+        :raises:
+            :exc:`~kazoo.exceptions.NoNodeError` if the node doesn't
+            exist.
+
+            :exc:`~kazoo.exceptions.ZookeeperError` if the server
+            returns a non-zero error code.
+
+        .. versionadded:: 0.5
+            The `include_data` option.
+
+        """
+        return self.get_children_async(path, watch, include_data).get()
+
+    def get_children_async(self, path, watch=None, include_data=False):
+        """Asynchronously get a list of child nodes of a path. Takes
+        the same arguments as :meth:`get_children`.
+
+        :rtype: :class:`~kazoo.interfaces.IAsyncResult`
+
+        """
+        if not isinstance(path, basestring):
+            raise TypeError("path must be a string")
+        if watch and not callable(watch):
+            raise TypeError("watch must be a callable")
+        if not isinstance(include_data, bool):
+            raise TypeError("include_data must be a bool")
+
+        async_result = self.handler.async_result()
+        if include_data:
+            req = GetChildren2(_prefix_root(self.chroot, path), watch)
+        else:
+            req = GetChildren(_prefix_root(self.chroot, path), watch)
+        self._call(req, async_result)
+        return async_result
+
+    def get_acls(self, path):
+        """Return the ACL and stat of the node of the given path.
+
+        :param path: Path of the node.
+        :returns: The ACL array of the given node and its
+            :class:`~kazoo.protocol.states.ZnodeStat`.
+        :rtype: tuple of (:class:`~kazoo.security.ACL` list,
+                :class:`~kazoo.protocol.states.ZnodeStat`)
+        :raises:
+            :exc:`~kazoo.exceptions.NoNodeError` if the node doesn't
+            exist.
+
+            :exc:`~kazoo.exceptions.ZookeeperError` if the server
+            returns a non-zero error code
+
+        .. versionadded:: 0.5
+
+        """
+        return self.get_acls_async(path).get()
+
+    def get_acls_async(self, path):
+        """Return the ACL and stat of the node of the given path. Takes
+        the same arguments as :meth:`get_acls`.
+
+        :rtype: :class:`~kazoo.interfaces.IAsyncResult`
+
+        """
+        if not isinstance(path, basestring):
+            raise TypeError("path must be a string")
+
+        async_result = self.handler.async_result()
+        self._call(GetACL(_prefix_root(self.chroot, path)), async_result)
+        return async_result
+
+    def set_acls(self, path, acls, version=-1):
+        """Set the ACL for the node of the given path.
+
+        Set the ACL for the node of the given path if such a node
+        exists and the given version matches the version of the node.
+
+        :param path: Path for the node.
+        :param acls: List of :class:`~kazoo.security.ACL` objects to
+                     set.
+        :param version: The expected node version that must match.
+        :returns: The stat of the node.
+        :raises:
+            :exc:`~kazoo.exceptions.BadVersionError` if version doesn't
+            match.
+
+            :exc:`~kazoo.exceptions.NoNodeError` if the node doesn't
+            exist.
+
+            :exc:`~kazoo.exceptions.InvalidACLError` if the ACL is
+            invalid.
+
+            :exc:`~kazoo.exceptions.ZookeeperError` if the server
+            returns a non-zero error code.
+
+        .. versionadded:: 0.5
+
+        """
+        return self.set_acls_async(path, acls, version).get()
+
+    def set_acls_async(self, path, acls, version=-1):
+        """Set the ACL for the node of the given path. Takes the same
+        arguments as :meth:`set_acls`.
+
+        :rtype: :class:`~kazoo.interfaces.IAsyncResult`
+
+        """
+        if not isinstance(path, basestring):
+            raise TypeError("path must be a string")
+        if isinstance(acls, ACL) or not isinstance(acls, (tuple, list)):
+            raise TypeError("acl must be a tuple/list of ACL's")
+        if not isinstance(version, int):
+            raise TypeError("version must be an int")
+
+        async_result = self.handler.async_result()
+        self._call(SetACL(_prefix_root(self.chroot, path), acls, version),
+                   async_result)
+        return async_result
+
+    def set(self, path, value, version=-1):
+        """Set the value of a node.
+
+        If the version of the node being updated is newer than the
+        supplied version (and the supplied version is not -1), a
+        BadVersionError will be raised.
+
+        This operation, if successful, will trigger all the watches on
+        the node of the given path left by :meth:`get` API calls.
+
+        The maximum allowable size of the value is 1 MB. Values larger
+        than this will cause a ZookeeperError to be raised.
+
+        :param path: Path of node.
+        :param value: New data value.
+        :param version: Version of node being updated, or -1.
+        :returns: Updated :class:`~kazoo.protocol.states.ZnodeStat` of
+                  the node.
+
+        :raises:
+            :exc:`~kazoo.exceptions.BadVersionError` if version doesn't
+            match.
+
+            :exc:`~kazoo.exceptions.NoNodeError` if the node doesn't
+            exist.
+
+            :exc:`~kazoo.exceptions.ZookeeperError` if the provided
+            value is too large.
+
+            :exc:`~kazoo.exceptions.ZookeeperError` if the server
+            returns a non-zero error code.
+
+        """
+        return self.set_async(path, value, version).get()
+
+    def set_async(self, path, value, version=-1):
+        """Set the value of a node. Takes the same arguments as
+        :meth:`set`.
+
+        :rtype: :class:`~kazoo.interfaces.IAsyncResult`
+
+        """
+        if not isinstance(path, basestring):
+            raise TypeError("path must be a string")
+        if value is not None and not isinstance(value, bytes):
+            raise TypeError("value must be a byte string")
+        if not isinstance(version, int):
+            raise TypeError("version must be an int")
+
+        async_result = self.handler.async_result()
+        self._call(SetData(_prefix_root(self.chroot, path), value, version),
+                   async_result)
+        return async_result
+
+    def transaction(self):
+        """Create and return a :class:`TransactionRequest` object
+
+        Creates a :class:`TransactionRequest` object. A Transaction can
+        consist of multiple operations which can be committed as a
+        single atomic unit. Either all of the operations will succeed
+        or none of them.
+
+        :returns: A TransactionRequest.
+        :rtype: :class:`TransactionRequest`
+
+        .. versionadded:: 0.6
+            Requires Zookeeper 3.4+
+
+        """
+        return TransactionRequest(self)
+
+    def delete(self, path, version=-1, recursive=False):
+        """Delete a node.
+
+        The call will succeed if such a node exists, and the given
+        version matches the node's version (if the given version is -1,
+        the default, it matches any node's versions).
+
+        This operation, if successful, will trigger all the watches on
+        the node of the given path left by `exists` API calls, and the
+        watches on the parent node left by `get_children` API calls.
+
+        :param path: Path of node to delete.
+        :param version: Version of node to delete, or -1 for any.
+        :param recursive: Recursively delete node and all its children,
+                          defaults to False.
+        :type recursive: bool
+
+        :raises:
+            :exc:`~kazoo.exceptions.BadVersionError` if version doesn't
+            match.
+
+            :exc:`~kazoo.exceptions.NoNodeError` if the node doesn't
+            exist.
+
+            :exc:`~kazoo.exceptions.NotEmptyError` if the node has
+            children.
+
+            :exc:`~kazoo.exceptions.ZookeeperError` if the server
+            returns a non-zero error code.
+
+        """
+        if not isinstance(recursive, bool):
+            raise TypeError("recursive must be a bool")
+
+        if recursive:
+            return self._delete_recursive(path)
+        else:
+            return self.delete_async(path, version).get()
+
+    def delete_async(self, path, version=-1):
+        """Asynchronously delete a node. Takes the same arguments as
+        :meth:`delete`, with the exception of `recursive`.
+
+        :rtype: :class:`~kazoo.interfaces.IAsyncResult`
+
+        """
+        if not isinstance(path, basestring):
+            raise TypeError("path must be a string")
+        if not isinstance(version, int):
+            raise TypeError("version must be an int")
+        async_result = self.handler.async_result()
+        self._call(Delete(_prefix_root(self.chroot, path), version),
+                   async_result)
+        return async_result
+
+    def _delete_recursive(self, path):
+        try:
+            children = self.get_children(path)
+        except NoNodeError:
+            return True
+
+        if children:
+            for child in children:
+                if path == "/":
+                    child_path = path + child
+                else:
+                    child_path = path + "/" + child
+
+                self._delete_recursive(child_path)
+        try:
+            self.delete(path)
+        except NoNodeError:  # pragma: nocover
+            pass
+
+
+class TransactionRequest(object):
+    """A Zookeeper Transaction Request
+
+    A Transaction provides a builder object that can be used to
+    construct and commit an atomic set of operations. The transaction
+    must be committed before its sent.
+
+    Transactions are not thread-safe and should not be accessed from
+    multiple threads at once.
+
+    .. versionadded:: 0.6
+        Requires Zookeeper 3.4+
+
+    """
+    def __init__(self, client):
+        self.client = client
+        self.operations = []
+        self.committed = False
+
+    def create(self, path, value=b"", acl=None, ephemeral=False,
+               sequence=False):
+        """Add a create ZNode to the transaction. Takes the same
+        arguments as :meth:`KazooClient.create`, with the exception
+        of `makepath`.
+
+        :returns: None
+
+        """
+        if acl is None and self.client.default_acl:
+            acl = self.client.default_acl
+
+        if not isinstance(path, basestring):
+            raise TypeError("path must be a string")
+        if acl and not isinstance(acl, (tuple, list)):
+            raise TypeError("acl must be a tuple/list of ACL's")
+        if not isinstance(value, bytes):
+            raise TypeError("value must be a byte string")
+        if not isinstance(ephemeral, bool):
+            raise TypeError("ephemeral must be a bool")
+        if not isinstance(sequence, bool):
+            raise TypeError("sequence must be a bool")
+
+        flags = 0
+        if ephemeral:
+            flags |= 1
+        if sequence:
+            flags |= 2
+        if acl is None:
+            acl = OPEN_ACL_UNSAFE
+
+        self._add(Create(_prefix_root(self.client.chroot, path), value, acl,
+                         flags), None)
+
+    def delete(self, path, version=-1):
+        """Add a delete ZNode to the transaction. Takes the same
+        arguments as :meth:`KazooClient.delete`, with the exception of
+        `recursive`.
+
+        """
+        if not isinstance(path, basestring):
+            raise TypeError("path must be a string")
+        if not isinstance(version, int):
+            raise TypeError("version must be an int")
+        self._add(Delete(_prefix_root(self.client.chroot, path), version))
+
+    def set_data(self, path, value, version=-1):
+        """Add a set ZNode value to the transaction. Takes the same
+        arguments as :meth:`KazooClient.set`.
+
+        """
+        if not isinstance(path, basestring):
+            raise TypeError("path must be a string")
+        if not isinstance(value, bytes):
+            raise TypeError("value must be a byte string")
+        if not isinstance(version, int):
+            raise TypeError("version must be an int")
+        self._add(SetData(_prefix_root(self.client.chroot, path), value,
+                  version))
+
+    def check(self, path, version):
+        """Add a Check Version to the transaction.
+
+        This command will fail and abort a transaction if the path
+        does not match the specified version.
+
+        """
+        if not isinstance(path, basestring):
+            raise TypeError("path must be a string")
+        if not isinstance(version, int):
+            raise TypeError("version must be an int")
+        self._add(CheckVersion(_prefix_root(self.client.chroot, path),
+                  version))
+
+    def commit_async(self):
+        """Commit the transaction asynchronously.
+
+        :rtype: :class:`~kazoo.interfaces.IAsyncResult`
+
+        """
+        self._check_tx_state()
+        self.committed = True
+        async_object = self.client.handler.async_result()
+        self.client._call(Transaction(self.operations), async_object)
+        return async_object
+
+    def commit(self):
+        """Commit the transaction.
+
+        :returns: A list of the results for each operation in the
+                  transaction.
+
+        """
+        return self.commit_async().get()
+
+    def __enter__(self):
+        return self
+
+    def __exit__(self, exc_type, exc_value, exc_tb):
+        """Commit and cleanup accumulated transaction data."""
+        if not exc_type:
+            self.commit()
+
+    def _check_tx_state(self):
+        if self.committed:
+            raise ValueError('Transaction already committed')
+
+    def _add(self, request, post_processor=None):
+        self._check_tx_state()
+        self.client.logger.log(BLATHER, 'Added %r to %r', request, self)
+        self.operations.append(request)

+ 199 - 0
desktop/core/ext-py/kazoo-2.0/kazoo/exceptions.py

@@ -0,0 +1,199 @@
+"""Kazoo Exceptions"""
+from collections import defaultdict
+
+
+class KazooException(Exception):
+    """Base Kazoo exception that all other kazoo library exceptions
+    inherit from"""
+
+
+class ZookeeperError(KazooException):
+    """Base Zookeeper exception for errors originating from the
+    Zookeeper server"""
+
+
+class CancelledError(KazooException):
+    """Raised when a process is cancelled by another thread"""
+
+
+class ConfigurationError(KazooException):
+    """Raised if the configuration arguments to an object are
+    invalid"""
+
+
+class ZookeeperStoppedError(KazooException):
+    """Raised when the kazoo client stopped (and thus not connected)"""
+
+
+class ConnectionDropped(KazooException):
+    """Internal error for jumping out of loops"""
+
+
+class LockTimeout(KazooException):
+    """Raised if failed to acquire a lock.
+
+    .. versionadded:: 1.1
+    """
+
+
+class WriterNotClosedException(KazooException):
+    """Raised if the writer is unable to stop closing when requested.
+
+    .. versionadded:: 1.2
+    """
+
+
+def _invalid_error_code():
+    raise RuntimeError('Invalid error code')
+
+
+EXCEPTIONS = defaultdict(_invalid_error_code)
+
+
+def _zookeeper_exception(code):
+    def decorator(klass):
+        def create(*args, **kwargs):
+            return klass(args, kwargs)
+
+        EXCEPTIONS[code] = create
+        klass.code = code
+        return klass
+
+    return decorator
+
+
+@_zookeeper_exception(0)
+class RolledBackError(ZookeeperError):
+    pass
+
+
+@_zookeeper_exception(-1)
+class SystemZookeeperError(ZookeeperError):
+    pass
+
+
+@_zookeeper_exception(-2)
+class RuntimeInconsistency(ZookeeperError):
+    pass
+
+
+@_zookeeper_exception(-3)
+class DataInconsistency(ZookeeperError):
+    pass
+
+
+@_zookeeper_exception(-4)
+class ConnectionLoss(ZookeeperError):
+    pass
+
+
+@_zookeeper_exception(-5)
+class MarshallingError(ZookeeperError):
+    pass
+
+
+@_zookeeper_exception(-6)
+class UnimplementedError(ZookeeperError):
+    pass
+
+
+@_zookeeper_exception(-7)
+class OperationTimeoutError(ZookeeperError):
+    pass
+
+
+@_zookeeper_exception(-8)
+class BadArgumentsError(ZookeeperError):
+    pass
+
+
+@_zookeeper_exception(-100)
+class APIError(ZookeeperError):
+    pass
+
+
+@_zookeeper_exception(-101)
+class NoNodeError(ZookeeperError):
+    pass
+
+
+@_zookeeper_exception(-102)
+class NoAuthError(ZookeeperError):
+    pass
+
+
+@_zookeeper_exception(-103)
+class BadVersionError(ZookeeperError):
+    pass
+
+
+@_zookeeper_exception(-108)
+class NoChildrenForEphemeralsError(ZookeeperError):
+    pass
+
+
+@_zookeeper_exception(-110)
+class NodeExistsError(ZookeeperError):
+    pass
+
+
+@_zookeeper_exception(-111)
+class NotEmptyError(ZookeeperError):
+    pass
+
+
+@_zookeeper_exception(-112)
+class SessionExpiredError(ZookeeperError):
+    pass
+
+
+@_zookeeper_exception(-113)
+class InvalidCallbackError(ZookeeperError):
+    pass
+
+
+@_zookeeper_exception(-114)
+class InvalidACLError(ZookeeperError):
+    pass
+
+
+@_zookeeper_exception(-115)
+class AuthFailedError(ZookeeperError):
+    pass
+
+
+@_zookeeper_exception(-118)
+class SessionMovedError(ZookeeperError):
+    pass
+
+
+@_zookeeper_exception(-119)
+class NotReadOnlyCallError(ZookeeperError):
+    """An API call that is not read-only was used while connected to
+    a read-only server"""
+
+
+class ConnectionClosedError(SessionExpiredError):
+    """Connection is closed"""
+
+
+# BW Compat aliases for C lib style exceptions
+ConnectionLossException = ConnectionLoss
+MarshallingErrorException = MarshallingError
+SystemErrorException = SystemZookeeperError
+RuntimeInconsistencyException = RuntimeInconsistency
+DataInconsistencyException = DataInconsistency
+UnimplementedException = UnimplementedError
+OperationTimeoutException = OperationTimeoutError
+BadArgumentsException = BadArgumentsError
+ApiErrorException = APIError
+NoNodeException = NoNodeError
+NoAuthException = NoAuthError
+BadVersionException = BadVersionError
+NoChildrenForEphemeralsException = NoChildrenForEphemeralsError
+NodeExistsException = NodeExistsError
+InvalidACLException = InvalidACLError
+AuthFailedException = AuthFailedError
+NotEmptyException = NotEmptyError
+SessionExpiredException = SessionExpiredError
+InvalidCallbackException = InvalidCallbackError

+ 1 - 0
desktop/core/ext-py/kazoo-2.0/kazoo/handlers/__init__.py

@@ -0,0 +1 @@
+#

+ 161 - 0
desktop/core/ext-py/kazoo-2.0/kazoo/handlers/gevent.py

@@ -0,0 +1,161 @@
+"""A gevent based handler."""
+from __future__ import absolute_import
+
+import atexit
+import logging
+
+import gevent
+import gevent.event
+import gevent.queue
+import gevent.select
+import gevent.thread
+
+from gevent.queue import Empty
+from gevent.queue import Queue
+from gevent import socket
+try:
+    from gevent.lock import Semaphore, RLock
+except ImportError:
+    from gevent.coros import Semaphore, RLock
+
+from kazoo.handlers.utils import create_tcp_socket, create_tcp_connection
+
+_using_libevent = gevent.__version__.startswith('0.')
+
+log = logging.getLogger(__name__)
+
+_STOP = object()
+
+AsyncResult = gevent.event.AsyncResult
+
+
+class SequentialGeventHandler(object):
+    """Gevent handler for sequentially executing callbacks.
+
+    This handler executes callbacks in a sequential manner. A queue is
+    created for each of the callback events, so that each type of event
+    has its callback type run sequentially.
+
+    Each queue type has a greenlet worker that pulls the callback event
+    off the queue and runs it in the order the client sees it.
+
+    This split helps ensure that watch callbacks won't block session
+    re-establishment should the connection be lost during a Zookeeper
+    client call.
+
+    Watch callbacks should avoid blocking behavior as the next callback
+    of that type won't be run until it completes. If you need to block,
+    spawn a new greenlet and return immediately so callbacks can
+    proceed.
+
+    """
+    name = "sequential_gevent_handler"
+    sleep_func = staticmethod(gevent.sleep)
+
+    def __init__(self):
+        """Create a :class:`SequentialGeventHandler` instance"""
+        self.callback_queue = Queue()
+        self._running = False
+        self._async = None
+        self._state_change = Semaphore()
+        self._workers = []
+
+    class timeout_exception(gevent.event.Timeout):
+        def __init__(self, msg):
+            gevent.event.Timeout.__init__(self, exception=msg)
+
+    def _create_greenlet_worker(self, queue):
+        def greenlet_worker():
+            while True:
+                try:
+                    func = queue.get()
+                    if func is _STOP:
+                        break
+                    func()
+                except Empty:
+                    continue
+                except Exception as exc:
+                    log.warning("Exception in worker greenlet")
+                    log.exception(exc)
+        return gevent.spawn(greenlet_worker)
+
+    def start(self):
+        """Start the greenlet workers."""
+        with self._state_change:
+            if self._running:
+                return
+
+            self._running = True
+
+            # Spawn our worker greenlets, we have
+            # - A callback worker for watch events to be called
+            for queue in (self.callback_queue,):
+                w = self._create_greenlet_worker(queue)
+                self._workers.append(w)
+            atexit.register(self.stop)
+
+    def stop(self):
+        """Stop the greenlet workers and empty all queues."""
+        with self._state_change:
+            if not self._running:
+                return
+
+            self._running = False
+
+            for queue in (self.callback_queue,):
+                queue.put(_STOP)
+
+            while self._workers:
+                worker = self._workers.pop()
+                worker.join()
+
+            # Clear the queues
+            self.callback_queue = Queue()  # pragma: nocover
+
+            if hasattr(atexit, "unregister"):
+                atexit.unregister(self.stop)
+
+    def select(self, *args, **kwargs):
+        return gevent.select.select(*args, **kwargs)
+
+    def socket(self, *args, **kwargs):
+        return create_tcp_socket(socket)
+
+    def create_connection(self, *args, **kwargs):
+        return create_tcp_connection(socket, *args, **kwargs)
+
+    def event_object(self):
+        """Create an appropriate Event object"""
+        return gevent.event.Event()
+
+    def lock_object(self):
+        """Create an appropriate Lock object"""
+        return gevent.thread.allocate_lock()
+
+    def rlock_object(self):
+        """Create an appropriate RLock object"""
+        return RLock()
+
+    def async_result(self):
+        """Create a :class:`AsyncResult` instance
+
+        The :class:`AsyncResult` instance will have its completion
+        callbacks executed in the thread the
+        :class:`SequentialGeventHandler` is created in (which should be
+        the gevent/main thread).
+
+        """
+        return AsyncResult()
+
+    def spawn(self, func, *args, **kwargs):
+        """Spawn a function to run asynchronously"""
+        return gevent.spawn(func, *args, **kwargs)
+
+    def dispatch_callback(self, callback):
+        """Dispatch to the callback object
+
+        The callback is put on separate queues to run depending on the
+        type as documented for the :class:`SequentialGeventHandler`.
+
+        """
+        self.callback_queue.put(lambda: callback.func(*callback.args))

+ 287 - 0
desktop/core/ext-py/kazoo-2.0/kazoo/handlers/threading.py

@@ -0,0 +1,287 @@
+"""A threading based handler.
+
+The :class:`SequentialThreadingHandler` is intended for regular Python
+environments that use threads.
+
+.. warning::
+
+    Do not use :class:`SequentialThreadingHandler` with applications
+    using asynchronous event loops (like gevent). Use the
+    :class:`~kazoo.handlers.gevent.SequentialGeventHandler` instead.
+
+"""
+from __future__ import absolute_import
+
+import atexit
+import logging
+import select
+import socket
+import threading
+import time
+
+try:
+    import Queue
+except ImportError:  # pragma: nocover
+    import queue as Queue
+
+from kazoo.handlers.utils import create_tcp_socket, create_tcp_connection
+
+# sentinel objects
+_NONE = object()
+_STOP = object()
+
+log = logging.getLogger(__name__)
+
+
+class TimeoutError(Exception):
+    pass
+
+
+class AsyncResult(object):
+    """A one-time event that stores a value or an exception"""
+    def __init__(self, handler):
+        self._handler = handler
+        self.value = None
+        self._exception = _NONE
+        self._condition = threading.Condition()
+        self._callbacks = []
+
+    def ready(self):
+        """Return true if and only if it holds a value or an
+        exception"""
+        return self._exception is not _NONE
+
+    def successful(self):
+        """Return true if and only if it is ready and holds a value"""
+        return self._exception is None
+
+    @property
+    def exception(self):
+        if self._exception is not _NONE:
+            return self._exception
+
+    def set(self, value=None):
+        """Store the value. Wake up the waiters."""
+        with self._condition:
+            self.value = value
+            self._exception = None
+
+            for callback in self._callbacks:
+                self._handler.completion_queue.put(
+                    lambda: callback(self)
+                )
+            self._condition.notify_all()
+
+    def set_exception(self, exception):
+        """Store the exception. Wake up the waiters."""
+        with self._condition:
+            self._exception = exception
+
+            for callback in self._callbacks:
+                self._handler.completion_queue.put(
+                    lambda: callback(self)
+                )
+            self._condition.notify_all()
+
+    def get(self, block=True, timeout=None):
+        """Return the stored value or raise the exception.
+
+        If there is no value raises TimeoutError.
+
+        """
+        with self._condition:
+            if self._exception is not _NONE:
+                if self._exception is None:
+                    return self.value
+                raise self._exception
+            elif block:
+                self._condition.wait(timeout)
+                if self._exception is not _NONE:
+                    if self._exception is None:
+                        return self.value
+                    raise self._exception
+
+            # if we get to this point we timeout
+            raise TimeoutError()
+
+    def get_nowait(self):
+        """Return the value or raise the exception without blocking.
+
+        If nothing is available, raises TimeoutError
+
+        """
+        return self.get(block=False)
+
+    def wait(self, timeout=None):
+        """Block until the instance is ready."""
+        with self._condition:
+            self._condition.wait(timeout)
+        return self._exception is not _NONE
+
+    def rawlink(self, callback):
+        """Register a callback to call when a value or an exception is
+        set"""
+        with self._condition:
+            # Are we already set? Dispatch it now
+            if self.ready():
+                self._handler.completion_queue.put(
+                    lambda: callback(self)
+                )
+                return
+
+            if callback not in self._callbacks:
+                self._callbacks.append(callback)
+
+    def unlink(self, callback):
+        """Remove the callback set by :meth:`rawlink`"""
+        with self._condition:
+            if self.ready():
+                # Already triggered, ignore
+                return
+
+            if callback in self._callbacks:
+                self._callbacks.remove(callback)
+
+
+class SequentialThreadingHandler(object):
+    """Threading handler for sequentially executing callbacks.
+
+    This handler executes callbacks in a sequential manner. A queue is
+    created for each of the callback events, so that each type of event
+    has its callback type run sequentially. These are split into two
+    queues, one for watch events and one for async result completion
+    callbacks.
+
+    Each queue type has a thread worker that pulls the callback event
+    off the queue and runs it in the order the client sees it.
+
+    This split helps ensure that watch callbacks won't block session
+    re-establishment should the connection be lost during a Zookeeper
+    client call.
+
+    Watch and completion callbacks should avoid blocking behavior as
+    the next callback of that type won't be run until it completes. If
+    you need to block, spawn a new thread and return immediately so
+    callbacks can proceed.
+
+    .. note::
+
+        Completion callbacks can block to wait on Zookeeper calls, but
+        no other completion callbacks will execute until the callback
+        returns.
+
+    """
+    name = "sequential_threading_handler"
+    timeout_exception = TimeoutError
+    sleep_func = staticmethod(time.sleep)
+    queue_impl = Queue.Queue
+    queue_empty = Queue.Empty
+
+    def __init__(self):
+        """Create a :class:`SequentialThreadingHandler` instance"""
+        self.callback_queue = self.queue_impl()
+        self.completion_queue = self.queue_impl()
+        self._running = False
+        self._state_change = threading.Lock()
+        self._workers = []
+
+    def _create_thread_worker(self, queue):
+        def thread_worker():  # pragma: nocover
+            while True:
+                try:
+                    func = queue.get()
+                    try:
+                        if func is _STOP:
+                            break
+                        func()
+                    except Exception:
+                        log.exception("Exception in worker queue thread")
+                    finally:
+                        queue.task_done()
+                except self.queue_empty:
+                    continue
+        t = threading.Thread(target=thread_worker)
+
+        # Even though these should be joined, it's possible stop might
+        # not issue in time so we set them to daemon to let the program
+        # exit anyways
+        t.daemon = True
+        t.start()
+        return t
+
+    def start(self):
+        """Start the worker threads."""
+        with self._state_change:
+            if self._running:
+                return
+
+            # Spawn our worker threads, we have
+            # - A callback worker for watch events to be called
+            # - A completion worker for completion events to be called
+            for queue in (self.completion_queue, self.callback_queue):
+                w = self._create_thread_worker(queue)
+                self._workers.append(w)
+            self._running = True
+            atexit.register(self.stop)
+
+    def stop(self):
+        """Stop the worker threads and empty all queues."""
+        with self._state_change:
+            if not self._running:
+                return
+
+            self._running = False
+
+            for queue in (self.completion_queue, self.callback_queue):
+                queue.put(_STOP)
+
+            self._workers.reverse()
+            while self._workers:
+                worker = self._workers.pop()
+                worker.join()
+
+            # Clear the queues
+            self.callback_queue = self.queue_impl()
+            self.completion_queue = self.queue_impl()
+            if hasattr(atexit, "unregister"):
+                atexit.unregister(self.stop)
+
+    def select(self, *args, **kwargs):
+        return select.select(*args, **kwargs)
+
+    def socket(self):
+        return create_tcp_socket(socket)
+
+    def create_connection(self, *args, **kwargs):
+        return create_tcp_connection(socket, *args, **kwargs)
+
+    def event_object(self):
+        """Create an appropriate Event object"""
+        return threading.Event()
+
+    def lock_object(self):
+        """Create a lock object"""
+        return threading.Lock()
+
+    def rlock_object(self):
+        """Create an appropriate RLock object"""
+        return threading.RLock()
+
+    def async_result(self):
+        """Create a :class:`AsyncResult` instance"""
+        return AsyncResult(self)
+
+    def spawn(self, func, *args, **kwargs):
+        t = threading.Thread(target=func, args=args, kwargs=kwargs)
+        t.daemon = True
+        t.start()
+        return t
+
+    def dispatch_callback(self, callback):
+        """Dispatch to the callback object
+
+        The callback is put on separate queues to run depending on the
+        type as documented for the :class:`SequentialThreadingHandler`.
+
+        """
+        self.callback_queue.put(lambda: callback.func(*callback.args))

+ 93 - 0
desktop/core/ext-py/kazoo-2.0/kazoo/handlers/utils.py

@@ -0,0 +1,93 @@
+"""Kazoo handler helpers"""
+
+HAS_FNCTL = True
+try:
+    import fcntl
+except ImportError:  # pragma: nocover
+    HAS_FNCTL = False
+import functools
+import os
+
+
+def _set_fd_cloexec(fd):
+    flags = fcntl.fcntl(fd, fcntl.F_GETFD)
+    fcntl.fcntl(fd, fcntl.F_SETFD, flags | fcntl.FD_CLOEXEC)
+
+
+def _set_default_tcpsock_options(module, sock):
+    sock.setsockopt(module.IPPROTO_TCP, module.TCP_NODELAY, 1)
+    if HAS_FNCTL:
+        _set_fd_cloexec(sock)
+    return sock
+
+
+def create_pipe():
+    """Create a non-blocking read/write pipe.
+    """
+    r, w = os.pipe()
+    if HAS_FNCTL:
+        fcntl.fcntl(r, fcntl.F_SETFL, os.O_NONBLOCK)
+        fcntl.fcntl(w, fcntl.F_SETFL, os.O_NONBLOCK)
+        _set_fd_cloexec(r)
+        _set_fd_cloexec(w)
+    return r, w
+
+
+def create_tcp_socket(module):
+    """Create a TCP socket with the CLOEXEC flag set.
+    """
+    type_ = module.SOCK_STREAM
+    if hasattr(module, 'SOCK_CLOEXEC'):  # pragma: nocover
+        # if available, set cloexec flag during socket creation
+        type_ |= module.SOCK_CLOEXEC
+    sock = module.socket(module.AF_INET, type_)
+    _set_default_tcpsock_options(module, sock)
+    return sock
+
+
+def create_tcp_connection(module, address, timeout=None):
+    if timeout is None:
+        # thanks to create_connection() developers for
+        # this ugliness...
+        timeout = module._GLOBAL_DEFAULT_TIMEOUT
+
+    sock = module.create_connection(address, timeout)
+    _set_default_tcpsock_options(module, sock)
+    return sock
+
+
+def capture_exceptions(async_result):
+    """Return a new decorated function that propagates the exceptions of the
+    wrapped function to an async_result.
+
+    :param async_result: An async result implementing :class:`IAsyncResult`
+
+    """
+    def capture(function):
+        @functools.wraps(function)
+        def captured_function(*args, **kwargs):
+            try:
+                return function(*args, **kwargs)
+            except Exception as exc:
+                async_result.set_exception(exc)
+        return captured_function
+    return capture
+
+
+def wrap(async_result):
+    """Return a new decorated function that propagates the return value or
+    exception of wrapped function to an async_result.  NOTE: Only propagates a
+    non-None return value.
+
+    :param async_result: An async result implementing :class:`IAsyncResult`
+
+    """
+    def capture(function):
+        @capture_exceptions(async_result)
+        def captured_function(*args, **kwargs):
+            value = function(*args, **kwargs)
+            if value is not None:
+                async_result.set(value)
+            return value
+        return captured_function
+    return capture

+ 26 - 0
desktop/core/ext-py/kazoo-2.0/kazoo/hosts.py

@@ -0,0 +1,26 @@
+import random
+
+try:
+    from urlparse import urlsplit
+except ImportError:
+    # try python3 then
+    from urllib.parse import urlsplit
+
+def collect_hosts(hosts, randomize=True):
+    """Collect a set of hosts and an optional chroot from a string."""
+    host_ports, chroot = hosts.partition("/")[::2]
+    chroot = "/" + chroot if chroot else None
+
+    result = []
+    for host_port in host_ports.split(","):
+        # put all complexity of dealing with
+        # IPv4 & IPv6 address:port on the urlsplit
+        res = urlsplit("xxx://" + host_port)
+        host = res.hostname
+        port = int(res.port) if res.port else 2181
+        result.append((host.strip(), port))
+
+    if randomize:
+        random.shuffle(result)
+
+    return result, chroot

+ 203 - 0
desktop/core/ext-py/kazoo-2.0/kazoo/interfaces.py

@@ -0,0 +1,203 @@
+"""Kazoo Interfaces
+
+.. versionchanged:: 1.4
+
+    The classes in this module used to be interface declarations based on
+    `zope.interface.Interface`. They were converted to normal classes and
+    now serve as documentation only.
+
+"""
+
+# public API
+
+
+class IHandler(object):
+    """A Callback Handler for Zookeeper completion and watch callbacks.
+
+    This object must implement several methods responsible for
+    determining how completion / watch callbacks are handled as well as
+    the method for calling :class:`IAsyncResult` callback functions.
+
+    These functions are used to abstract differences between a Python
+    threading environment and asynchronous single-threaded environments
+    like gevent. The minimum functionality needed for Kazoo to handle
+    these differences is encompassed in this interface.
+
+    The Handler should document how callbacks are called for:
+
+    * Zookeeper completion events
+    * Zookeeper watch events
+
+    .. attribute:: name
+
+        Human readable name of the Handler interface.
+
+    .. attribute:: timeout_exception
+
+        Exception class that should be thrown and captured if a
+        result is not available within the given time.
+
+    .. attribute:: sleep_func
+
+        Appropriate sleep function that can be called with a single
+        argument and sleep.
+
+    """
+
+    def start(self):
+        """Start the handler, used for setting up the handler."""
+
+    def stop(self):
+        """Stop the handler. Should block until the handler is safely
+        stopped."""
+
+    def select(self):
+        """A select method that implements Python's select.select
+        API"""
+
+    def socket(self):
+        """A socket method that implements Python's socket.socket
+        API"""
+
+    def create_connection(self):
+        """A socket method that implements Python's
+        socket.create_connection API"""
+
+    def event_object(self):
+        """Return an appropriate object that implements Python's
+        threading.Event API"""
+
+    def lock_object(self):
+        """Return an appropriate object that implements Python's
+        threading.Lock API"""
+
+    def rlock_object(self):
+        """Return an appropriate object that implements Python's
+        threading.RLock API"""
+
+    def async_result(self):
+        """Return an instance that conforms to the
+        :class:`~IAsyncResult` interface appropriate for this
+        handler"""
+
+    def spawn(self, func, *args, **kwargs):
+        """Spawn a function to run asynchronously
+
+        :param args: args to call the function with.
+        :param kwargs: keyword args to call the function with.
+
+        This method should return immediately and execute the function
+        with the provided args and kwargs in an asynchronous manner.
+
+        """
+
+    def dispatch_callback(self, callback):
+        """Dispatch to the callback object
+
+        :param callback: A :class:`~kazoo.protocol.states.Callback`
+                         object to be called.
+
+        """
+
+
+class IAsyncResult(object):
+    """An Async Result object that can be queried for a value that has
+    been set asynchronously.
+
+    This object is modeled on the ``gevent`` AsyncResult object.
+
+    The implementation must account for the fact that the :meth:`set`
+    and :meth:`set_exception` methods will be called from within the
+    Zookeeper thread which may require extra care under asynchronous
+    environments.
+
+    .. attribute:: value
+
+        Holds the value passed to :meth:`set` if :meth:`set` was
+        called. Otherwise `None`.
+
+    .. attribute:: exception
+
+        Holds the exception instance passed to :meth:`set_exception`
+        if :meth:`set_exception` was called. Otherwise `None`.
+
+    """
+
+    def ready(self):
+        """Return `True` if and only if it holds a value or an
+        exception"""
+
+    def successful(self):
+        """Return `True` if and only if it is ready and holds a
+        value"""
+
+    def set(self, value=None):
+        """Store the value. Wake up the waiters.
+
+        :param value: Value to store as the result.
+
+        Any waiters blocking on :meth:`get` or :meth:`wait` are woken
+        up. Sequential calls to :meth:`wait` and :meth:`get` will not
+        block at all."""
+
+    def set_exception(self, exception):
+        """Store the exception. Wake up the waiters.
+
+        :param exception: Exception to raise when fetching the value.
+
+        Any waiters blocking on :meth:`get` or :meth:`wait` are woken
+        up. Sequential calls to :meth:`wait` and :meth:`get` will not
+        block at all."""
+
+    def get(self, block=True, timeout=None):
+        """Return the stored value or raise the exception
+
+        :param block: Whether this method should block or return
+                      immediately.
+        :type block: bool
+        :param timeout: How long to wait for a value when `block` is
+                        `True`.
+        :type timeout: float
+
+        If this instance already holds a value / an exception, return /
+        raise it immediately. Otherwise, block until :meth:`set` or
+        :meth:`set_exception` has been called or until the optional
+        timeout occurs."""
+
+    def get_nowait(self):
+        """Return the value or raise the exception without blocking.
+
+        If nothing is available, raise the Timeout exception class on
+        the associated :class:`IHandler` interface."""
+
+    def wait(self, timeout=None):
+        """Block until the instance is ready.
+
+        :param timeout: How long to wait for a value when `block` is
+                        `True`.
+        :type timeout: float
+
+        If this instance already holds a value / an exception, return /
+        raise it immediately. Otherwise, block until :meth:`set` or
+        :meth:`set_exception` has been called or until the optional
+        timeout occurs."""
+
+    def rawlink(self, callback):
+        """Register a callback to call when a value or an exception is
+        set
+
+        :param callback:
+            A callback function to call after :meth:`set` or
+            :meth:`set_exception` has been called. This function will
+            be passed a single argument, this instance.
+        :type callback: func
+
+        """
+
+    def unlink(self, callback):
+        """Remove the callback set by :meth:`rawlink`
+
+        :param callback: A callback function to remove.
+        :type callback: func
+
+        """

+ 2 - 0
desktop/core/ext-py/kazoo-2.0/kazoo/loggingsupport.py

@@ -0,0 +1,2 @@
+BLATHER = 5 # log level for low-level debugging
+

+ 1 - 0
desktop/core/ext-py/kazoo-2.0/kazoo/protocol/__init__.py

@@ -0,0 +1 @@
+#

+ 623 - 0
desktop/core/ext-py/kazoo-2.0/kazoo/protocol/connection.py

@@ -0,0 +1,623 @@
+"""Zookeeper Protocol Connection Handler"""
+import logging
+import os
+import random
+import select
+import socket
+import sys
+import time
+from binascii import hexlify
+from contextlib import contextmanager
+
+from kazoo.exceptions import (
+    AuthFailedError,
+    ConnectionDropped,
+    EXCEPTIONS,
+    SessionExpiredError,
+    NoNodeError
+)
+from kazoo.handlers.utils import create_pipe
+from kazoo.loggingsupport import BLATHER
+from kazoo.protocol.serialization import (
+    Auth,
+    Close,
+    Connect,
+    Exists,
+    GetChildren,
+    Ping,
+    PingInstance,
+    ReplyHeader,
+    Transaction,
+    Watch,
+    int_struct
+)
+from kazoo.protocol.states import (
+    Callback,
+    KeeperState,
+    WatchedEvent,
+    EVENT_TYPE_MAP,
+)
+from kazoo.retry import (
+    ForceRetryError,
+    RetryFailedError
+)
+
+log = logging.getLogger(__name__)
+
+
+# Special testing hook objects used to force a session expired error as
+# if it came from the server
+_SESSION_EXPIRED = object()
+_CONNECTION_DROP = object()
+
+STOP_CONNECTING = object()
+
+CREATED_EVENT = 1
+DELETED_EVENT = 2
+CHANGED_EVENT = 3
+CHILD_EVENT = 4
+
+WATCH_XID = -1
+PING_XID = -2
+AUTH_XID = -4
+
+CLOSE_RESPONSE = Close.type
+
+if sys.version_info > (3, ):  # pragma: nocover
+    def buffer(obj, offset=0):
+        return memoryview(obj)[offset:]
+
+    advance_iterator = next
+else:  # pragma: nocover
+    def advance_iterator(it):
+        return it.next()
+
+
+class RWPinger(object):
+    """A Read/Write Server Pinger Iterable
+
+    This object is initialized with the hosts iterator object and the
+    socket creation function. Anytime `next` is called on its iterator
+    it yields either False, or a host, port tuple if it found a r/w
+    capable Zookeeper node.
+
+    After the first run-through of hosts, an exponential back-off delay
+    is added before the next run. This delay is tracked internally and
+    the iterator will yield False if called too soon.
+
+    """
+    def __init__(self, hosts, connection_func, socket_handling):
+        self.hosts = hosts
+        self.connection = connection_func
+        self.last_attempt = None
+        self.socket_handling = socket_handling
+
+    def __iter__(self):
+        if not self.last_attempt:
+            self.last_attempt = time.time()
+        delay = 0.5
+        while True:
+            yield self._next_server(delay)
+
+    def _next_server(self, delay):
+        jitter = random.randint(0, 100) / 100.0
+        while time.time() < self.last_attempt + delay + jitter:
+            # Skip rw ping checks if its too soon
+            return False
+        for host, port in self.hosts:
+            log.debug("Pinging server for r/w: %s:%s", host, port)
+            self.last_attempt = time.time()
+            try:
+                with self.socket_handling():
+                    sock = self.connection((host, port))
+                    sock.sendall(b"isro")
+                    result = sock.recv(8192)
+                    sock.close()
+                    if result == b'rw':
+                        return (host, port)
+                    else:
+                        return False
+            except ConnectionDropped:
+                return False
+
+            # Add some jitter between host pings
+            while time.time() < self.last_attempt + jitter:
+                return False
+        delay *= 2
+
+
+class RWServerAvailable(Exception):
+    """Thrown if a RW Server becomes available"""
+
+
+class ConnectionHandler(object):
+    """Zookeeper connection handler"""
+    def __init__(self, client, retry_sleeper, logger=None):
+        self.client = client
+        self.handler = client.handler
+        self.retry_sleeper = retry_sleeper
+        self.logger = logger or log
+
+        # Our event objects
+        self.connection_closed = client.handler.event_object()
+        self.connection_closed.set()
+        self.connection_stopped = client.handler.event_object()
+        self.connection_stopped.set()
+        self.ping_outstanding = client.handler.event_object()
+
+        self._read_pipe = None
+        self._write_pipe = None
+
+        self._socket = None
+        self._xid = None
+        self._rw_server = None
+        self._ro_mode = False
+
+        self._connection_routine = None
+
+    # This is instance specific to avoid odd thread bug issues in Python
+    # during shutdown global cleanup
+    @contextmanager
+    def _socket_error_handling(self):
+        try:
+            yield
+        except (socket.error, select.error) as e:
+            err = getattr(e, 'strerror', e)
+            raise ConnectionDropped("socket connection error: %s" % (err,))
+
+    def start(self):
+        """Start the connection up"""
+        if self.connection_closed.is_set():
+            self._read_pipe, self._write_pipe = create_pipe()
+            self.connection_closed.clear()
+        if self._connection_routine:
+            raise Exception("Unable to start, connection routine already "
+                            "active.")
+        self._connection_routine = self.handler.spawn(self.zk_loop)
+
+    def stop(self, timeout=None):
+        """Ensure the writer has stopped, wait to see if it does."""
+        self.connection_stopped.wait(timeout)
+        if self._connection_routine:
+            self._connection_routine.join()
+            self._connection_routine = None
+        return self.connection_stopped.is_set()
+
+    def close(self):
+        """Release resources held by the connection
+
+        The connection can be restarted afterwards.
+        """
+        if not self.connection_stopped.is_set():
+            raise Exception("Cannot close connection until it is stopped")
+        self.connection_closed.set()
+        wp, rp = self._write_pipe, self._read_pipe
+        self._write_pipe = self._read_pipe = None
+        if wp is not None:
+            os.close(wp)
+        if rp is not None:
+            os.close(rp)
+
+    def _server_pinger(self):
+        """Returns a server pinger iterable, that will ping the next
+        server in the list, and apply a back-off between attempts."""
+        return RWPinger(self.client.hosts, self.handler.create_connection,
+                        self._socket_error_handling)
+
+    def _read_header(self, timeout):
+        b = self._read(4, timeout)
+        length = int_struct.unpack(b)[0]
+        b = self._read(length, timeout)
+        header, offset = ReplyHeader.deserialize(b, 0)
+        return header, b, offset
+
+    def _read(self, length, timeout):
+        msgparts = []
+        remaining = length
+        with self._socket_error_handling():
+            while remaining > 0:
+                s = self.handler.select([self._socket], [], [], timeout)[0]
+                if not s:  # pragma: nocover
+                    # If the read list is empty, we got a timeout. We don't
+                    # have to check wlist and xlist as we don't set any
+                    raise self.handler.timeout_exception("socket time-out")
+
+                chunk = self._socket.recv(remaining)
+                if chunk == b'':
+                    raise ConnectionDropped('socket connection broken')
+                msgparts.append(chunk)
+                remaining -= len(chunk)
+            return b"".join(msgparts)
+
+    def _invoke(self, timeout, request, xid=None):
+        """A special writer used during connection establishment
+        only"""
+        self._submit(request, timeout, xid)
+        zxid = None
+        if xid:
+            header, buffer, offset = self._read_header(timeout)
+            if header.xid != xid:
+                raise RuntimeError('xids do not match, expected %r received %r',
+                                   xid, header.xid)
+            if header.zxid > 0:
+                zxid = header.zxid
+            if header.err:
+                callback_exception = EXCEPTIONS[header.err]()
+                self.logger.debug(
+                    'Received error(xid=%s) %r', xid, callback_exception)
+                raise callback_exception
+            return zxid
+
+        msg = self._read(4, timeout)
+        length = int_struct.unpack(msg)[0]
+        msg = self._read(length, timeout)
+
+        if hasattr(request, 'deserialize'):
+            try:
+                obj, _ = request.deserialize(msg, 0)
+            except Exception:
+                self.logger.exception("Exception raised during deserialization"
+                                      " of request: %s", request)
+
+                # raise ConnectionDropped so connect loop will retry
+                raise ConnectionDropped('invalid server response')
+            self.logger.log(BLATHER, 'Read response %s', obj)
+            return obj, zxid
+
+        return zxid
+
+    def _submit(self, request, timeout, xid=None):
+        """Submit a request object with a timeout value and optional
+        xid"""
+        b = bytearray()
+        if xid:
+            b.extend(int_struct.pack(xid))
+        if request.type:
+            b.extend(int_struct.pack(request.type))
+        b += request.serialize()
+        self.logger.log((BLATHER if isinstance(request, Ping) else logging.DEBUG),
+                        "Sending request(xid=%s): %s", xid, request)
+        self._write(int_struct.pack(len(b)) + b, timeout)
+
+    def _write(self, msg, timeout):
+        """Write a raw msg to the socket"""
+        sent = 0
+        msg_length = len(msg)
+        with self._socket_error_handling():
+            while sent < msg_length:
+                s = self.handler.select([], [self._socket], [], timeout)[1]
+                if not s:  # pragma: nocover
+                    # If the write list is empty, we got a timeout. We don't
+                    # have to check rlist and xlist as we don't set any
+                    raise self.handler.timeout_exception("socket time-out")
+                msg_slice = buffer(msg, sent)
+                bytes_sent = self._socket.send(msg_slice)
+                if not bytes_sent:
+                    raise ConnectionDropped('socket connection broken')
+                sent += bytes_sent
+
+    def _read_watch_event(self, buffer, offset):
+        client = self.client
+        watch, offset = Watch.deserialize(buffer, offset)
+        path = watch.path
+
+        self.logger.debug('Received EVENT: %s', watch)
+
+        watchers = []
+
+        if watch.type in (CREATED_EVENT, CHANGED_EVENT):
+            watchers.extend(client._data_watchers.pop(path, []))
+        elif watch.type == DELETED_EVENT:
+            watchers.extend(client._data_watchers.pop(path, []))
+            watchers.extend(client._child_watchers.pop(path, []))
+        elif watch.type == CHILD_EVENT:
+            watchers.extend(client._child_watchers.pop(path, []))
+        else:
+            self.logger.warn('Received unknown event %r', watch.type)
+            return
+
+        # Strip the chroot if needed
+        path = client.unchroot(path)
+        ev = WatchedEvent(EVENT_TYPE_MAP[watch.type], client._state, path)
+
+        # Last check to ignore watches if we've been stopped
+        if client._stopped.is_set():
+            return
+
+        # Dump the watchers to the watch thread
+        for watch in watchers:
+            client.handler.dispatch_callback(Callback('watch', watch, (ev,)))
+
+    def _read_response(self, header, buffer, offset):
+        client = self.client
+        request, async_object, xid = client._pending.popleft()
+        if header.zxid and header.zxid > 0:
+            client.last_zxid = header.zxid
+        if header.xid != xid:
+            raise RuntimeError('xids do not match, expected %r '
+                               'received %r', xid, header.xid)
+
+        # Determine if its an exists request and a no node error
+        exists_error = (header.err == NoNodeError.code and
+                        request.type == Exists.type)
+
+        # Set the exception if its not an exists error
+        if header.err and not exists_error:
+            callback_exception = EXCEPTIONS[header.err]()
+            self.logger.debug(
+                'Received error(xid=%s) %r', xid, callback_exception)
+            if async_object:
+                async_object.set_exception(callback_exception)
+        elif request and async_object:
+            if exists_error:
+                # It's a NoNodeError, which is fine for an exists
+                # request
+                async_object.set(None)
+            else:
+                try:
+                    response = request.deserialize(buffer, offset)
+                except Exception as exc:
+                    self.logger.exception("Exception raised during deserialization"
+                                          " of request: %s", request)
+                    async_object.set_exception(exc)
+                    return
+                self.logger.debug(
+                    'Received response(xid=%s): %r', xid, response)
+
+                # We special case a Transaction as we have to unchroot things
+                if request.type == Transaction.type:
+                    response = Transaction.unchroot(client, response)
+
+                async_object.set(response)
+
+            # Determine if watchers should be registered
+            watcher = getattr(request, 'watcher', None)
+            if not client._stopped.is_set() and watcher:
+                if isinstance(request, GetChildren):
+                    client._child_watchers[request.path].add(watcher)
+                else:
+                    client._data_watchers[request.path].add(watcher)
+
+        if isinstance(request, Close):
+            self.logger.log(BLATHER, 'Read close response')
+            return CLOSE_RESPONSE
+
+    def _read_socket(self, read_timeout):
+        """Called when there's something to read on the socket"""
+        client = self.client
+
+        header, buffer, offset = self._read_header(read_timeout)
+        if header.xid == PING_XID:
+            self.logger.log(BLATHER, 'Received Ping')
+            self.ping_outstanding.clear()
+        elif header.xid == AUTH_XID:
+            self.logger.log(BLATHER, 'Received AUTH')
+
+            request, async_object, xid = client._pending.popleft()
+            if header.err:
+                async_object.set_exception(AuthFailedError())
+                client._session_callback(KeeperState.AUTH_FAILED)
+            else:
+                async_object.set(True)
+        elif header.xid == WATCH_XID:
+            self._read_watch_event(buffer, offset)
+        else:
+            self.logger.log(BLATHER, 'Reading for header %r', header)
+
+            return self._read_response(header, buffer, offset)
+
+    def _send_request(self, read_timeout, connect_timeout):
+        """Called when we have something to send out on the socket"""
+        client = self.client
+        try:
+            request, async_object = client._queue[0]
+        except IndexError:
+            # Not actually something on the queue, this can occur if
+            # something happens to cancel the request such that we
+            # don't clear the pipe below after sending
+            try:
+                # Clear possible inconsistence (no request in the queue
+                # but have data in the read pipe), which causes cpu to spin.
+                os.read(self._read_pipe, 1)
+            except OSError:
+                pass
+            return
+
+        # Special case for testing, if this is a _SessionExpire object
+        # then throw a SessionExpiration error as if we were dropped
+        if request is _SESSION_EXPIRED:
+            raise SessionExpiredError("Session expired: Testing")
+        if request is _CONNECTION_DROP:
+            raise ConnectionDropped("Connection dropped: Testing")
+
+        # Special case for auth packets
+        if request.type == Auth.type:
+            xid = AUTH_XID
+        else:
+            self._xid += 1
+            xid = self._xid
+
+        self._submit(request, connect_timeout, xid)
+        client._queue.popleft()
+        os.read(self._read_pipe, 1)
+        client._pending.append((request, async_object, xid))
+
+    def _send_ping(self, connect_timeout):
+        self.ping_outstanding.set()
+        self._submit(PingInstance, connect_timeout, PING_XID)
+
+        # Determine if we need to check for a r/w server
+        if self._ro_mode:
+            result = advance_iterator(self._ro_mode)
+            if result:
+                self._rw_server = result
+                raise RWServerAvailable()
+
+    def zk_loop(self):
+        """Main Zookeeper handling loop"""
+        self.logger.log(BLATHER, 'ZK loop started')
+
+        self.connection_stopped.clear()
+
+        retry = self.retry_sleeper.copy()
+        try:
+            while not self.client._stopped.is_set():
+                # If the connect_loop returns STOP_CONNECTING, stop retrying
+                if retry(self._connect_loop, retry) is STOP_CONNECTING:
+                    break
+        except RetryFailedError:
+            self.logger.warning("Failed connecting to Zookeeper "
+                                "within the connection retry policy.")
+        finally:
+            self.connection_stopped.set()
+            self.client._session_callback(KeeperState.CLOSED)
+            self.logger.log(BLATHER, 'Connection stopped')
+
+    def _connect_loop(self, retry):
+        # Iterate through the hosts a full cycle before starting over
+        status = None
+        for host, port in self.client.hosts:
+            if self.client._stopped.is_set():
+                status = STOP_CONNECTING
+                break
+            status = self._connect_attempt(host, port, retry)
+            if status is STOP_CONNECTING:
+                break
+
+        if status is STOP_CONNECTING:
+            return STOP_CONNECTING
+        else:
+            raise ForceRetryError('Reconnecting')
+
+    def _connect_attempt(self, host, port, retry):
+        client = self.client
+        TimeoutError = self.handler.timeout_exception
+        close_connection = False
+
+        self._socket = None
+
+        # Were we given a r/w server? If so, use that instead
+        if self._rw_server:
+            self.logger.log(BLATHER,
+                            "Found r/w server to use, %s:%s", host, port)
+            host, port = self._rw_server
+            self._rw_server = None
+
+        if client._state != KeeperState.CONNECTING:
+            client._session_callback(KeeperState.CONNECTING)
+
+        try:
+            read_timeout, connect_timeout = self._connect(host, port)
+            read_timeout = read_timeout / 1000.0
+            connect_timeout = connect_timeout / 1000.0
+            retry.reset()
+            self._xid = 0
+
+            while not close_connection:
+                # Watch for something to read or send
+                jitter_time = random.randint(0, 40) / 100.0
+                # Ensure our timeout is positive
+                timeout = max([read_timeout / 2.0 - jitter_time, jitter_time])
+                s = self.handler.select([self._socket, self._read_pipe],
+                                        [], [], timeout)[0]
+
+                if not s:
+                    if self.ping_outstanding.is_set():
+                        self.ping_outstanding.clear()
+                        raise ConnectionDropped(
+                            "outstanding heartbeat ping not received")
+                    self._send_ping(connect_timeout)
+                elif s[0] == self._socket:
+                    response = self._read_socket(read_timeout)
+                    close_connection = response == CLOSE_RESPONSE
+                else:
+                    self._send_request(read_timeout, connect_timeout)
+
+            self.logger.info('Closing connection to %s:%s', host, port)
+            client._session_callback(KeeperState.CLOSED)
+            return STOP_CONNECTING
+        except (ConnectionDropped, TimeoutError) as e:
+            if isinstance(e, ConnectionDropped):
+                self.logger.warning('Connection dropped: %s', e)
+            else:
+                self.logger.warning('Connection time-out')
+            if client._state != KeeperState.CONNECTING:
+                self.logger.warning("Transition to CONNECTING")
+                client._session_callback(KeeperState.CONNECTING)
+        except AuthFailedError:
+            retry.reset()
+            self.logger.warning('AUTH_FAILED closing')
+            client._session_callback(KeeperState.AUTH_FAILED)
+            return STOP_CONNECTING
+        except SessionExpiredError:
+            retry.reset()
+            self.logger.warning('Session has expired')
+            client._session_callback(KeeperState.EXPIRED_SESSION)
+        except RWServerAvailable:
+            retry.reset()
+            self.logger.warning('Found a RW server, dropping connection')
+            client._session_callback(KeeperState.CONNECTING)
+        except Exception:
+            self.logger.exception('Unhandled exception in connection loop')
+            raise
+        finally:
+            if self._socket is not None:
+                self._socket.close()
+
+    def _connect(self, host, port):
+        client = self.client
+        self.logger.info('Connecting to %s:%s', host, port)
+
+        self.logger.log(BLATHER,
+                          '    Using session_id: %r session_passwd: %s',
+                          client._session_id,
+                          hexlify(client._session_passwd))
+
+        with self._socket_error_handling():
+            self._socket = self.handler.create_connection(
+                (host, port), client._session_timeout / 1000.0)
+
+        self._socket.setblocking(0)
+
+        connect = Connect(0, client.last_zxid, client._session_timeout,
+                          client._session_id or 0, client._session_passwd,
+                          client.read_only)
+
+        connect_result, zxid = self._invoke(client._session_timeout, connect)
+
+        if connect_result.time_out <= 0:
+            raise SessionExpiredError("Session has expired")
+
+        if zxid:
+            client.last_zxid = zxid
+
+        # Load return values
+        client._session_id = connect_result.session_id
+        client._protocol_version = connect_result.protocol_version
+        negotiated_session_timeout = connect_result.time_out
+        connect_timeout = negotiated_session_timeout / len(client.hosts)
+        read_timeout = negotiated_session_timeout * 2.0 / 3.0
+        client._session_passwd = connect_result.passwd
+
+        self.logger.log(BLATHER,
+                          'Session created, session_id: %r session_passwd: %s\n'
+                          '    negotiated session timeout: %s\n'
+                          '    connect timeout: %s\n'
+                          '    read timeout: %s', client._session_id,
+                          hexlify(client._session_passwd),
+                          negotiated_session_timeout, connect_timeout,
+                          read_timeout)
+
+        if connect_result.read_only:
+            client._session_callback(KeeperState.CONNECTED_RO)
+            self._ro_mode = iter(self._server_pinger())
+        else:
+            client._session_callback(KeeperState.CONNECTED)
+            self._ro_mode = None
+
+        for scheme, auth in client.auth_data:
+            ap = Auth(0, scheme, auth)
+            zxid = self._invoke(connect_timeout, ap, xid=AUTH_XID)
+            if zxid:
+                client.last_zxid = zxid
+        return read_timeout, connect_timeout

+ 54 - 0
desktop/core/ext-py/kazoo-2.0/kazoo/protocol/paths.py

@@ -0,0 +1,54 @@
+def normpath(path, trailing=False):
+    """Normalize path, eliminating double slashes, etc."""
+    comps = path.split('/')
+    new_comps = []
+    for comp in comps:
+        if comp == '':
+            continue
+        if comp in ('.', '..'):
+            raise ValueError('relative paths not allowed')
+        new_comps.append(comp)
+    new_path = '/'.join(new_comps)
+    if trailing is True and path.endswith('/'):
+        new_path += '/'
+    if path.startswith('/'):
+        return '/' + new_path
+    return new_path
+
+
+def join(a, *p):
+    """Join two or more pathname components, inserting '/' as needed.
+
+    If any component is an absolute path, all previous path components
+    will be discarded.
+
+    """
+    path = a
+    for b in p:
+        if b.startswith('/'):
+            path = b
+        elif path == '' or path.endswith('/'):
+            path += b
+        else:
+            path += '/' + b
+    return path
+
+
+def isabs(s):
+    """Test whether a path is absolute"""
+    return s.startswith('/')
+
+
+def basename(p):
+    """Returns the final component of a pathname"""
+    i = p.rfind('/') + 1
+    return p[i:]
+
+
+def _prefix_root(root, path, trailing=False):
+    """Prepend a root to a path. """
+    return normpath(join(_norm_root(root), path.lstrip('/')), trailing=trailing)
+
+
+def _norm_root(root):
+    return normpath(join('/', root))

+ 396 - 0
desktop/core/ext-py/kazoo-2.0/kazoo/protocol/serialization.py

@@ -0,0 +1,396 @@
+"""Zookeeper Serializers, Deserializers, and NamedTuple objects"""
+from collections import namedtuple
+import struct
+
+from kazoo.exceptions import EXCEPTIONS
+from kazoo.protocol.states import ZnodeStat
+from kazoo.security import ACL
+from kazoo.security import Id
+
+# Struct objects with formats compiled
+bool_struct = struct.Struct('B')
+int_struct = struct.Struct('!i')
+int_int_struct = struct.Struct('!ii')
+int_int_long_struct = struct.Struct('!iiq')
+
+int_long_int_long_struct = struct.Struct('!iqiq')
+multiheader_struct = struct.Struct('!iBi')
+reply_header_struct = struct.Struct('!iqi')
+stat_struct = struct.Struct('!qqqqiiiqiiq')
+
+try:  # pragma: nocover
+    basestring
+except NameError:
+    basestring = str
+
+
+def read_string(buffer, offset):
+    """Reads an int specified buffer into a string and returns the
+    string and the new offset in the buffer"""
+    length = int_struct.unpack_from(buffer, offset)[0]
+    offset += int_struct.size
+    if length < 0:
+        return None, offset
+    else:
+        index = offset
+        offset += length
+        return buffer[index:index + length].decode('utf-8'), offset
+
+
+def read_acl(bytes, offset):
+    perms = int_struct.unpack_from(bytes, offset)[0]
+    offset += int_struct.size
+    scheme, offset = read_string(bytes, offset)
+    id, offset = read_string(bytes, offset)
+    return ACL(perms, Id(scheme, id)), offset
+
+
+def write_string(bytes):
+    if not bytes:
+        return int_struct.pack(-1)
+    else:
+        utf8_str = bytes.encode('utf-8')
+        return int_struct.pack(len(utf8_str)) + utf8_str
+
+
+def write_buffer(bytes):
+    if bytes is None:
+        return int_struct.pack(-1)
+    else:
+        return int_struct.pack(len(bytes)) + bytes
+
+
+def read_buffer(bytes, offset):
+    length = int_struct.unpack_from(bytes, offset)[0]
+    offset += int_struct.size
+    if length < 0:
+        return None, offset
+    else:
+        index = offset
+        offset += length
+        return bytes[index:index + length], offset
+
+
+class Close(namedtuple('Close', '')):
+    type = -11
+
+    @classmethod
+    def serialize(cls):
+        return b''
+
+CloseInstance = Close()
+
+
+class Ping(namedtuple('Ping', '')):
+    type = 11
+
+    @classmethod
+    def serialize(cls):
+        return b''
+
+PingInstance = Ping()
+
+
+class Connect(namedtuple('Connect', 'protocol_version last_zxid_seen'
+                         ' time_out session_id passwd read_only')):
+    type = None
+
+    def serialize(self):
+        b = bytearray()
+        b.extend(int_long_int_long_struct.pack(
+            self.protocol_version, self.last_zxid_seen, self.time_out,
+            self.session_id))
+        b.extend(write_buffer(self.passwd))
+        b.extend([1 if self.read_only else 0])
+        return b
+
+    @classmethod
+    def deserialize(cls, bytes, offset):
+        proto_version, timeout, session_id = int_int_long_struct.unpack_from(
+            bytes, offset)
+        offset += int_int_long_struct.size
+        password, offset = read_buffer(bytes, offset)
+
+        try:
+            read_only = bool_struct.unpack_from(bytes, offset)[0] is 1
+            offset += bool_struct.size
+        except struct.error:
+            read_only = False
+        return cls(proto_version, 0, timeout, session_id, password,
+                   read_only), offset
+
+
+class Create(namedtuple('Create', 'path data acl flags')):
+    type = 1
+
+    def serialize(self):
+        b = bytearray()
+        b.extend(write_string(self.path))
+        b.extend(write_buffer(self.data))
+        b.extend(int_struct.pack(len(self.acl)))
+        for acl in self.acl:
+            b.extend(int_struct.pack(acl.perms) +
+                     write_string(acl.id.scheme) + write_string(acl.id.id))
+        b.extend(int_struct.pack(self.flags))
+        return b
+
+    @classmethod
+    def deserialize(cls, bytes, offset):
+        return read_string(bytes, offset)[0]
+
+
+class Delete(namedtuple('Delete', 'path version')):
+    type = 2
+
+    def serialize(self):
+        b = bytearray()
+        b.extend(write_string(self.path))
+        b.extend(int_struct.pack(self.version))
+        return b
+
+    @classmethod
+    def deserialize(self, bytes, offset):
+        return True
+
+
+class Exists(namedtuple('Exists', 'path watcher')):
+    type = 3
+
+    def serialize(self):
+        b = bytearray()
+        b.extend(write_string(self.path))
+        b.extend([1 if self.watcher else 0])
+        return b
+
+    @classmethod
+    def deserialize(cls, bytes, offset):
+        stat = ZnodeStat._make(stat_struct.unpack_from(bytes, offset))
+        return stat if stat.czxid != -1 else None
+
+
+class GetData(namedtuple('GetData', 'path watcher')):
+    type = 4
+
+    def serialize(self):
+        b = bytearray()
+        b.extend(write_string(self.path))
+        b.extend([1 if self.watcher else 0])
+        return b
+
+    @classmethod
+    def deserialize(cls, bytes, offset):
+        data, offset = read_buffer(bytes, offset)
+        stat = ZnodeStat._make(stat_struct.unpack_from(bytes, offset))
+        return data, stat
+
+
+class SetData(namedtuple('SetData', 'path data version')):
+    type = 5
+
+    def serialize(self):
+        b = bytearray()
+        b.extend(write_string(self.path))
+        b.extend(write_buffer(self.data))
+        b.extend(int_struct.pack(self.version))
+        return b
+
+    @classmethod
+    def deserialize(cls, bytes, offset):
+        return ZnodeStat._make(stat_struct.unpack_from(bytes, offset))
+
+
+class GetACL(namedtuple('GetACL', 'path')):
+    type = 6
+
+    def serialize(self):
+        return bytearray(write_string(self.path))
+
+    @classmethod
+    def deserialize(cls, bytes, offset):
+        count = int_struct.unpack_from(bytes, offset)[0]
+        offset += int_struct.size
+        if count == -1:  # pragma: nocover
+            return []
+
+        acls = []
+        for c in range(count):
+            acl, offset = read_acl(bytes, offset)
+            acls.append(acl)
+        stat = ZnodeStat._make(stat_struct.unpack_from(bytes, offset))
+        return acls, stat
+
+
+class SetACL(namedtuple('SetACL', 'path acls version')):
+    type = 7
+
+    def serialize(self):
+        b = bytearray()
+        b.extend(write_string(self.path))
+        b.extend(int_struct.pack(len(self.acls)))
+        for acl in self.acls:
+            b.extend(int_struct.pack(acl.perms) +
+                     write_string(acl.id.scheme) + write_string(acl.id.id))
+        b.extend(int_struct.pack(self.version))
+        return b
+
+    @classmethod
+    def deserialize(cls, bytes, offset):
+        return ZnodeStat._make(stat_struct.unpack_from(bytes, offset))
+
+
+class GetChildren(namedtuple('GetChildren', 'path watcher')):
+    type = 8
+
+    def serialize(self):
+        b = bytearray()
+        b.extend(write_string(self.path))
+        b.extend([1 if self.watcher else 0])
+        return b
+
+    @classmethod
+    def deserialize(cls, bytes, offset):
+        count = int_struct.unpack_from(bytes, offset)[0]
+        offset += int_struct.size
+        if count == -1:  # pragma: nocover
+            return []
+
+        children = []
+        for c in range(count):
+            child, offset = read_string(bytes, offset)
+            children.append(child)
+        return children
+
+
+class Sync(namedtuple('Sync', 'path')):
+    type = 9
+
+    def serialize(self):
+        return write_string(self.path)
+
+    @classmethod
+    def deserialize(cls, buffer, offset):
+        return read_string(buffer, offset)[0]
+
+
+class GetChildren2(namedtuple('GetChildren2', 'path watcher')):
+    type = 12
+
+    def serialize(self):
+        b = bytearray()
+        b.extend(write_string(self.path))
+        b.extend([1 if self.watcher else 0])
+        return b
+
+    @classmethod
+    def deserialize(cls, bytes, offset):
+        count = int_struct.unpack_from(bytes, offset)[0]
+        offset += int_struct.size
+        if count == -1:  # pragma: nocover
+            return []
+
+        children = []
+        for c in range(count):
+            child, offset = read_string(bytes, offset)
+            children.append(child)
+        stat = ZnodeStat._make(stat_struct.unpack_from(bytes, offset))
+        return children, stat
+
+
+class CheckVersion(namedtuple('CheckVersion', 'path version')):
+    type = 13
+
+    def serialize(self):
+        b = bytearray()
+        b.extend(write_string(self.path))
+        b.extend(int_struct.pack(self.version))
+        return b
+
+
+class Transaction(namedtuple('Transaction', 'operations')):
+    type = 14
+
+    def serialize(self):
+        b = bytearray()
+        for op in self.operations:
+            b.extend(MultiHeader(op.type, False, -1).serialize() +
+                     op.serialize())
+        return b + multiheader_struct.pack(-1, True, -1)
+
+    @classmethod
+    def deserialize(cls, bytes, offset):
+        header = MultiHeader(None, False, None)
+        results = []
+        response = None
+        while not header.done:
+            if header.type == Create.type:
+                response, offset = read_string(bytes, offset)
+            elif header.type == Delete.type:
+                response = True
+            elif header.type == SetData.type:
+                response = ZnodeStat._make(
+                    stat_struct.unpack_from(bytes, offset))
+                offset += stat_struct.size
+            elif header.type == CheckVersion.type:
+                response = True
+            elif header.type == -1:
+                err = int_struct.unpack_from(bytes, offset)[0]
+                offset += int_struct.size
+                response = EXCEPTIONS[err]()
+            if response:
+                results.append(response)
+            header, offset = MultiHeader.deserialize(bytes, offset)
+        return results
+
+    @staticmethod
+    def unchroot(client, response):
+        resp = []
+        for result in response:
+            if isinstance(result, basestring):
+                resp.append(client.unchroot(result))
+            else:
+                resp.append(result)
+        return resp
+
+
+class Auth(namedtuple('Auth', 'auth_type scheme auth')):
+    type = 100
+
+    def serialize(self):
+        return (int_struct.pack(self.auth_type) + write_string(self.scheme) +
+                write_string(self.auth))
+
+
+class Watch(namedtuple('Watch', 'type state path')):
+    @classmethod
+    def deserialize(cls, bytes, offset):
+        """Given bytes and the current bytes offset, return the
+        type, state, path, and new offset"""
+        type, state = int_int_struct.unpack_from(bytes, offset)
+        offset += int_int_struct.size
+        path, offset = read_string(bytes, offset)
+        return cls(type, state, path), offset
+
+
+class ReplyHeader(namedtuple('ReplyHeader', 'xid, zxid, err')):
+    @classmethod
+    def deserialize(cls, bytes, offset):
+        """Given bytes and the current bytes offset, return a
+        :class:`ReplyHeader` instance and the new offset"""
+        new_offset = offset + reply_header_struct.size
+        return cls._make(
+            reply_header_struct.unpack_from(bytes, offset)), new_offset
+
+
+class MultiHeader(namedtuple('MultiHeader', 'type done err')):
+    def serialize(self):
+        b = bytearray()
+        b.extend(int_struct.pack(self.type))
+        b.extend([1 if self.done else 0])
+        b.extend(int_struct.pack(self.err))
+        return b
+
+    @classmethod
+    def deserialize(cls, bytes, offset):
+        t, done, err = multiheader_struct.unpack_from(bytes, offset)
+        offset += multiheader_struct.size
+        return cls(t, done is 1, err), offset

+ 237 - 0
desktop/core/ext-py/kazoo-2.0/kazoo/protocol/states.py

@@ -0,0 +1,237 @@
+"""Kazoo State and Event objects"""
+from collections import namedtuple
+
+
+class KazooState(object):
+    """High level connection state values
+
+    States inspired by Netflix Curator.
+
+    .. attribute:: SUSPENDED
+
+        The connection has been lost but may be recovered. We should
+        operate in a "safe mode" until then. When the connection is
+        resumed, it may be discovered that the session expired. A
+        client should not assume that locks are valid during this
+        time.
+
+    .. attribute:: CONNECTED
+
+        The connection is alive and well.
+
+    .. attribute:: LOST
+
+        The connection has been confirmed dead. Any ephemeral nodes
+        will need to be recreated upon re-establishing a connection.
+        If locks were acquired or recipes using ephemeral nodes are in
+        use, they can be considered lost as well.
+
+    """
+    SUSPENDED = "SUSPENDED"
+    CONNECTED = "CONNECTED"
+    LOST = "LOST"
+
+
+class KeeperState(object):
+    """Zookeeper State
+
+    Represents the Zookeeper state. Watch functions will receive a
+    :class:`KeeperState` attribute as their state argument.
+
+    .. attribute:: AUTH_FAILED
+
+        Authentication has failed, this is an unrecoverable error.
+
+    .. attribute:: CONNECTED
+
+        Zookeeper is connected.
+
+    .. attribute:: CONNECTED_RO
+
+        Zookeeper is connected in read-only state.
+
+    .. attribute:: CONNECTING
+
+        Zookeeper is currently attempting to establish a connection.
+
+    .. attribute:: EXPIRED_SESSION
+
+        The prior session was invalid, all prior ephemeral nodes are
+        gone.
+
+    """
+    AUTH_FAILED = 'AUTH_FAILED'
+    CONNECTED = 'CONNECTED'
+    CONNECTED_RO = 'CONNECTED_RO'
+    CONNECTING = 'CONNECTING'
+    CLOSED = 'CLOSED'
+    EXPIRED_SESSION = 'EXPIRED_SESSION'
+
+
+class EventType(object):
+    """Zookeeper Event
+
+    Represents a Zookeeper event. Events trigger watch functions which
+    will receive a :class:`EventType` attribute as their event
+    argument.
+
+    .. attribute:: CREATED
+
+        A node has been created.
+
+    .. attribute:: DELETED
+
+        A node has been deleted.
+
+    .. attribute:: CHANGED
+
+        The data for a node has changed.
+
+    .. attribute:: CHILD
+
+        The children under a node have changed (a child was added or
+        removed). This event does not indicate the data for a child
+        node has changed, which must have its own watch established.
+
+    """
+    CREATED = 'CREATED'
+    DELETED = 'DELETED'
+    CHANGED = 'CHANGED'
+    CHILD = 'CHILD'
+
+EVENT_TYPE_MAP = {
+    1: EventType.CREATED,
+    2: EventType.DELETED,
+    3: EventType.CHANGED,
+    4: EventType.CHILD
+}
+
+
+class WatchedEvent(namedtuple('WatchedEvent', ('type', 'state', 'path'))):
+    """A change on ZooKeeper that a Watcher is able to respond to.
+
+    The :class:`WatchedEvent` includes exactly what happened, the
+    current state of ZooKeeper, and the path of the node that was
+    involved in the event. An instance of :class:`WatchedEvent` will be
+    passed to registered watch functions.
+
+    .. attribute:: type
+
+        A :class:`EventType` attribute indicating the event type.
+
+    .. attribute:: state
+
+        A :class:`KeeperState` attribute indicating the Zookeeper
+        state.
+
+    .. attribute:: path
+
+        The path of the node for the watch event.
+
+    """
+
+
+class Callback(namedtuple('Callback', ('type', 'func', 'args'))):
+    """A callback that is handed to a handler for dispatch
+
+    :param type: Type of the callback, currently is only 'watch'
+    :param func: Callback function
+    :param args: Argument list for the callback function
+
+    """
+
+
+class ZnodeStat(namedtuple('ZnodeStat', 'czxid mzxid ctime mtime version'
+                           ' cversion aversion ephemeralOwner dataLength'
+                           ' numChildren pzxid')):
+    """A ZnodeStat structure with convenience properties
+
+    When getting the value of a node from Zookeeper, the properties for
+    the node known as a "Stat structure" will be retrieved. The
+    :class:`ZnodeStat` object provides access to the standard Stat
+    properties and additional properties that are more readable and use
+    Python time semantics (seconds since epoch instead of ms).
+
+    .. note::
+
+        The original Zookeeper Stat name is in parens next to the name
+        when it differs from the convenience attribute. These are **not
+        functions**, just attributes.
+
+    .. attribute:: creation_transaction_id (czxid)
+
+        The transaction id of the change that caused this znode to be
+        created.
+
+    .. attribute:: last_modified_transaction_id (mzxid)
+
+        The transaction id of the change that last modified this znode.
+
+    .. attribute:: created (ctime)
+
+        The time in seconds from epoch when this node was created.
+        (ctime is in milliseconds)
+
+    .. attribute:: last_modified (mtime)
+
+        The time in seconds from epoch when this znode was last
+        modified. (mtime is in milliseconds)
+
+    .. attribute:: version
+
+        The number of changes to the data of this znode.
+
+    .. attribute:: acl_version (aversion)
+
+        The number of changes to the ACL of this znode.
+
+    .. attribute:: owner_session_id (ephemeralOwner)
+
+        The session id of the owner of this znode if the znode is an
+        ephemeral node. If it is not an ephemeral node, it will be
+        `None`. (ephemeralOwner will be 0 if it is not ephemeral)
+
+    .. attribute:: data_length (dataLength)
+
+        The length of the data field of this znode.
+
+    .. attribute:: children_count (numChildren)
+
+        The number of children of this znode.
+
+    """
+    @property
+    def acl_version(self):
+        return self.aversion
+
+    @property
+    def children_version(self):
+        return self.cversion
+
+    @property
+    def created(self):
+        return self.ctime / 1000.0
+
+    @property
+    def last_modified(self):
+        return self.mtime / 1000.0
+
+    @property
+    def owner_session_id(self):
+        return self.ephemeralOwner or None
+
+    @property
+    def creation_transaction_id(self):
+        return self.czxid
+
+    @property
+    def last_modified_transaction_id(self):
+        return self.mzxid
+
+    @property
+    def data_length(self):
+        return self.dataLength
+
+    @property
+    def children_count(self):
+        return self.numChildren

+ 1 - 0
desktop/core/ext-py/kazoo-2.0/kazoo/recipe/__init__.py

@@ -0,0 +1 @@
+#

+ 214 - 0
desktop/core/ext-py/kazoo-2.0/kazoo/recipe/barrier.py

@@ -0,0 +1,214 @@
+"""Zookeeper Barriers
+
+:Maintainer: None
+:Status: Unknown
+
+"""
+import os
+import socket
+import uuid
+
+from kazoo.protocol.states import EventType
+from kazoo.exceptions import KazooException
+from kazoo.exceptions import NoNodeError
+from kazoo.exceptions import NodeExistsError
+
+
+class Barrier(object):
+    """Kazoo Barrier
+
+    Implements a barrier to block processing of a set of nodes until
+    a condition is met at which point the nodes will be allowed to
+    proceed. The barrier is in place if its node exists.
+
+    .. warning::
+
+        The :meth:`wait` function does not handle connection loss and
+        may raise :exc:`~kazoo.exceptions.ConnectionLossException` if
+        the connection is lost while waiting.
+
+    """
+    def __init__(self, client, path):
+        """Create a Kazoo Barrier
+
+        :param client: A :class:`~kazoo.client.KazooClient` instance.
+        :param path: The barrier path to use.
+
+        """
+        self.client = client
+        self.path = path
+
+    def create(self):
+        """Establish the barrier if it doesn't exist already"""
+        self.client.retry(self.client.ensure_path, self.path)
+
+    def remove(self):
+        """Remove the barrier
+
+        :returns: Whether the barrier actually needed to be removed.
+        :rtype: bool
+
+        """
+        try:
+            self.client.retry(self.client.delete, self.path)
+            return True
+        except NoNodeError:
+            return False
+
+    def wait(self, timeout=None):
+        """Wait on the barrier to be cleared
+
+        :returns: True if the barrier has been cleared, otherwise
+                  False.
+        :rtype: bool
+
+        """
+        cleared = self.client.handler.event_object()
+
+        def wait_for_clear(event):
+            if event.type == EventType.DELETED:
+                cleared.set()
+
+        exists = self.client.exists(self.path, watch=wait_for_clear)
+        if not exists:
+            return True
+
+        cleared.wait(timeout)
+        return cleared.is_set()
+
+
+class DoubleBarrier(object):
+    """Kazoo Double Barrier
+
+    Double barriers are used to synchronize the beginning and end of
+    a distributed task. The barrier blocks when entering it until all
+    the members have joined, and blocks when leaving until all the
+    members have left.
+
+    .. note::
+
+        You should register a listener for session loss as the process
+        will no longer be part of the barrier once the session is
+        gone. Connection losses will be retried with the default retry
+        policy.
+
+    """
+    def __init__(self, client, path, num_clients, identifier=None):
+        """Create a Double Barrier
+
+        :param client: A :class:`~kazoo.client.KazooClient` instance.
+        :param path: The barrier path to use.
+        :param num_clients: How many clients must enter the barrier to
+                            proceed.
+        :type num_clients: int
+        :param identifier: An identifier to use for this member of the
+                           barrier when participating. Defaults to the
+                           hostname + process id.
+
+        """
+        self.client = client
+        self.path = path
+        self.num_clients = num_clients
+        self._identifier = identifier or '%s-%s' % (
+            socket.getfqdn(), os.getpid())
+        self.participating = False
+        self.assured_path = False
+        self.node_name = uuid.uuid4().hex
+        self.create_path = self.path + "/" + self.node_name
+
+    def enter(self):
+        """Enter the barrier, blocks until all nodes have entered"""
+        try:
+            self.client.retry(self._inner_enter)
+            self.participating = True
+        except KazooException:
+            # We failed to enter, best effort cleanup
+            self._best_effort_cleanup()
+            self.participating = False
+
+    def _inner_enter(self):
+        # make sure our barrier parent node exists
+        if not self.assured_path:
+            self.client.ensure_path(self.path)
+            self.assured_path = True
+
+        ready = self.client.handler.event_object()
+
+        try:
+            self.client.create(self.create_path,
+                self._identifier.encode('utf-8'), ephemeral=True)
+        except NodeExistsError:
+            pass
+
+        def created(event):
+            if event.type == EventType.CREATED:
+                ready.set()
+
+        self.client.exists(self.path + '/' + 'ready', watch=created)
+
+        children = self.client.get_children(self.path)
+
+        if len(children) < self.num_clients:
+            ready.wait()
+        else:
+            self.client.ensure_path(self.path + '/ready')
+        return True
+
+    def leave(self):
+        """Leave the barrier, blocks until all nodes have left"""
+        try:
+            self.client.retry(self._inner_leave)
+        except KazooException:  # pragma: nocover
+            # Failed to cleanly leave
+            self._best_effort_cleanup()
+        self.participating = False
+
+    def _inner_leave(self):
+        # Delete the ready node if its around
+        try:
+            self.client.delete(self.path + '/ready')
+        except NoNodeError:
+            pass
+
+        while True:
+            children = self.client.get_children(self.path)
+            if not children:
+                return True
+
+            if len(children) == 1 and children[0] == self.node_name:
+                self.client.delete(self.create_path)
+                return True
+
+            children.sort()
+
+            ready = self.client.handler.event_object()
+
+            def deleted(event):
+                if event.type == EventType.DELETED:
+                    ready.set()
+
+            if self.node_name == children[0]:
+                # We're first, wait on the highest to leave
+                if not self.client.exists(self.path + '/' + children[-1],
+                                          watch=deleted):
+                    continue
+
+                ready.wait()
+                continue
+
+            # Delete our node
+            self.client.delete(self.create_path)
+
+            # Wait on the first
+            if not self.client.exists(self.path + '/' + children[0],
+                                      watch=deleted):
+                continue
+
+            # Wait for the lowest to be deleted
+            ready.wait()
+
+    def _best_effort_cleanup(self):
+        try:
+            self.client.retry(self.client.delete, self.create_path)
+        except NoNodeError:
+            pass

+ 94 - 0
desktop/core/ext-py/kazoo-2.0/kazoo/recipe/counter.py

@@ -0,0 +1,94 @@
+"""Zookeeper Counter
+
+:Maintainer: None
+:Status: Unknown
+
+"""
+
+from kazoo.exceptions import BadVersionError
+from kazoo.retry import ForceRetryError
+
+
+class Counter(object):
+    """Kazoo Counter
+
+    A shared counter of either int or float values. Changes to the
+    counter are done atomically. The general retry policy is used to
+    retry operations if concurrent changes are detected.
+
+    The data is marshaled using `repr(value)` and converted back using
+    `type(counter.default)(value)` both using an ascii encoding. As
+    such other data types might be used for the counter value.
+
+    Counter changes can raise
+    :class:`~kazoo.exceptions.BadVersionError` if the retry policy
+    wasn't able to apply a change.
+
+    Example usage:
+
+    .. code-block:: python
+
+        zk = KazooClient()
+        counter = zk.Counter("/int")
+        counter += 2
+        counter -= 1
+        counter.value == 1
+
+        counter = zk.Counter("/float", default=1.0)
+        counter += 2.0
+        counter.value == 3.0
+
+    """
+    def __init__(self, client, path, default=0):
+        """Create a Kazoo Counter
+
+        :param client: A :class:`~kazoo.client.KazooClient` instance.
+        :param path: The counter path to use.
+        :param default: The default value.
+
+        """
+        self.client = client
+        self.path = path
+        self.default = default
+        self.default_type = type(default)
+        self._ensured_path = False
+
+    def _ensure_node(self):
+        if not self._ensured_path:
+            # make sure our node exists
+            self.client.ensure_path(self.path)
+            self._ensured_path = True
+
+    def _value(self):
+        self._ensure_node()
+        old, stat = self.client.get(self.path)
+        old = old.decode('ascii') if old != b'' else self.default
+        version = stat.version
+        data = self.default_type(old)
+        return data, version
+
+    @property
+    def value(self):
+        return self._value()[0]
+
+    def _change(self, value):
+        if not isinstance(value, self.default_type):
+            raise TypeError('invalid type for value change')
+        self.client.retry(self._inner_change, value)
+        return self
+
+    def _inner_change(self, value):
+        data, version = self._value()
+        data = repr(data + value).encode('ascii')
+        try:
+            self.client.set(self.path, data, version=version)
+        except BadVersionError:  # pragma: nocover
+            raise ForceRetryError()
+
+    def __add__(self, value):
+        """Add value to counter."""
+        return self._change(value)
+
+    def __sub__(self, value):
+        """Subtract value from counter."""
+        return self._change(-value)

+ 79 - 0
desktop/core/ext-py/kazoo-2.0/kazoo/recipe/election.py

@@ -0,0 +1,79 @@
+"""ZooKeeper Leader Elections
+
+:Maintainer: None
+:Status: Unknown
+
+"""
+from kazoo.exceptions import CancelledError
+
+
+class Election(object):
+    """Kazoo Basic Leader Election
+
+    Example usage with a :class:`~kazoo.client.KazooClient` instance::
+
+        zk = KazooClient()
+        election = zk.Election("/electionpath", "my-identifier")
+
+        # blocks until the election is won, then calls
+        # my_leader_function()
+        election.run(my_leader_function)
+
+    """
+    def __init__(self, client, path, identifier=None):
+        """Create a Kazoo Leader Election
+
+        :param client: A :class:`~kazoo.client.KazooClient` instance.
+        :param path: The election path to use.
+        :param identifier: Name to use for this lock contender. This
+                           can be useful for querying to see who the
+                           current lock contenders are.
+
+        """
+        self.lock = client.Lock(path, identifier)
+
+    def run(self, func, *args, **kwargs):
+        """Contend for the leadership
+
+        This call will block until either this contender is cancelled
+        or this contender wins the election and the provided leadership
+        function subsequently returns or fails.
+
+        :param func: A function to be called if/when the election is
+                     won.
+        :param args: Arguments to leadership function.
+        :param kwargs: Keyword arguments to leadership function.
+
+        """
+        if not callable(func):
+            raise ValueError("leader function is not callable")
+
+        try:
+            with self.lock:
+                func(*args, **kwargs)
+
+        except CancelledError:
+            pass
+
+    def cancel(self):
+        """Cancel participation in the election
+
+        .. note::
+
+            If this contender has already been elected leader, this
+            method will not interrupt the leadership function.
+
+        """
+        self.lock.cancel()
+
+    def contenders(self):
+        """Return an ordered list of the current contenders in the
+        election
+
+        .. note::
+
+            If the contenders did not set an identifier, it will appear
+            as a blank string.
+
+        """
+        return self.lock.contenders()

+ 520 - 0
desktop/core/ext-py/kazoo-2.0/kazoo/recipe/lock.py

@@ -0,0 +1,520 @@
+"""Zookeeper Locking Implementations
+
+:Maintainer: None
+:Status: Unknown
+
+Error Handling
+==============
+
+It's highly recommended to add a state listener with
+:meth:`~KazooClient.add_listener` and watch for
+:attr:`~KazooState.LOST` and :attr:`~KazooState.SUSPENDED` state
+changes and re-act appropriately. In the event that a
+:attr:`~KazooState.LOST` state occurs, its certain that the lock
+and/or the lease has been lost.
+
+"""
+import uuid
+
+from kazoo.retry import (
+    KazooRetry,
+    RetryFailedError,
+    ForceRetryError
+)
+from kazoo.exceptions import CancelledError
+from kazoo.exceptions import KazooException
+from kazoo.exceptions import LockTimeout
+from kazoo.exceptions import NoNodeError
+from kazoo.protocol.states import KazooState
+
+
+class Lock(object):
+    """Kazoo Lock
+
+    Example usage with a :class:`~kazoo.client.KazooClient` instance:
+
+    .. code-block:: python
+
+        zk = KazooClient()
+        lock = zk.Lock("/lockpath", "my-identifier")
+        with lock:  # blocks waiting for lock acquisition
+            # do something with the lock
+
+    Note: This lock is re-entrant. Repeat calls after acquired will
+    continue to return ''True''.
+
+    """
+    _NODE_NAME = '__lock__'
+
+    def __init__(self, client, path, identifier=None):
+        """Create a Kazoo lock.
+
+        :param client: A :class:`~kazoo.client.KazooClient` instance.
+        :param path: The lock path to use.
+        :param identifier: Name to use for this lock contender. This
+                           can be useful for querying to see who the
+                           current lock contenders are.
+
+        """
+        self.client = client
+        self.path = path
+
+        # some data is written to the node. this can be queried via
+        # contenders() to see who is contending for the lock
+        self.data = str(identifier or "").encode('utf-8')
+
+        self.wake_event = client.handler.event_object()
+
+        # props to Netflix Curator for this trick. It is possible for our
+        # create request to succeed on the server, but for a failure to
+        # prevent us from getting back the full path name. We prefix our
+        # lock name with a uuid and can check for its presence on retry.
+        self.prefix = uuid.uuid4().hex + self._NODE_NAME
+        self.create_path = self.path + "/" + self.prefix
+
+        self.create_tried = False
+        self.is_acquired = False
+        self.assured_path = False
+        self.cancelled = False
+        self._retry = KazooRetry(max_tries=None,
+                                 sleep_func=client.handler.sleep_func)
+
+    def _ensure_path(self):
+        self.client.ensure_path(self.path)
+        self.assured_path = True
+
+    def cancel(self):
+        """Cancel a pending lock acquire."""
+        self.cancelled = True
+        self.wake_event.set()
+
+    def acquire(self, blocking=True, timeout=None):
+        """
+        Acquire the lock. By defaults blocks and waits forever.
+
+        :param blocking: Block until lock is obtained or return immediately.
+        :type blocking: bool
+        :param timeout: Don't wait forever to acquire the lock.
+        :type timeout: float or None
+
+        :returns: Was the lock acquired?
+        :rtype: bool
+
+        :raises: :exc:`~kazoo.exceptions.LockTimeout` if the lock
+                 wasn't acquired within `timeout` seconds.
+
+        .. versionadded:: 1.1
+            The timeout option.
+        """
+        try:
+            retry = self._retry.copy()
+            retry.deadline = timeout
+            self.is_acquired = retry(self._inner_acquire,
+                blocking=blocking, timeout=timeout)
+        except RetryFailedError:
+            self._best_effort_cleanup()
+        except KazooException:
+            # if we did ultimately fail, attempt to clean up
+            self._best_effort_cleanup()
+            self.cancelled = False
+            raise
+
+        if not self.is_acquired:
+            self._delete_node(self.node)
+
+        return self.is_acquired
+
+    def _watch_session(self, state):
+        self.wake_event.set()
+        return True
+
+    def _inner_acquire(self, blocking, timeout):
+        # make sure our election parent node exists
+        if not self.assured_path:
+            self._ensure_path()
+
+        node = None
+        if self.create_tried:
+            node = self._find_node()
+        else:
+            self.create_tried = True
+
+        if not node:
+            node = self.client.create(self.create_path, self.data,
+                                      ephemeral=True, sequence=True)
+            # strip off path to node
+            node = node[len(self.path) + 1:]
+
+        self.node = node
+
+        while True:
+            self.wake_event.clear()
+
+            # bail out with an exception if cancellation has been requested
+            if self.cancelled:
+                raise CancelledError()
+
+            children = self._get_sorted_children()
+
+            try:
+                our_index = children.index(node)
+            except ValueError:  # pragma: nocover
+                # somehow we aren't in the children -- probably we are
+                # recovering from a session failure and our ephemeral
+                # node was removed
+                raise ForceRetryError()
+
+            if self.acquired_lock(children, our_index):
+                return True
+
+            if not blocking:
+                return False
+
+            # otherwise we are in the mix. watch predecessor and bide our time
+            predecessor = self.path + "/" + children[our_index - 1]
+            self.client.add_listener(self._watch_session)
+            try:
+                if self.client.exists(predecessor, self._watch_predecessor):
+                    self.wake_event.wait(timeout)
+                    if not self.wake_event.isSet():
+                        raise LockTimeout("Failed to acquire lock on %s after %s "
+                                          "seconds" % (self.path, timeout))
+            finally:
+                self.client.remove_listener(self._watch_session)
+
+    def acquired_lock(self, children, index):
+        return index == 0
+
+    def _watch_predecessor(self, event):
+        self.wake_event.set()
+
+    def _get_sorted_children(self):
+        children = self.client.get_children(self.path)
+
+        # can't just sort directly: the node names are prefixed by uuids
+        lockname = self._NODE_NAME
+        children.sort(key=lambda c: c[c.find(lockname) + len(lockname):])
+        return children
+
+    def _find_node(self):
+        children = self.client.get_children(self.path)
+        for child in children:
+            if child.startswith(self.prefix):
+                return child
+        return None
+
+    def _delete_node(self, node):
+        self.client.delete(self.path + "/" + node)
+
+    def _best_effort_cleanup(self):
+        try:
+            node = self._find_node()
+            if node:
+                self._delete_node(node)
+        except KazooException:  # pragma: nocover
+            pass
+
+    def release(self):
+        """Release the lock immediately."""
+        return self.client.retry(self._inner_release)
+
+    def _inner_release(self):
+        if not self.is_acquired:
+            return False
+
+        try:
+            self._delete_node(self.node)
+        except NoNodeError:  # pragma: nocover
+            pass
+
+        self.is_acquired = False
+        self.node = None
+
+        return True
+
+    def contenders(self):
+        """Return an ordered list of the current contenders for the
+        lock.
+
+        .. note::
+
+            If the contenders did not set an identifier, it will appear
+            as a blank string.
+
+        """
+        # make sure our election parent node exists
+        if not self.assured_path:
+            self._ensure_path()
+
+        children = self._get_sorted_children()
+
+        contenders = []
+        for child in children:
+            try:
+                data, stat = self.client.get(self.path + "/" + child)
+                contenders.append(data.decode('utf-8'))
+            except NoNodeError:  # pragma: nocover
+                pass
+        return contenders
+
+    def __enter__(self):
+        self.acquire()
+
+    def __exit__(self, exc_type, exc_value, traceback):
+        self.release()
+
+
+class Semaphore(object):
+    """A Zookeeper-based Semaphore
+
+    This synchronization primitive operates in the same manner as the
+    Python threading version only uses the concept of leases to
+    indicate how many available leases are available for the lock
+    rather than counting.
+
+    Example:
+
+    .. code-block:: python
+
+        zk = KazooClient()
+        semaphore = zk.Semaphore("/leasepath", "my-identifier")
+        with semaphore:  # blocks waiting for lock acquisition
+            # do something with the semaphore
+
+    .. warning::
+
+        This class stores the allowed max_leases as the data on the
+        top-level semaphore node. The stored value is checked once
+        against the max_leases of each instance. This check is
+        performed when acquire is called the first time. The semaphore
+        node needs to be deleted to change the allowed leases.
+
+    .. versionadded:: 0.6
+        The Semaphore class.
+
+    .. versionadded:: 1.1
+        The max_leases check.
+
+    """
+    def __init__(self, client, path, identifier=None, max_leases=1):
+        """Create a Kazoo Lock
+
+        :param client: A :class:`~kazoo.client.KazooClient` instance.
+        :param path: The semaphore path to use.
+        :param identifier: Name to use for this lock contender. This
+                           can be useful for querying to see who the
+                           current lock contenders are.
+        :param max_leases: The maximum amount of leases available for
+                           the semaphore.
+
+        """
+        # Implementation notes about how excessive thundering herd
+        # and watches are avoided
+        # - A node (lease pool) holds children for each lease in use
+        # - A lock is acquired for a process attempting to acquire a
+        #   lease. If a lease is available, the ephemeral node is
+        #   created in the lease pool and the lock is released.
+        # - Only the lock holder watches for children changes in the
+        #   lease pool
+        self.client = client
+        self.path = path
+
+        # some data is written to the node. this can be queried via
+        # contenders() to see who is contending for the lock
+        self.data = str(identifier or "").encode('utf-8')
+        self.max_leases = max_leases
+        self.wake_event = client.handler.event_object()
+
+        self.create_path = self.path + "/" + uuid.uuid4().hex
+        self.lock_path = path + '-' + '__lock__'
+        self.is_acquired = False
+        self.assured_path = False
+        self.cancelled = False
+        self._session_expired = False
+
+    def _ensure_path(self):
+        result = self.client.ensure_path(self.path)
+        self.assured_path = True
+        if result is True:
+            # node did already exist
+            data, _ = self.client.get(self.path)
+            try:
+                leases = int(data.decode('utf-8'))
+            except (ValueError, TypeError):
+                # ignore non-numeric data, maybe the node data is used
+                # for other purposes
+                pass
+            else:
+                if leases != self.max_leases:
+                    raise ValueError(
+                        "Inconsistent max leases: %s, expected: %s" %
+                        (leases, self.max_leases)
+                    )
+        else:
+            self.client.set(self.path, str(self.max_leases).encode('utf-8'))
+
+    def cancel(self):
+        """Cancel a pending semaphore acquire."""
+        self.cancelled = True
+        self.wake_event.set()
+
+    def acquire(self, blocking=True, timeout=None):
+        """Acquire the semaphore. By defaults blocks and waits forever.
+
+        :param blocking: Block until semaphore is obtained or
+                         return immediately.
+        :type blocking: bool
+        :param timeout: Don't wait forever to acquire the semaphore.
+        :type timeout: float or None
+
+        :returns: Was the semaphore acquired?
+        :rtype: bool
+
+        :raises:
+            ValueError if the max_leases value doesn't match the
+            stored value.
+
+            :exc:`~kazoo.exceptions.LockTimeout` if the semaphore
+            wasn't acquired within `timeout` seconds.
+
+        .. versionadded:: 1.1
+            The blocking, timeout arguments and the max_leases check.
+        """
+        # If the semaphore had previously been canceled, make sure to
+        # reset that state.
+        self.cancelled = False
+
+        try:
+            self.is_acquired = self.client.retry(
+                self._inner_acquire, blocking=blocking, timeout=timeout)
+        except KazooException:
+            # if we did ultimately fail, attempt to clean up
+            self._best_effort_cleanup()
+            self.cancelled = False
+            raise
+
+        return self.is_acquired
+
+    def _inner_acquire(self, blocking, timeout=None):
+        """Inner loop that runs from the top anytime a command hits a
+        retryable Zookeeper exception."""
+        self._session_expired = False
+        self.client.add_listener(self._watch_session)
+
+        if not self.assured_path:
+            self._ensure_path()
+
+        # Do we already have a lease?
+        if self.client.exists(self.create_path):
+            return True
+
+        with self.client.Lock(self.lock_path, self.data):
+            while True:
+                self.wake_event.clear()
+
+                # Attempt to grab our lease...
+                if self._get_lease():
+                    return True
+
+                if blocking:
+                    # If blocking, wait until self._watch_lease_change() is
+                    # called before returning
+                    self.wake_event.wait(timeout)
+                    if not self.wake_event.isSet():
+                        raise LockTimeout(
+                            "Failed to acquire semaphore on %s "
+                            "after %s seconds" % (self.path, timeout))
+                else:
+                    # If not blocking, register another watch that will trigger
+                    # self._get_lease() as soon as the children change again.
+                    self.client.get_children(self.path, self._get_lease)
+                    return False
+
+    def _watch_lease_change(self, event):
+        self.wake_event.set()
+
+    def _get_lease(self, data=None):
+        # Make sure the session is still valid
+        if self._session_expired:
+            raise ForceRetryError("Retry on session loss at top")
+
+        # Make sure that the request hasn't been canceled
+        if self.cancelled:
+            raise CancelledError("Semaphore cancelled")
+
+        # Get a list of the current potential lock holders. If they change,
+        # notify our wake_event object. This is used to unblock a blocking
+        # self._inner_acquire call.
+        children = self.client.get_children(self.path,
+                                            self._watch_lease_change)
+
+        # If there are leases available, acquire one
+        if len(children) < self.max_leases:
+            self.client.create(self.create_path, self.data, ephemeral=True)
+
+        # Check if our acquisition was successful or not. Update our state.
+        if self.client.exists(self.create_path):
+            self.is_acquired = True
+        else:
+            self.is_acquired = False
+
+        # Return current state
+        return self.is_acquired
+
+    def _watch_session(self, state):
+        if state == KazooState.LOST:
+            self._session_expired = True
+            self.wake_event.set()
+
+            # Return true to de-register
+            return True
+
+    def _best_effort_cleanup(self):
+        try:
+            self.client.delete(self.create_path)
+        except KazooException:  # pragma: nocover
+            pass
+
+    def release(self):
+        """Release the lease immediately."""
+        return self.client.retry(self._inner_release)
+
+    def _inner_release(self):
+        if not self.is_acquired:
+            return False
+
+        try:
+            self.client.delete(self.create_path)
+        except NoNodeError:  # pragma: nocover
+            pass
+        self.is_acquired = False
+        return True
+
+    def lease_holders(self):
+        """Return an unordered list of the current lease holders.
+
+        .. note::
+
+            If the lease holder did not set an identifier, it will
+            appear as a blank string.
+
+        """
+        if not self.client.exists(self.path):
+            return []
+
+        children = self.client.get_children(self.path)
+
+        lease_holders = []
+        for child in children:
+            try:
+                data, stat = self.client.get(self.path + "/" + child)
+                lease_holders.append(data.decode('utf-8'))
+            except NoNodeError:  # pragma: nocover
+                pass
+        return lease_holders
+
+    def __enter__(self):
+        self.acquire()
+
+    def __exit__(self, exc_type, exc_value, traceback):
+        self.release()

+ 377 - 0
desktop/core/ext-py/kazoo-2.0/kazoo/recipe/partitioner.py

@@ -0,0 +1,377 @@
+"""Zookeeper Partitioner Implementation
+
+:Maintainer: None
+:Status: Unknown
+
+:class:`SetPartitioner` implements a partitioning scheme using
+Zookeeper for dividing up resources amongst members of a party.
+
+This is useful when there is a set of resources that should only be
+accessed by a single process at a time that multiple processes
+across a cluster might want to divide up.
+
+Example Use-Case
+----------------
+
+- Multiple workers across a cluster need to divide up a list of queues
+  so that no two workers own the same queue.
+
+"""
+import logging
+import os
+import socket
+from functools import partial
+
+from kazoo.exceptions import KazooException
+from kazoo.protocol.states import KazooState
+from kazoo.recipe.watchers import PatientChildrenWatch
+
+log = logging.getLogger(__name__)
+
+
+class PartitionState(object):
+    """High level partition state values
+
+    .. attribute:: ALLOCATING
+
+        The set needs to be partitioned, and may require an existing
+        partition set to be released before acquiring a new partition
+        of the set.
+
+    .. attribute:: ACQUIRED
+
+        The set has been partitioned and acquired.
+
+    .. attribute:: RELEASE
+
+        The set needs to be repartitioned, and the current partitions
+        must be released before a new allocation can be made.
+
+    .. attribute:: FAILURE
+
+        The set partition has failed. This occurs when the maximum
+        time to partition the set is exceeded or the Zookeeper session
+        is lost. The partitioner is unusable after this state and must
+        be recreated.
+
+    """
+    ALLOCATING = "ALLOCATING"
+    ACQUIRED = "ACQUIRED"
+    RELEASE = "RELEASE"
+    FAILURE = "FAILURE"
+
+
+class SetPartitioner(object):
+    """Partitions a set amongst members of a party
+
+    This class will partition a set amongst members of a party such
+    that each member will be given zero or more items of the set and
+    each set item will be given to a single member. When new members
+    enter or leave the party, the set will be re-partitioned amongst
+    the members.
+
+    When the :class:`SetPartitioner` enters the
+    :attr:`~PartitionState.FAILURE` state, it is unrecoverable
+    and a new :class:`SetPartitioner` should be created.
+
+    Example:
+
+    .. code-block:: python
+
+        from kazoo.client import KazooClient
+        client = KazooClient()
+
+        qp = client.SetPartitioner(
+            path='/work_queues', set=('queue-1', 'queue-2', 'queue-3'))
+
+        while 1:
+            if qp.failed:
+                raise Exception("Lost or unable to acquire partition")
+            elif qp.release:
+                qp.release_set()
+            elif qp.acquired:
+                for partition in qp:
+                    # Do something with each partition
+            elif qp.allocating:
+                qp.wait_for_acquire()
+
+    **State Transitions**
+
+    When created, the :class:`SetPartitioner` enters the
+    :attr:`PartitionState.ALLOCATING` state.
+
+    :attr:`~PartitionState.ALLOCATING` ->
+    :attr:`~PartitionState.ACQUIRED`
+
+        Set was partitioned successfully, the partition list assigned
+        is accessible via list/iter methods or calling list() on the
+        :class:`SetPartitioner` instance.
+
+    :attr:`~PartitionState.ALLOCATING` ->
+    :attr:`~PartitionState.FAILURE`
+
+        Allocating the set failed either due to a Zookeeper session
+        expiration, or failure to acquire the items of the set within
+        the timeout period.
+
+    :attr:`~PartitionState.ACQUIRED` ->
+    :attr:`~PartitionState.RELEASE`
+
+        The members of the party have changed, and the set needs to be
+        repartitioned. :meth:`SetPartitioner.release` should be called
+        as soon as possible.
+
+    :attr:`~PartitionState.ACQUIRED` ->
+    :attr:`~PartitionState.FAILURE`
+
+        The current partition was lost due to a Zookeeper session
+        expiration.
+
+    :attr:`~PartitionState.RELEASE` ->
+    :attr:`~PartitionState.ALLOCATING`
+
+        The current partition was released and is being re-allocated.
+
+    """
+    def __init__(self, client, path, set, partition_func=None,
+                 identifier=None, time_boundary=30):
+        """Create a :class:`~SetPartitioner` instance
+
+        :param client: A :class:`~kazoo.client.KazooClient` instance.
+        :param path: The partition path to use.
+        :param set: The set of items to partition.
+        :param partition_func: A function to use to decide how to
+                               partition the set.
+        :param identifier: An identifier to use for this member of the
+                           party when participating. Defaults to the
+                           hostname + process id.
+        :param time_boundary: How long the party members must be stable
+                              before allocation can complete.
+
+        """
+        self.state = PartitionState.ALLOCATING
+
+        self._client = client
+        self._path = path
+        self._set = set
+        self._partition_set = []
+        self._partition_func = partition_func or self._partitioner
+        self._identifier = identifier or '%s-%s' % (
+            socket.getfqdn(), os.getpid())
+        self._locks = []
+        self._lock_path = '/'.join([path, 'locks'])
+        self._party_path = '/'.join([path, 'party'])
+        self._time_boundary = time_boundary
+
+        self._acquire_event = client.handler.event_object()
+
+        # Create basic path nodes
+        client.ensure_path(path)
+        client.ensure_path(self._lock_path)
+        client.ensure_path(self._party_path)
+
+        # Join the party
+        self._party = client.ShallowParty(self._party_path,
+                                          identifier=self._identifier)
+        self._party.join()
+
+        self._was_allocated = False
+        self._state_change = client.handler.rlock_object()
+        client.add_listener(self._establish_sessionwatch)
+
+        # Now watch the party and set the callback on the async result
+        # so we know when we're ready
+        self._children_updated = False
+        self._child_watching(self._allocate_transition, async=True)
+
+    def __iter__(self):
+        """Return the partitions in this partition set"""
+        for partition in self._partition_set:
+            yield partition
+
+    @property
+    def failed(self):
+        """Corresponds to the :attr:`PartitionState.FAILURE` state"""
+        return self.state == PartitionState.FAILURE
+
+    @property
+    def release(self):
+        """Corresponds to the :attr:`PartitionState.RELEASE` state"""
+        return self.state == PartitionState.RELEASE
+
+    @property
+    def allocating(self):
+        """Corresponds to the :attr:`PartitionState.ALLOCATING`
+        state"""
+        return self.state == PartitionState.ALLOCATING
+
+    @property
+    def acquired(self):
+        """Corresponds to the :attr:`PartitionState.ACQUIRED` state"""
+        return self.state == PartitionState.ACQUIRED
+
+    def wait_for_acquire(self, timeout=30):
+        """Wait for the set to be partitioned and acquired
+
+        :param timeout: How long to wait before returning.
+        :type timeout: int
+
+        """
+        self._acquire_event.wait(timeout)
+
+    def release_set(self):
+        """Call to release the set
+
+        This method begins the step of allocating once the set has
+        been released.
+
+        """
+        self._release_locks()
+        if self._locks:  # pragma: nocover
+            # This shouldn't happen, it means we couldn't release our
+            # locks, abort
+            self._fail_out()
+            return
+        else:
+            with self._state_change:
+                if self.failed:
+                    return
+                self.state = PartitionState.ALLOCATING
+        self._child_watching(self._allocate_transition, async=True)
+
+    def finish(self):
+        """Call to release the set and leave the party"""
+        self._release_locks()
+        self._fail_out()
+
+    def _fail_out(self):
+        with self._state_change:
+            self.state = PartitionState.FAILURE
+        if self._party.participating:
+            try:
+                self._party.leave()
+            except KazooException:  # pragma: nocover
+                pass
+
+    def _allocate_transition(self, result):
+        """Called when in allocating mode, and the children settled"""
+        # Did we get an exception waiting for children to settle?
+        if result.exception:  # pragma: nocover
+            self._fail_out()
+            return
+
+        children, async_result = result.get()
+        self._children_updated = False
+
+        # Add a callback when children change on the async_result
+        def updated(result):
+            with self._state_change:
+                if self.acquired:
+                    self.state = PartitionState.RELEASE
+            self._children_updated = True
+
+        async_result.rawlink(updated)
+
+        # Split up the set
+        self._partition_set = self._partition_func(
+            self._identifier, list(self._party), self._set)
+
+        # Proceed to acquire locks for the working set as needed
+        for member in self._partition_set:
+            if self._children_updated or self.failed:
+                # Still haven't settled down, release locks acquired
+                # so far and go back
+                return self._abort_lock_acquisition()
+
+            lock = self._client.Lock(self._lock_path + '/' +
+                                     str(member))
+            try:
+                lock.acquire()
+            except KazooException:  # pragma: nocover
+                return self.finish()
+            self._locks.append(lock)
+
+        # All locks acquired! Time for state transition, make sure
+        # we didn't inadvertently get lost thus far
+        with self._state_change:
+            if self.failed:  # pragma: nocover
+                return self.finish()
+            self.state = PartitionState.ACQUIRED
+            self._acquire_event.set()
+
+    def _release_locks(self):
+        """Attempt to completely remove all the locks"""
+        self._acquire_event.clear()
+        for lock in self._locks[:]:
+            try:
+                lock.release()
+            except KazooException:  # pragma: nocover
+                # We proceed to remove as many as possible, and leave
+                # the ones we couldn't remove
+                pass
+            else:
+                self._locks.remove(lock)
+
+    def _abort_lock_acquisition(self):
+        """Called during lock acquisition if a party change occurs"""
+        self._partition_set = []
+        self._release_locks()
+        if self._locks:
+            # This shouldn't happen, it means we couldn't release our
+            # locks, abort
+            self._fail_out()
+            return
+        return self._child_watching(self._allocate_transition)
+
+    def _child_watching(self, func=None, async=False):
+        """Called when children are being watched to stabilize
+
+        This actually returns immediately, child watcher spins up a
+        new thread/greenlet and waits for it to stabilize before
+        any callbacks might run.
+
+        """
+        watcher = PatientChildrenWatch(self._client, self._party_path,
+                                       self._time_boundary)
+        asy = watcher.start()
+        if func is not None:
+            # We spin up the function in a separate thread/greenlet
+            # to ensure that the rawlink's it might use won't be
+            # blocked
+            if async:
+                func = partial(self._client.handler.spawn, func)
+            asy.rawlink(func)
+        return asy
+
+    def _establish_sessionwatch(self, state):
+        """Register ourself to listen for session events, we shut down
+        if we become lost"""
+        with self._state_change:
+            # Handle network partition: If connection gets suspended,
+            # change state to ALLOCATING if we had already ACQUIRED. This way
+            # the caller does not process the members since we could eventually
+            # lose session get repartitioned. If we got connected after a suspension
+            # it means we've not lost the session and still have our members. Hence,
+            # restore to ACQUIRED
+            if state == KazooState.SUSPENDED:
+                if self.state == PartitionState.ACQUIRED:
+                    self._was_allocated = True
+                    self.state = PartitionState.ALLOCATING
+            elif state == KazooState.CONNECTED:
+                if self._was_allocated:
+                    self._was_allocated = False
+                    self.state = PartitionState.ACQUIRED
+
+        if state == KazooState.LOST:
+            self._client.handler.spawn(self._fail_out)
+            return True
+
+    def _partitioner(self, identifier, members, partitions):
+        # Ensure consistent order of partitions/members
+        all_partitions = sorted(partitions)
+        workers = sorted(members)
+
+        i = workers.index(identifier)
+        # Now return the partition list starting at our location and
+        # skipping the other workers
+        return all_partitions[i::len(workers)]

+ 118 - 0
desktop/core/ext-py/kazoo-2.0/kazoo/recipe/party.py

@@ -0,0 +1,118 @@
+"""Party
+
+:Maintainer: Ben Bangert <ben@groovie.org>
+:Status: Production
+
+A Zookeeper pool of party members. The :class:`Party` object can be
+used for determining members of a party.
+
+"""
+import uuid
+
+from kazoo.exceptions import NodeExistsError, NoNodeError
+
+
+class BaseParty(object):
+    """Base implementation of a party."""
+    def __init__(self, client, path, identifier=None):
+        """
+        :param client: A :class:`~kazoo.client.KazooClient` instance.
+        :param path: The party path to use.
+        :param identifier: An identifier to use for this member of the
+                           party when participating.
+
+        """
+        self.client = client
+        self.path = path
+        self.data = str(identifier or "").encode('utf-8')
+        self.ensured_path = False
+        self.participating = False
+
+    def _ensure_parent(self):
+        if not self.ensured_path:
+            # make sure our parent node exists
+            self.client.ensure_path(self.path)
+            self.ensured_path = True
+
+    def join(self):
+        """Join the party"""
+        return self.client.retry(self._inner_join)
+
+    def _inner_join(self):
+        self._ensure_parent()
+        try:
+            self.client.create(self.create_path, self.data, ephemeral=True)
+            self.participating = True
+        except NodeExistsError:
+            # node was already created, perhaps we are recovering from a
+            # suspended connection
+            self.participating = True
+
+    def leave(self):
+        """Leave the party"""
+        self.participating = False
+        return self.client.retry(self._inner_leave)
+
+    def _inner_leave(self):
+        try:
+            self.client.delete(self.create_path)
+        except NoNodeError:
+            return False
+        return True
+
+    def __len__(self):
+        """Return a count of participating clients"""
+        self._ensure_parent()
+        return len(self._get_children())
+
+    def _get_children(self):
+        return self.client.retry(self.client.get_children, self.path)
+
+
+class Party(BaseParty):
+    """Simple pool of participating processes"""
+    _NODE_NAME = "__party__"
+
+    def __init__(self, client, path, identifier=None):
+        BaseParty.__init__(self, client, path, identifier=identifier)
+        self.node = uuid.uuid4().hex + self._NODE_NAME
+        self.create_path = self.path + "/" + self.node
+
+    def __iter__(self):
+        """Get a list of participating clients' data values"""
+        self._ensure_parent()
+        children = self._get_children()
+        for child in children:
+            try:
+                d, _ = self.client.retry(self.client.get, self.path +
+                                         "/" + child)
+                yield d.decode('utf-8')
+            except NoNodeError:  # pragma: nocover
+                pass
+
+    def _get_children(self):
+        children = BaseParty._get_children(self)
+        return [c for c in children if self._NODE_NAME in c]
+
+
+class ShallowParty(BaseParty):
+    """Simple shallow pool of participating processes
+
+    This differs from the :class:`Party` as the identifier is used in
+    the name of the party node itself, rather than the data. This
+    places some restrictions on the length as it must be a valid
+    Zookeeper node (an alphanumeric string), but reduces the overhead
+    of getting a list of participants to a single Zookeeper call.
+
+    """
+    def __init__(self, client, path, identifier=None):
+        BaseParty.__init__(self, client, path, identifier=identifier)
+        self.node = '-'.join([uuid.uuid4().hex, self.data.decode('utf-8')])
+        self.create_path = self.path + "/" + self.node
+
+    def __iter__(self):
+        """Get a list of participating clients' identifiers"""
+        self._ensure_parent()
+        children = self._get_children()
+        for child in children:
+            yield child[child.find('-') + 1:]

+ 321 - 0
desktop/core/ext-py/kazoo-2.0/kazoo/recipe/queue.py

@@ -0,0 +1,321 @@
+"""Zookeeper based queue implementations.
+
+:Maintainer: None
+:Status: Unknown
+
+"""
+
+import uuid
+from kazoo.exceptions import NoNodeError, NodeExistsError
+from kazoo.retry import ForceRetryError
+from kazoo.protocol.states import EventType
+
+
+class BaseQueue(object):
+    """A common base class for queue implementations."""
+
+    def __init__(self, client, path):
+        """
+        :param client: A :class:`~kazoo.client.KazooClient` instance.
+        :param path: The queue path to use in ZooKeeper.
+        """
+        self.client = client
+        self.path = path
+        self._entries_path = path
+        self.structure_paths = (self.path, )
+        self.ensured_path = False
+
+    def _check_put_arguments(self, value, priority=100):
+        if not isinstance(value, bytes):
+            raise TypeError("value must be a byte string")
+        if not isinstance(priority, int):
+            raise TypeError("priority must be an int")
+        elif priority < 0 or priority > 999:
+            raise ValueError("priority must be between 0 and 999")
+
+    def _ensure_paths(self):
+        if not self.ensured_path:
+            # make sure our parent / internal structure nodes exists
+            for path in self.structure_paths:
+                self.client.ensure_path(path)
+            self.ensured_path = True
+
+    def __len__(self):
+        self._ensure_paths()
+        _, stat = self.client.retry(self.client.get, self._entries_path)
+        return stat.children_count
+
+
+class Queue(BaseQueue):
+    """A distributed queue with optional priority support.
+
+    This queue does not offer reliable consumption. An entry is removed
+    from the queue prior to being processed. So if an error occurs, the
+    consumer has to re-queue the item or it will be lost.
+
+    """
+
+    prefix = "entry-"
+
+    def __init__(self, client, path):
+        """
+        :param client: A :class:`~kazoo.client.KazooClient` instance.
+        :param path: The queue path to use in ZooKeeper.
+        """
+        super(Queue, self).__init__(client, path)
+        self._children = []
+
+    def __len__(self):
+        """Return queue size."""
+        return super(Queue, self).__len__()
+
+    def get(self):
+        """
+        Get item data and remove an item from the queue.
+
+        :returns: Item data or None.
+        :rtype: bytes
+        """
+        self._ensure_paths()
+        return self.client.retry(self._inner_get)
+
+    def _inner_get(self):
+        if not self._children:
+            self._children = self.client.retry(self.client.get_children, self.path)
+            self._children = sorted(self._children)
+        if not self._children:
+            return None
+        name = self._children[0]
+        try:
+            data, stat = self.client.get(self.path + "/" + name)
+        except NoNodeError:  # pragma: nocover
+            # the first node has vanished in the meantime, try to
+            # get another one
+            raise ForceRetryError()
+        try:
+            self.client.delete(self.path + "/" + name)
+        except NoNodeError:  # pragma: nocover
+            # we were able to get the data but someone else has removed
+            # the node in the meantime. consider the item as processed
+            # by the other process
+            raise ForceRetryError()
+        self._children.pop(0)
+        return data
+
+    def put(self, value, priority=100):
+        """Put an item into the queue.
+
+        :param value: Byte string to put into the queue.
+        :param priority:
+            An optional priority as an integer with at most 3 digits.
+            Lower values signify higher priority.
+        """
+        self._check_put_arguments(value, priority)
+        self._ensure_paths()
+        path = '{path}/{prefix}{priority:03d}-'.format(
+            path=self.path, prefix=self.prefix, priority=priority)
+        self.client.create(path, value, sequence=True)
+
+
+class LockingQueue(BaseQueue):
+    """A distributed queue with priority and locking support.
+
+    Upon retrieving an entry from the queue, the entry gets locked with an
+    ephemeral node (instead of deleted). If an error occurs, this lock gets
+    released so that others could retake the entry. This adds a little penalty
+    as compared to :class:`Queue` implementation.
+
+    The user should call the :meth:`LockingQueue.get` method first to lock and
+    retrieve the next entry. When finished processing the entry, a user should
+    call the :meth:`LockingQueue.consume` method that will remove the entry
+    from the queue.
+
+    This queue will not track connection status with ZooKeeper. If a node locks
+    an element, then loses connection with ZooKeeper and later reconnects, the
+    lock will probably be removed by Zookeeper in the meantime, but a node
+    would still think that it holds a lock. The user should check the
+    connection status with Zookeeper or call :meth:`LockingQueue.holds_lock`
+    method that will check if a node still holds the lock.
+
+    .. note::
+        :class:`LockingQueue` requires ZooKeeper 3.4 or above, since it is
+        using transactions.
+    """
+    lock = "/taken"
+    entries = "/entries"
+    entry = "entry"
+
+    def __init__(self, client, path):
+        """
+        :param client: A :class:`~kazoo.client.KazooClient` instance.
+        :param path: The queue path to use in ZooKeeper.
+        """
+        super(LockingQueue, self).__init__(client, path)
+        self.id = uuid.uuid4().hex.encode()
+        self.processing_element = None
+        self._lock_path = self.path + self.lock
+        self._entries_path = self.path + self.entries
+        self.structure_paths = (self._lock_path, self._entries_path)
+
+    def __len__(self):
+        """Returns the current length of the queue.
+
+        :returns: queue size (includes locked entries count).
+        """
+        return super(LockingQueue, self).__len__()
+
+    def put(self, value, priority=100):
+        """Put an entry into the queue.
+
+        :param value: Byte string to put into the queue.
+        :param priority:
+            An optional priority as an integer with at most 3 digits.
+            Lower values signify higher priority.
+
+        """
+        self._check_put_arguments(value, priority)
+        self._ensure_paths()
+
+        self.client.create(
+            "{path}/{prefix}-{priority:03d}-".format(
+                path=self._entries_path,
+                prefix=self.entry,
+                priority=priority),
+            value, sequence=True)
+
+    def put_all(self, values, priority=100):
+        """Put several entries into the queue. The action only succeeds
+        if all entries where put into the queue.
+
+        :param values: A list of values to put into the queue.
+        :param priority:
+            An optional priority as an integer with at most 3 digits.
+            Lower values signify higher priority.
+
+        """
+        if not isinstance(values, list):
+            raise TypeError("values must be a list of byte strings")
+        if not isinstance(priority, int):
+            raise TypeError("priority must be an int")
+        elif priority < 0 or priority > 999:
+            raise ValueError("priority must be between 0 and 999")
+        self._ensure_paths()
+
+        with self.client.transaction() as transaction:
+            for value in values:
+                if not isinstance(value, bytes):
+                    raise TypeError("value must be a byte string")
+                transaction.create(
+                    "{path}/{prefix}-{priority:03d}-".format(
+                        path=self._entries_path,
+                        prefix=self.entry,
+                        priority=priority),
+                    value, sequence=True)
+
+    def get(self, timeout=None):
+        """Locks and gets an entry from the queue. If a previously got entry
+        was not consumed, this method will return that entry.
+
+        :param timeout:
+            Maximum waiting time in seconds. If None then it will wait
+            untill an entry appears in the queue.
+        :returns: A locked entry value or None if the timeout was reached.
+        :rtype: bytes
+        """
+        self._ensure_paths()
+        if not self.processing_element is None:
+            return self.processing_element[1]
+        else:
+            return self._inner_get(timeout)
+
+    def holds_lock(self):
+        """Checks if a node still holds the lock.
+
+        :returns: True if a node still holds the lock, False otherwise.
+        :rtype: bool
+        """
+        if self.processing_element is None:
+            return False
+        lock_id, _ = self.processing_element
+        lock_path = "{path}/{id}".format(path=self._lock_path, id=lock_id)
+        self.client.sync(lock_path)
+        value, stat = self.client.retry(self.client.get, lock_path)
+        return value == self.id
+
+    def consume(self):
+        """Removes a currently processing entry from the queue.
+
+        :returns: True if element was removed successfully, False otherwise.
+        :rtype: bool
+        """
+        if not self.processing_element is None and self.holds_lock:
+            id_, value = self.processing_element
+            with self.client.transaction() as transaction:
+                transaction.delete("{path}/{id}".format(
+                    path=self._entries_path,
+                    id=id_))
+                transaction.delete("{path}/{id}".format(
+                    path=self._lock_path,
+                    id=id_))
+            self.processing_element = None
+            return True
+        else:
+            return False
+
+    def _inner_get(self, timeout):
+        flag = self.client.handler.event_object()
+        lock = self.client.handler.lock_object()
+        canceled = False
+        value = []
+
+        def check_for_updates(event):
+            if not event is None and event.type != EventType.CHILD:
+                return
+            with lock:
+                if canceled or flag.isSet():
+                    return
+                values = self.client.retry(self.client.get_children,
+                    self._entries_path,
+                    check_for_updates)
+                taken = self.client.retry(self.client.get_children,
+                    self._lock_path,
+                    check_for_updates)
+                available = self._filter_locked(values, taken)
+                if len(available) > 0:
+                    ret = self._take(available[0])
+                    if not ret is None:
+                        # By this time, no one took the task
+                        value.append(ret)
+                        flag.set()
+
+        check_for_updates(None)
+        retVal = None
+        flag.wait(timeout)
+        with lock:
+            canceled = True
+            if len(value) > 0:
+                # We successfully locked an entry
+                self.processing_element = value[0]
+                retVal = value[0][1]
+        return retVal
+
+    def _filter_locked(self, values, taken):
+        taken = set(taken)
+        available = sorted(values)
+        return (available if len(taken) == 0 else
+            [x for x in available if x not in taken])
+
+    def _take(self, id_):
+        try:
+            self.client.create(
+                "{path}/{id}".format(
+                    path=self._lock_path,
+                    id=id_),
+                self.id,
+                ephemeral=True)
+            value, stat = self.client.retry(self.client.get,
+                "{path}/{id}".format(path=self._entries_path, id=id_))
+        except (NoNodeError, NodeExistsError):
+            # Item is already consumed or locked
+            return None
+        return (id_, value)

+ 419 - 0
desktop/core/ext-py/kazoo-2.0/kazoo/recipe/watchers.py

@@ -0,0 +1,419 @@
+"""Higher level child and data watching API's.
+
+:Maintainer: Ben Bangert <ben@groovie.org>
+:Status: Production
+
+.. note::
+
+    :ref:`DataWatch` and :ref:`ChildrenWatch` may only handle a single
+    function, attempts to associate a single instance with multiple functions
+    will result in an exception being thrown.
+
+"""
+import logging
+import time
+import warnings
+from functools import partial, wraps
+
+from kazoo.retry import KazooRetry
+from kazoo.exceptions import (
+    ConnectionClosedError,
+    NoNodeError,
+    KazooException
+)
+from kazoo.protocol.states import KazooState
+
+log = logging.getLogger(__name__)
+
+
+_STOP_WATCHING = object()
+
+
+def _ignore_closed(func):
+    @wraps(func)
+    def wrapper(*args, **kwargs):
+        try:
+            return func(*args, **kwargs)
+        except ConnectionClosedError:
+            pass
+    return wrapper
+
+
+class DataWatch(object):
+    """Watches a node for data updates and calls the specified
+    function each time it changes
+
+    The function will also be called the very first time its
+    registered to get the data.
+
+    Returning `False` from the registered function will disable future
+    data change calls. If the client connection is closed (using the
+    close command), the DataWatch will no longer get updates.
+
+    If the function supplied takes three arguments, then the third one
+    will be a :class:`~kazoo.protocol.states.WatchedEvent`. It will
+    only be set if the change to the data occurs as a result of the
+    server notifying the watch that there has been a change. Events
+    like reconnection or the first call will not include an event.
+
+    If the node does not exist, then the function will be called with
+    ``None`` for all values.
+
+    .. tip::
+
+        Because :class:`DataWatch` can watch nodes that don't exist, it
+        can be used alternatively as a higher-level Exists watcher that
+        survives reconnections and session loss.
+
+    Example with client:
+
+    .. code-block:: python
+
+        @client.DataWatch('/path/to/watch')
+        def my_func(data, stat):
+            print("Data is %s" % data)
+            print("Version is %s" % stat.version)
+
+        # Above function is called immediately and prints
+
+        # Or if you want the event object
+        @client.DataWatch('/path/to/watch')
+        def my_func(data, stat, event):
+            print("Data is %s" % data)
+            print("Version is %s" % stat.version)
+            print("Event is %s" % event)
+
+    .. versionchanged:: 1.2
+
+        DataWatch now ignores additional arguments that were previously
+        passed to it and warns that they are no longer respected.
+
+    """
+    def __init__(self, client, path, func=None, *args, **kwargs):
+        """Create a data watcher for a path
+
+        :param client: A zookeeper client.
+        :type client: :class:`~kazoo.client.KazooClient`
+        :param path: The path to watch for data changes on.
+        :type path: str
+        :param func: Function to call initially and every time the
+                     node changes. `func` will be called with a
+                     tuple, the value of the node and a
+                     :class:`~kazoo.client.ZnodeStat` instance.
+        :type func: callable
+
+        """
+        self._client = client
+        self._path = path
+        self._func = func
+        self._stopped = False
+        self._run_lock = client.handler.lock_object()
+        self._version = None
+        self._retry = KazooRetry(max_tries=None,
+            sleep_func=client.handler.sleep_func)
+        self._include_event = None
+        self._ever_called = False
+        self._used = False
+
+        if args or kwargs:
+            warnings.warn('Passing additional arguments to DataWatch is'
+                          ' deprecated. ignore_missing_node is now assumed '
+                          ' to be True by default, and the event will be '
+                          ' sent if the function can handle receiving it',
+                          DeprecationWarning, stacklevel=2)
+
+        # Register our session listener if we're going to resume
+        # across session losses
+        if func is not None:
+            self._used = True
+            self._client.add_listener(self._session_watcher)
+            self._get_data()
+
+    def __call__(self, func):
+        """Callable version for use as a decorator
+
+        :param func: Function to call initially and every time the
+                     data changes. `func` will be called with a
+                     tuple, the value of the node and a
+                     :class:`~kazoo.client.ZnodeStat` instance.
+        :type func: callable
+
+        """
+        if self._used:
+            raise KazooException(
+                "A function has already been associated with this "
+                "DataWatch instance.")
+
+        self._func = func
+
+        self._used = True
+        self._client.add_listener(self._session_watcher)
+        self._get_data()
+        return func
+
+    def _log_func_exception(self, data, stat, event=None):
+        try:
+            # For backwards compatibility, don't send event to the
+            # callback unless the send_event is set in constructor
+            if not self._ever_called:
+                self._ever_called = True
+            try:
+                result = self._func(data, stat, event)
+            except TypeError:
+                result = self._func(data, stat)
+            if result is False:
+                self._stopped = True
+                self._client.remove_listener(self._session_watcher)
+        except Exception as exc:
+            log.exception(exc)
+            raise
+
+    @_ignore_closed
+    def _get_data(self, event=None):
+        # Ensure this runs one at a time, possible because the session
+        # watcher may trigger a run
+        with self._run_lock:
+            if self._stopped:
+                return
+
+            initial_version = self._version
+
+            try:
+                data, stat = self._retry(self._client.get,
+                                         self._path, self._watcher)
+            except NoNodeError:
+                data = None
+
+                # This will set 'stat' to None if the node does not yet
+                # exist.
+                stat = self._retry(self._client.exists, self._path,
+                                   self._watcher)
+                if stat:
+                    self._client.handler.spawn(self._get_data)
+                    return
+
+            # No node data, clear out version
+            if stat is None:
+                self._version = None
+            else:
+                self._version = stat.mzxid
+
+            # Call our function if its the first time ever, or if the
+            # version has changed
+            if initial_version != self._version or not self._ever_called:
+                self._log_func_exception(data, stat, event)
+
+    def _watcher(self, event):
+        self._get_data(event=event)
+
+    def _set_watch(self, state):
+        with self._run_lock:
+            self._watch_established = state
+
+    def _session_watcher(self, state):
+        if state == KazooState.CONNECTED:
+            self._client.handler.spawn(self._get_data)
+
+
+class ChildrenWatch(object):
+    """Watches a node for children updates and calls the specified
+    function each time it changes
+
+    The function will also be called the very first time its
+    registered to get children.
+
+    Returning `False` from the registered function will disable future
+    children change calls. If the client connection is closed (using
+    the close command), the ChildrenWatch will no longer get updates.
+
+    if send_event=True in __init__, then the function will always be
+    called with second parameter, ``event``. Upon initial call or when
+    recovering a lost session the ``event`` is always ``None``.
+    Otherwise it's a :class:`~kazoo.prototype.state.WatchedEvent`
+    instance.
+
+    Example with client:
+
+    .. code-block:: python
+
+        @client.ChildrenWatch('/path/to/watch')
+        def my_func(children):
+            print "Children are %s" % children
+
+        # Above function is called immediately and prints children
+
+    """
+    def __init__(self, client, path, func=None,
+                 allow_session_lost=True, send_event=False):
+        """Create a children watcher for a path
+
+        :param client: A zookeeper client.
+        :type client: :class:`~kazoo.client.KazooClient`
+        :param path: The path to watch for children on.
+        :type path: str
+        :param func: Function to call initially and every time the
+                     children change. `func` will be called with a
+                     single argument, the list of children.
+        :type func: callable
+        :param allow_session_lost: Whether the watch should be
+                                   re-registered if the zookeeper
+                                   session is lost.
+        :type allow_session_lost: bool
+        :type send_event: bool
+        :param send_event: Whether the function should be passed the
+                           event sent by ZooKeeper or None upon
+                           initialization (see class documentation)
+
+        The path must already exist for the children watcher to
+        run.
+
+        """
+        self._client = client
+        self._path = path
+        self._func = func
+        self._send_event = send_event
+        self._stopped = False
+        self._watch_established = False
+        self._allow_session_lost = allow_session_lost
+        self._run_lock = client.handler.lock_object()
+        self._prior_children = None
+        self._used = False
+
+        # Register our session listener if we're going to resume
+        # across session losses
+        if func is not None:
+            self._used = True
+            if allow_session_lost:
+                self._client.add_listener(self._session_watcher)
+            self._get_children()
+
+    def __call__(self, func):
+        """Callable version for use as a decorator
+
+        :param func: Function to call initially and every time the
+                     children change. `func` will be called with a
+                     single argument, the list of children.
+        :type func: callable
+
+        """
+        if self._used:
+            raise KazooException(
+                "A function has already been associated with this "
+                "ChildrenWatch instance.")
+
+        self._func = func
+
+        self._used = True
+        if self._allow_session_lost:
+            self._client.add_listener(self._session_watcher)
+        self._get_children()
+        return func
+
+    @_ignore_closed
+    def _get_children(self, event=None):
+        with self._run_lock:  # Ensure this runs one at a time
+            if self._stopped:
+                return
+
+            children = self._client.retry(self._client.get_children,
+                                          self._path, self._watcher)
+            if not self._watch_established:
+                self._watch_established = True
+
+                if self._prior_children is not None and \
+                   self._prior_children == children:
+                    return
+
+            self._prior_children = children
+
+            try:
+                if self._send_event:
+                    result = self._func(children, event)
+                else:
+                    result = self._func(children)
+                if result is False:
+                    self._stopped = True
+            except Exception as exc:
+                log.exception(exc)
+                raise
+
+    def _watcher(self, event):
+        self._get_children(event)
+
+    def _session_watcher(self, state):
+        if state in (KazooState.LOST, KazooState.SUSPENDED):
+            self._watch_established = False
+        elif state == KazooState.CONNECTED and \
+             not self._watch_established and not self._stopped:
+            self._client.handler.spawn(self._get_children)
+
+
+class PatientChildrenWatch(object):
+    """Patient Children Watch that returns values after the children
+    of a node don't change for a period of time
+
+    A separate watcher for the children of a node, that ignores
+    changes within a boundary time and sets the result only when the
+    boundary time has elapsed with no children changes.
+
+    Example::
+
+        watcher = PatientChildrenWatch(client, '/some/path',
+                                       time_boundary=5)
+        async_object = watcher.start()
+
+        # Blocks until the children have not changed for time boundary
+        # (5 in this case) seconds, returns children list and an
+        # async_result that will be set if the children change in the
+        # future
+        children, child_async = async_object.get()
+
+    .. note::
+
+        This Watch is different from :class:`DataWatch` and
+        :class:`ChildrenWatch` as it only returns once, does not take
+        a function that is called, and provides an
+        :class:`~kazoo.interfaces.IAsyncResult` object that can be
+        checked to see if the children have changed later.
+
+    """
+    def __init__(self, client, path, time_boundary=30):
+        self.client = client
+        self.path = path
+        self.children = []
+        self.time_boundary = time_boundary
+        self.children_changed = client.handler.event_object()
+
+    def start(self):
+        """Begin the watching process asynchronously
+
+        :returns: An :class:`~kazoo.interfaces.IAsyncResult` instance
+                  that will be set when no change has occurred to the
+                  children for time boundary seconds.
+
+        """
+        self.asy = asy = self.client.handler.async_result()
+        self.client.handler.spawn(self._inner_start)
+        return asy
+
+    def _inner_start(self):
+        try:
+            while True:
+                async_result = self.client.handler.async_result()
+                self.children = self.client.retry(
+                    self.client.get_children, self.path,
+                    partial(self._children_watcher, async_result))
+                self.client.handler.sleep_func(self.time_boundary)
+
+                if self.children_changed.is_set():
+                    self.children_changed.clear()
+                else:
+                    break
+
+            self.asy.set((self.children, async_result))
+        except Exception as exc:
+            self.asy.set_exception(exc)
+
+    def _children_watcher(self, async, event):
+        self.children_changed.set()
+        async.set(time.time())

+ 150 - 0
desktop/core/ext-py/kazoo-2.0/kazoo/retry.py

@@ -0,0 +1,150 @@
+import logging
+import random
+import time
+
+from kazoo.exceptions import (
+    ConnectionClosedError,
+    ConnectionLoss,
+    KazooException,
+    OperationTimeoutError,
+    SessionExpiredError,
+)
+
+log = logging.getLogger(__name__)
+
+
+class ForceRetryError(Exception):
+    """Raised when some recipe logic wants to force a retry."""
+
+
+class RetryFailedError(KazooException):
+    """Raised when retrying an operation ultimately failed, after
+    retrying the maximum number of attempts.
+    """
+
+
+class InterruptedError(RetryFailedError):
+    """Raised when the retry is forcibly interrupted by the interrupt
+    function"""
+
+
+class KazooRetry(object):
+    """Helper for retrying a method in the face of retry-able
+    exceptions"""
+    RETRY_EXCEPTIONS = (
+        ConnectionLoss,
+        OperationTimeoutError,
+        ForceRetryError
+    )
+
+    EXPIRED_EXCEPTIONS = (
+        SessionExpiredError,
+    )
+
+    def __init__(self, max_tries=1, delay=0.1, backoff=2, max_jitter=0.8,
+                 max_delay=3600, ignore_expire=True, sleep_func=time.sleep,
+                 deadline=None, interrupt=None):
+        """Create a :class:`KazooRetry` instance for retrying function
+        calls
+
+        :param max_tries: How many times to retry the command. -1 means
+                          infinite tries.
+        :param delay: Initial delay between retry attempts.
+        :param backoff: Backoff multiplier between retry attempts.
+                        Defaults to 2 for exponential backoff.
+        :param max_jitter: Additional max jitter period to wait between
+                           retry attempts to avoid slamming the server.
+        :param max_delay: Maximum delay in seconds, regardless of other
+                          backoff settings. Defaults to one hour.
+        :param ignore_expire:
+            Whether a session expiration should be ignored and treated
+            as a retry-able command.
+        :param interrupt:
+            Function that will be called with no args that may return
+            True if the retry should be ceased immediately. This will
+            be called no more than every 0.1 seconds during a wait
+            between retries.
+
+        """
+        self.max_tries = max_tries
+        self.delay = delay
+        self.backoff = backoff
+        self.max_jitter = int(max_jitter * 100)
+        self.max_delay = float(max_delay)
+        self._attempts = 0
+        self._cur_delay = delay
+        self.deadline = deadline
+        self._cur_stoptime = None
+        self.sleep_func = sleep_func
+        self.retry_exceptions = self.RETRY_EXCEPTIONS
+        self.interrupt = interrupt
+        if ignore_expire:
+            self.retry_exceptions += self.EXPIRED_EXCEPTIONS
+
+    def reset(self):
+        """Reset the attempt counter"""
+        self._attempts = 0
+        self._cur_delay = self.delay
+        self._cur_stoptime = None
+
+    def copy(self):
+        """Return a clone of this retry manager"""
+        obj = KazooRetry(max_tries=self.max_tries,
+                         delay=self.delay,
+                         backoff=self.backoff,
+                         max_jitter=self.max_jitter / 100.0,
+                         max_delay=self.max_delay,
+                         sleep_func=self.sleep_func,
+                         deadline=self.deadline,
+                         interrupt=self.interrupt)
+        obj.retry_exceptions = self.retry_exceptions
+        return obj
+
+    def __call__(self, func, *args, **kwargs):
+        """Call a function with arguments until it completes without
+        throwing a Kazoo exception
+
+        :param func: Function to call
+        :param args: Positional arguments to call the function with
+        :params kwargs: Keyword arguments to call the function with
+
+        The function will be called until it doesn't throw one of the
+        retryable exceptions (ConnectionLoss, OperationTimeout, or
+        ForceRetryError), and optionally retrying on session
+        expiration.
+
+        """
+        self.reset()
+
+        while True:
+            try:
+                if self.deadline is not None and self._cur_stoptime is None:
+                    self._cur_stoptime = time.time() + self.deadline
+                return func(*args, **kwargs)
+            except ConnectionClosedError:
+                raise
+            except self.retry_exceptions:
+                # Note: max_tries == -1 means infinite tries.
+                if self._attempts == self.max_tries:
+                    raise RetryFailedError("Too many retry attempts")
+                self._attempts += 1
+                sleeptime = self._cur_delay + (random.randint(0, self.max_jitter) / 100.0)
+
+                if self._cur_stoptime is not None and time.time() + sleeptime >= self._cur_stoptime:
+                    raise RetryFailedError("Exceeded retry deadline")
+
+                if self.interrupt:
+                    while sleeptime > 0:
+                        # Break the time period down and sleep for no longer than
+                        # 0.1 before calling the interrupt
+                        if sleeptime < 0.1:
+                            self.sleep_func(sleeptime)
+                            sleeptime -= sleeptime
+                        else:
+                            self.sleep_func(0.1)
+                            sleeptime -= 0.1
+                        if self.interrupt():
+                            raise InterruptedError()
+                else:
+                    self.sleep_func(sleeptime)
+                self._cur_delay = min(self._cur_delay * self.backoff, self.max_delay)

+ 138 - 0
desktop/core/ext-py/kazoo-2.0/kazoo/security.py

@@ -0,0 +1,138 @@
+"""Kazoo Security"""
+from base64 import b64encode
+from collections import namedtuple
+import hashlib
+
+
+# Represents a Zookeeper ID and ACL object
+Id = namedtuple('Id', 'scheme id')
+
+
+class ACL(namedtuple('ACL', 'perms id')):
+    """An ACL for a Zookeeper Node
+
+    An ACL object is created by using an :class:`Id` object along with
+    a :class:`Permissions` setting. For convenience,
+    :meth:`make_digest_acl` should be used to create an ACL object with
+    the desired scheme, id, and permissions.
+
+    """
+    @property
+    def acl_list(self):
+        perms = []
+        if self.perms & Permissions.ALL == Permissions.ALL:
+            perms.append('ALL')
+            return perms
+        if self.perms & Permissions.READ == Permissions.READ:
+            perms.append('READ')
+        if self.perms & Permissions.WRITE == Permissions.WRITE:
+            perms.append('WRITE')
+        if self.perms & Permissions.CREATE == Permissions.CREATE:
+            perms.append('CREATE')
+        if self.perms & Permissions.DELETE == Permissions.DELETE:
+            perms.append('DELETE')
+        if self.perms & Permissions.ADMIN == Permissions.ADMIN:
+            perms.append('ADMIN')
+        return perms
+
+    def __repr__(self):
+        return 'ACL(perms=%r, acl_list=%s, id=%r)' % (
+            self.perms, self.acl_list, self.id)
+
+
+class Permissions(object):
+    READ = 1
+    WRITE = 2
+    CREATE = 4
+    DELETE = 8
+    ADMIN = 16
+    ALL = 31
+
+
+# Shortcuts for common Ids
+ANYONE_ID_UNSAFE = Id('world', 'anyone')
+AUTH_IDS = Id('auth', '')
+
+# Shortcuts for common ACLs
+OPEN_ACL_UNSAFE = [ACL(Permissions.ALL, ANYONE_ID_UNSAFE)]
+CREATOR_ALL_ACL = [ACL(Permissions.ALL, AUTH_IDS)]
+READ_ACL_UNSAFE = [ACL(Permissions.READ, ANYONE_ID_UNSAFE)]
+
+
+def make_digest_acl_credential(username, password):
+    """Create a SHA1 digest credential"""
+    credential = username.encode('utf-8') + b":" + password.encode('utf-8')
+    cred_hash = b64encode(hashlib.sha1(credential).digest()).strip()
+    return username + ":" + cred_hash.decode('utf-8')
+
+
+def make_acl(scheme, credential, read=False, write=False,
+             create=False, delete=False, admin=False, all=False):
+    """Given a scheme and credential, return an :class:`ACL` object
+    appropriate for use with Kazoo.
+
+    :param scheme: The scheme to use. I.e. `digest`.
+    :param credential:
+        A colon separated username, password. The password should be
+        hashed with the `scheme` specified. The
+        :meth:`make_digest_acl_credential` method will create and
+        return a credential appropriate for use with the `digest`
+        scheme.
+    :param write: Write permission.
+    :type write: bool
+    :param create: Create permission.
+    :type create: bool
+    :param delete: Delete permission.
+    :type delete: bool
+    :param admin: Admin permission.
+    :type admin: bool
+    :param all: All permissions.
+    :type all: bool
+
+    :rtype: :class:`ACL`
+
+    """
+    if all:
+        permissions = Permissions.ALL
+    else:
+        permissions = 0
+        if read:
+            permissions |= Permissions.READ
+        if write:
+            permissions |= Permissions.WRITE
+        if create:
+            permissions |= Permissions.CREATE
+        if delete:
+            permissions |= Permissions.DELETE
+        if admin:
+            permissions |= Permissions.ADMIN
+    return ACL(permissions, Id(scheme, credential))
+
+
+def make_digest_acl(username, password, read=False, write=False,
+                    create=False, delete=False, admin=False, all=False):
+    """Create a digest ACL for Zookeeper with the given permissions
+
+    This method combines :meth:`make_digest_acl_credential` and
+    :meth:`make_acl` to create an :class:`ACL` object appropriate for
+    use with Kazoo's ACL methods.
+
+    :param username: Username to use for the ACL.
+    :param password: A plain-text password to hash.
+    :param write: Write permission.
+    :type write: bool
+    :param create: Create permission.
+    :type create: bool
+    :param delete: Delete permission.
+    :type delete: bool
+    :param admin: Admin permission.
+    :type admin: bool
+    :param all: All permissions.
+    :type all: bool
+
+    :rtype: :class:`ACL`
+
+    """
+    cred = make_digest_acl_credential(username, password)
+    return make_acl("digest", cred, read=read, write=write, create=create,
+        delete=delete, admin=admin, all=all)

+ 5 - 0
desktop/core/ext-py/kazoo-2.0/kazoo/testing/__init__.py

@@ -0,0 +1,5 @@
+from kazoo.testing.harness import KazooTestCase
+from kazoo.testing.harness import KazooTestHarness
+
+
+__all__ = ('KazooTestHarness', 'KazooTestCase', )

+ 283 - 0
desktop/core/ext-py/kazoo-2.0/kazoo/testing/common.py

@@ -0,0 +1,283 @@
+#
+#  Copyright (C) 2010-2011, 2011 Canonical Ltd. All Rights Reserved
+#
+#  This file was originally taken from txzookeeper and modified later.
+#
+#  Authors:
+#   Kapil Thangavelu and the Kazoo team
+#
+#  txzookeeper is free software: you can redistribute it and/or modify
+#  it under the terms of the GNU Lesser General Public License as published by
+#  the Free Software Foundation, either version 3 of the License, or
+#  (at your option) any later version.
+#
+#  txzookeeper is distributed in the hope that it will be useful,
+#  but WITHOUT ANY WARRANTY; without even the implied warranty of
+#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#  GNU Lesser General Public License for more details.
+#
+#  You should have received a copy of the GNU Lesser General Public License
+#  along with txzookeeper.  If not, see <http://www.gnu.org/licenses/>.
+
+
+import code
+import os
+import os.path
+import shutil
+import signal
+import subprocess
+import tempfile
+import traceback
+
+from itertools import chain
+from collections import namedtuple
+from glob import glob
+
+
+def debug(sig, frame):
+    """Interrupt running process, and provide a python prompt for
+    interactive debugging."""
+    d = {'_frame': frame}         # Allow access to frame object.
+    d.update(frame.f_globals)  # Unless shadowed by global
+    d.update(frame.f_locals)
+
+    i = code.InteractiveConsole(d)
+    message = "Signal recieved : entering python shell.\nTraceback:\n"
+    message += ''.join(traceback.format_stack(frame))
+    i.interact(message)
+
+
+def listen():
+    if os.name != 'nt':  # SIGUSR1 is not supported on Windows
+        signal.signal(signal.SIGUSR1, debug)  # Register handler
+listen()
+
+
+def to_java_compatible_path(path):
+    if os.name == 'nt':
+        path = path.replace('\\', '/')
+    return path
+
+ServerInfo = namedtuple(
+    "ServerInfo", "server_id client_port election_port leader_port")
+
+
+class ManagedZooKeeper(object):
+    """Class to manage the running of a ZooKeeper instance for testing.
+
+    Note: no attempt is made to probe the ZooKeeper instance is
+    actually available, or that the selected port is free. In the
+    future, we may want to do that, especially when run in a
+    Hudson/Buildbot context, to ensure more test robustness."""
+
+    def __init__(self, software_path, server_info, peers=(), classpath=None):
+        """Define the ZooKeeper test instance.
+
+        @param install_path: The path to the install for ZK
+        @param port: The port to run the managed ZK instance
+        """
+        self.install_path = software_path
+        self._classpath = classpath
+        self.server_info = server_info
+        self.host = "127.0.0.1"
+        self.peers = peers
+        self.working_path = tempfile.mkdtemp()
+        self._running = False
+
+    def run(self):
+        """Run the ZooKeeper instance under a temporary directory.
+
+        Writes ZK log messages to zookeeper.log in the current directory.
+        """
+        if self.running:
+            return
+        config_path = os.path.join(self.working_path, "zoo.cfg")
+        log_path = os.path.join(self.working_path, "log")
+        log4j_path = os.path.join(self.working_path, "log4j.properties")
+        data_path = os.path.join(self.working_path, "data")
+
+        # various setup steps
+        if not os.path.exists(self.working_path):
+            os.mkdir(self.working_path)
+        if not os.path.exists(log_path):
+            os.mkdir(log_path)
+        if not os.path.exists(data_path):
+            os.mkdir(data_path)
+
+        with open(config_path, "w") as config:
+            config.write("""
+tickTime=2000
+dataDir=%s
+clientPort=%s
+maxClientCnxns=0
+""" % (to_java_compatible_path(data_path), self.server_info.client_port))
+
+        # setup a replicated setup if peers are specified
+        if self.peers:
+            servers_cfg = []
+            for p in chain((self.server_info,), self.peers):
+                servers_cfg.append("server.%s=localhost:%s:%s" % (
+                    p.server_id, p.leader_port, p.election_port))
+
+            with open(config_path, "a") as config:
+                config.write("""
+initLimit=4
+syncLimit=2
+%s
+""" % ("\n".join(servers_cfg)))
+
+        # Write server ids into datadir
+        with open(os.path.join(data_path, "myid"), "w") as myid_file:
+            myid_file.write(str(self.server_info.server_id))
+
+        with open(log4j_path, "w") as log4j:
+            log4j.write("""
+# DEFAULT: console appender only
+log4j.rootLogger=INFO, ROLLINGFILE
+log4j.appender.ROLLINGFILE.layout=org.apache.log4j.PatternLayout
+log4j.appender.ROLLINGFILE.layout.ConversionPattern=%d{ISO8601} [myid:%X{myid}] - %-5p [%t:%C{1}@%L] - %m%n
+log4j.appender.ROLLINGFILE=org.apache.log4j.RollingFileAppender
+log4j.appender.ROLLINGFILE.Threshold=DEBUG
+log4j.appender.ROLLINGFILE.File=""" + to_java_compatible_path(
+                self.working_path + os.sep + "zookeeper.log\n"))
+
+        self.process = subprocess.Popen(
+            args=["java",
+                  "-cp", self.classpath,
+                  "-Dreadonlymode.enabled=true",
+                  "-Dzookeeper.log.dir=%s" % log_path,
+                  "-Dzookeeper.root.logger=INFO,CONSOLE",
+                  "-Dlog4j.configuration=file:%s" % log4j_path,
+                  # "-Dlog4j.debug",
+                  "org.apache.zookeeper.server.quorum.QuorumPeerMain",
+                  config_path])
+        self._running = True
+
+    @property
+    def classpath(self):
+        """Get the classpath necessary to run ZooKeeper."""
+
+        if self._classpath:
+            return self._classpath
+
+        # Two possibilities, as seen in zkEnv.sh:
+        # Check for a release - top-level zookeeper-*.jar?
+        jars = glob((os.path.join(
+            self.install_path, 'zookeeper-*.jar')))
+        if jars:
+            # Release build (`ant package`)
+            jars.extend(glob(os.path.join(
+                self.install_path,
+                "lib/*.jar")))
+            # support for different file locations on Debian/Ubuntu
+            jars.extend(glob(os.path.join(
+                self.install_path,
+                "log4j-*.jar")))
+            jars.extend(glob(os.path.join(
+                self.install_path,
+                "slf4j-api-*.jar")))
+            jars.extend(glob(os.path.join(
+                self.install_path,
+                "slf4j-log4j-*.jar")))
+        else:
+            # Development build (plain `ant`)
+            jars = glob((os.path.join(
+                self.install_path, 'build/zookeeper-*.jar')))
+            jars.extend(glob(os.path.join(
+                self.install_path,
+                "build/lib/*.jar")))
+
+        return os.pathsep.join(jars)
+
+    @property
+    def address(self):
+        """Get the address of the ZooKeeper instance."""
+        return "%s:%s" % (self.host, self.client_port)
+
+    @property
+    def running(self):
+        return self._running
+
+    @property
+    def client_port(self):
+        return self.server_info.client_port
+
+    def reset(self):
+        """Stop the zookeeper instance, cleaning out its on disk-data."""
+        self.stop()
+        shutil.rmtree(os.path.join(self.working_path, "data"))
+        os.mkdir(os.path.join(self.working_path, "data"))
+        with open(os.path.join(self.working_path, "data", "myid"), "w") as fh:
+            fh.write(str(self.server_info.server_id))
+
+    def stop(self):
+        """Stop the Zookeeper instance, retaining on disk state."""
+        if not self.running:
+            return
+        self.process.terminate()
+        self.process.wait()
+        self._running = False
+
+    def destroy(self):
+        """Stop the ZooKeeper instance and destroy its on disk-state"""
+        # called by at exit handler, reimport to avoid cleanup race.
+        import shutil
+        self.stop()
+
+        shutil.rmtree(self.working_path)
+
+
+class ZookeeperCluster(object):
+
+    def __init__(self, install_path=None, classpath=None, size=3, port_offset=20000):
+        self._install_path = install_path
+        self._classpath = classpath
+        self._servers = []
+
+        # Calculate ports and peer group
+        port = port_offset
+        peers = []
+
+        for i in range(size):
+            info = ServerInfo(i + 1, port, port + 1, port + 2)
+            peers.append(info)
+            port += 10
+
+        # Instantiate Managed ZK Servers
+        for i in range(size):
+            server_peers = list(peers)
+            server_info = server_peers.pop(i)
+            self._servers.append(
+                ManagedZooKeeper(
+                    self._install_path, server_info, server_peers, classpath=self._classpath))
+
+    def __getitem__(self, k):
+        return self._servers[k]
+
+    def __iter__(self):
+        return iter(self._servers)
+
+    def start(self):
+        # Zookeeper client expresses a preference for either lower ports or
+        # lexicographical ordering of hosts, to ensure that all servers have a
+        # chance to startup, start them in reverse order.
+        for server in reversed(list(self)):
+            server.run()
+        # Giving the servers a moment to start, decreases the overall time
+        # required for a client to successfully connect (2s vs. 4s without
+        # the sleep).
+        import time
+        time.sleep(2)
+
+    def stop(self):
+        for server in self:
+            server.stop()
+        self._servers = []
+
+    def terminate(self):
+        for server in self:
+            server.destroy()
+
+    def reset(self):
+        for server in self:
+            server.reset()

+ 180 - 0
desktop/core/ext-py/kazoo-2.0/kazoo/testing/harness.py

@@ -0,0 +1,180 @@
+"""Kazoo testing harnesses"""
+import atexit
+import logging
+import os
+import uuid
+import threading
+import unittest
+
+from kazoo.client import KazooClient
+from kazoo.exceptions import NotEmptyError
+from kazoo.protocol.states import (
+    KazooState
+)
+from kazoo.testing.common import ZookeeperCluster
+from kazoo.protocol.connection import _SESSION_EXPIRED
+
+log = logging.getLogger(__name__)
+
+CLUSTER = None
+
+
+def get_global_cluster():
+    global CLUSTER
+    if CLUSTER is None:
+        ZK_HOME = os.environ.get("ZOOKEEPER_PATH")
+        ZK_CLASSPATH = os.environ.get("ZOOKEEPER_CLASSPATH")
+        ZK_PORT_OFFSET = int(os.environ.get("ZOOKEEPER_PORT_OFFSET", 20000))
+
+        assert ZK_HOME or ZK_CLASSPATH, (
+            "either ZOOKEEPER_PATH or ZOOKEEPER_CLASSPATH environment variable "
+            "must be defined.\n"
+            "For deb package installations this is /usr/share/java")
+
+        CLUSTER = ZookeeperCluster(
+            install_path=ZK_HOME,
+            classpath=ZK_CLASSPATH,
+            port_offset=ZK_PORT_OFFSET,
+        )
+        atexit.register(lambda cluster: cluster.terminate(), CLUSTER)
+    return CLUSTER
+
+
+class KazooTestHarness(unittest.TestCase):
+    """Harness for testing code that uses Kazoo
+
+    This object can be used directly or as a mixin. It supports starting
+    and stopping a complete ZooKeeper cluster locally and provides an
+    API for simulating errors and expiring sessions.
+
+    Example::
+
+        class MyTestCase(KazooTestHarness):
+            def setUp(self):
+                self.setup_zookeeper()
+
+                # additional test setup
+
+            def tearDown(self):
+                self.teardown_zookeeper()
+
+            def test_something(self):
+                something_that_needs_a_kazoo_client(self.client)
+
+            def test_something_else(self):
+                something_that_needs_zk_servers(self.servers)
+
+    """
+
+    def __init__(self, *args, **kw):
+        super(KazooTestHarness, self).__init__(*args, **kw)
+        self.client = None
+        self._clients = []
+
+    @property
+    def cluster(self):
+        return get_global_cluster()
+
+    @property
+    def servers(self):
+        return ",".join([s.address for s in self.cluster])
+
+    def _get_nonchroot_client(self):
+        return KazooClient(self.servers)
+
+    def _get_client(self, **kwargs):
+        c = KazooClient(self.hosts, **kwargs)
+        try:
+            self._clients.append(c)
+        except AttributeError:
+            self._client = [c]
+        return c
+
+    def expire_session(self, client_id=None):
+        """Force ZK to expire a client session
+
+        :param client_id: id of client to expire. If unspecified, the id of
+                          self.client will be used.
+
+        """
+        client_id = client_id or self.client.client_id
+
+        lost = threading.Event()
+        safe = threading.Event()
+
+        def watch_loss(state):
+            if state == KazooState.LOST:
+                lost.set()
+            if lost.is_set() and state == KazooState.CONNECTED:
+                safe.set()
+                return True
+
+        self.client.add_listener(watch_loss)
+
+        self.client._call(_SESSION_EXPIRED, None)
+
+        lost.wait(5)
+        if not lost.isSet():
+            raise Exception("Failed to get notified of session loss")
+
+        # Wait for the reconnect now
+        safe.wait(15)
+        if not safe.isSet():
+            raise Exception("Failed to see client reconnect")
+        self.client.retry(self.client.get_async, '/')
+
+    def setup_zookeeper(self, **client_options):
+        """Create a ZK cluster and chrooted :class:`KazooClient`
+
+        The cluster will only be created on the first invocation and won't be
+        fully torn down until exit.
+        """
+        if not self.cluster[0].running:
+            self.cluster.start()
+        namespace = "/kazootests" + uuid.uuid4().hex
+        self.hosts = self.servers + namespace
+
+        if 'timeout' not in client_options:
+            client_options['timeout'] = 0.8
+        self.client = self._get_client(**client_options)
+        self.client.start()
+        self.client.ensure_path("/")
+
+    def teardown_zookeeper(self):
+        """Clean up any ZNodes created during the test
+        """
+        if not self.cluster[0].running:
+            self.cluster.start()
+
+        tries = 0
+        if self.client and self.client.connected:
+            while tries < 3:
+                try:
+                    self.client.retry(self.client.delete, '/', recursive=True)
+                    break
+                except NotEmptyError:
+                    pass
+                tries += 1
+            self.client.stop()
+            self.client.close()
+            del self.client
+        else:
+            client = self._get_client()
+            client.start()
+            client.retry(client.delete, '/', recursive=True)
+            client.stop()
+            client.close()
+            del client
+
+        for client in self._clients:
+            client.stop()
+            del client
+        self._clients = None
+
+
+class KazooTestCase(KazooTestHarness):
+    def setUp(self):
+        self.setup_zookeeper()
+
+    def tearDown(self):
+        self.teardown_zookeeper()

+ 0 - 0
desktop/core/ext-py/kazoo-2.0/kazoo/tests/__init__.py


+ 157 - 0
desktop/core/ext-py/kazoo-2.0/kazoo/tests/test_barrier.py

@@ -0,0 +1,157 @@
+import threading
+
+from nose.tools import eq_
+
+from kazoo.testing import KazooTestCase
+
+
+class KazooBarrierTests(KazooTestCase):
+    def test_barrier_not_exist(self):
+        b = self.client.Barrier("/some/path")
+        eq_(b.wait(), True)
+
+    def test_barrier_exists(self):
+        b = self.client.Barrier("/some/path")
+        b.create()
+        eq_(b.wait(0), False)
+        b.remove()
+        eq_(b.wait(), True)
+
+    def test_remove_nonexistent_barrier(self):
+        b = self.client.Barrier("/some/path")
+        eq_(b.remove(), False)
+
+
+class KazooDoubleBarrierTests(KazooTestCase):
+
+    def test_basic_barrier(self):
+        b = self.client.DoubleBarrier("/some/path", 1)
+        eq_(b.participating, False)
+        b.enter()
+        eq_(b.participating, True)
+        b.leave()
+        eq_(b.participating, False)
+
+    def test_two_barrier(self):
+        av = threading.Event()
+        ev = threading.Event()
+        bv = threading.Event()
+        release_all = threading.Event()
+        b1 = self.client.DoubleBarrier("/some/path", 2)
+        b2 = self.client.DoubleBarrier("/some/path", 2)
+
+        def make_barrier_one():
+            b1.enter()
+            ev.set()
+            release_all.wait()
+            b1.leave()
+            ev.set()
+
+        def make_barrier_two():
+            bv.wait()
+            b2.enter()
+            av.set()
+            release_all.wait()
+            b2.leave()
+            av.set()
+
+        # Spin up both of them
+        t1 = threading.Thread(target=make_barrier_one)
+        t1.start()
+        t2 = threading.Thread(target=make_barrier_two)
+        t2.start()
+
+        eq_(b1.participating, False)
+        eq_(b2.participating, False)
+
+        bv.set()
+        av.wait()
+        ev.wait()
+        eq_(b1.participating, True)
+        eq_(b2.participating, True)
+
+        av.clear()
+        ev.clear()
+
+        release_all.set()
+        av.wait()
+        ev.wait()
+        eq_(b1.participating, False)
+        eq_(b2.participating, False)
+        t1.join()
+        t2.join()
+
+    def test_three_barrier(self):
+        av = threading.Event()
+        ev = threading.Event()
+        bv = threading.Event()
+        release_all = threading.Event()
+        b1 = self.client.DoubleBarrier("/some/path", 3)
+        b2 = self.client.DoubleBarrier("/some/path", 3)
+        b3 = self.client.DoubleBarrier("/some/path", 3)
+
+        def make_barrier_one():
+            b1.enter()
+            ev.set()
+            release_all.wait()
+            b1.leave()
+            ev.set()
+
+        def make_barrier_two():
+            bv.wait()
+            b2.enter()
+            av.set()
+            release_all.wait()
+            b2.leave()
+            av.set()
+
+        # Spin up both of them
+        t1 = threading.Thread(target=make_barrier_one)
+        t1.start()
+        t2 = threading.Thread(target=make_barrier_two)
+        t2.start()
+
+        eq_(b1.participating, False)
+        eq_(b2.participating, False)
+
+        bv.set()
+        eq_(b1.participating, False)
+        eq_(b2.participating, False)
+        b3.enter()
+        ev.wait()
+        av.wait()
+
+        eq_(b1.participating, True)
+        eq_(b2.participating, True)
+        eq_(b3.participating, True)
+
+        av.clear()
+        ev.clear()
+
+        release_all.set()
+        b3.leave()
+        av.wait()
+        ev.wait()
+        eq_(b1.participating, False)
+        eq_(b2.participating, False)
+        eq_(b3.participating, False)
+        t1.join()
+        t2.join()
+
+    def test_barrier_existing_parent_node(self):
+        b = self.client.DoubleBarrier('/some/path', 1)
+        self.assertFalse(b.participating)
+        self.client.create('/some', ephemeral=True)
+        # the barrier cannot create children under an ephemeral node
+        b.enter()
+        self.assertFalse(b.participating)
+
+    def test_barrier_existing_node(self):
+        b = self.client.DoubleBarrier('/some', 1)
+        self.assertFalse(b.participating)
+        self.client.ensure_path(b.path)
+        self.client.create(b.create_path, ephemeral=True)
+        # the barrier will re-use an existing node
+        b.enter()
+        self.assertTrue(b.participating)
+        b.leave()

+ 29 - 0
desktop/core/ext-py/kazoo-2.0/kazoo/tests/test_build.py

@@ -0,0 +1,29 @@
+import os
+
+from nose import SkipTest
+
+from kazoo.testing import KazooTestCase
+
+
+class TestBuildEnvironment(KazooTestCase):
+
+    def setUp(self):
+        KazooTestCase.setUp(self)
+        if not os.environ.get('TRAVIS'):
+            raise SkipTest('Only run build config tests on Travis.')
+
+    def test_gevent_version(self):
+        try:
+            import gevent
+        except ImportError:
+            raise SkipTest('gevent not available.')
+        env_version = os.environ.get('GEVENT_VERSION')
+        if env_version:
+            self.assertEqual(env_version, gevent.__version__)
+
+    def test_zookeeper_version(self):
+        server_version = self.client.server_version()
+        server_version = '.'.join([str(i) for i in server_version])
+        env_version = os.environ.get('ZOOKEEPER_VERSION')
+        if env_version:
+            self.assertEqual(env_version, server_version)

+ 1098 - 0
desktop/core/ext-py/kazoo-2.0/kazoo/tests/test_client.py

@@ -0,0 +1,1098 @@
+import socket
+import sys
+import threading
+import time
+import uuid
+import unittest
+
+from mock import patch
+from nose import SkipTest
+from nose.tools import eq_
+from nose.tools import raises
+
+from kazoo.testing import KazooTestCase
+from kazoo.exceptions import (
+    AuthFailedError,
+    BadArgumentsError,
+    ConfigurationError,
+    ConnectionClosedError,
+    ConnectionLoss,
+    InvalidACLError,
+    NoAuthError,
+    NoNodeError,
+    NodeExistsError,
+    SessionExpiredError,
+)
+from kazoo.protocol.connection import _CONNECTION_DROP
+from kazoo.protocol.states import KeeperState, KazooState
+from kazoo.tests.util import TRAVIS_ZK_VERSION
+
+
+if sys.version_info > (3, ):  # pragma: nocover
+    def u(s):
+        return s
+else:  # pragma: nocover
+    def u(s):
+        return unicode(s, "unicode_escape")
+
+
+class TestClientTransitions(KazooTestCase):
+    def test_connection_and_disconnection(self):
+        states = []
+        rc = threading.Event()
+
+        @self.client.add_listener
+        def listener(state):
+            states.append(state)
+            if state == KazooState.CONNECTED:
+                rc.set()
+
+        self.client.stop()
+        eq_(states, [KazooState.LOST])
+        states.pop()
+
+        self.client.start()
+        rc.wait(2)
+        eq_(states, [KazooState.CONNECTED])
+        rc.clear()
+        states.pop()
+        self.expire_session()
+        rc.wait(2)
+
+        req_states = [KazooState.LOST, KazooState.CONNECTED]
+        eq_(states, req_states)
+
+
+class TestClientConstructor(unittest.TestCase):
+
+    def _makeOne(self, *args, **kw):
+        from kazoo.client import KazooClient
+        return KazooClient(*args, **kw)
+
+    def test_invalid_handler(self):
+        from kazoo.handlers.threading import SequentialThreadingHandler
+        self.assertRaises(ConfigurationError,
+            self._makeOne, handler=SequentialThreadingHandler)
+
+    def test_chroot(self):
+        self.assertEqual(self._makeOne(hosts='127.0.0.1:2181/').chroot, '')
+        self.assertEqual(self._makeOne(hosts='127.0.0.1:2181/a').chroot, '/a')
+        self.assertEqual(self._makeOne(hosts='127.0.0.1/a').chroot, '/a')
+        self.assertEqual(self._makeOne(hosts='127.0.0.1/a/b').chroot, '/a/b')
+        self.assertEqual(self._makeOne(
+            hosts='127.0.0.1:2181,127.0.0.1:2182/a/b').chroot, '/a/b')
+
+    def test_connection_timeout(self):
+        from kazoo.handlers.threading import TimeoutError
+        client = self._makeOne(hosts='127.0.0.1:9')
+        self.assertTrue(client.handler.timeout_exception is TimeoutError)
+        self.assertRaises(TimeoutError, client.start, 0.1)
+
+    def test_ordered_host_selection(self):
+        client = self._makeOne(hosts='127.0.0.1:9,127.0.0.2:9/a',
+            randomize_hosts=False)
+        hosts = [h for h in client.hosts]
+        eq_(hosts, [('127.0.0.1', 9), ('127.0.0.2', 9)])
+
+    def test_invalid_hostname(self):
+        client = self._makeOne(hosts='nosuchhost/a')
+        timeout = client.handler.timeout_exception
+        self.assertRaises(timeout, client.start, 0.1)
+
+    def test_retry_options_dict(self):
+        from kazoo.retry import KazooRetry
+        client = self._makeOne(command_retry=dict(max_tries=99),
+                               connection_retry=dict(delay=99))
+        self.assertTrue(type(client._conn_retry) is KazooRetry)
+        self.assertTrue(type(client._retry) is KazooRetry)
+        eq_(client._retry.max_tries, 99)
+        eq_(client._conn_retry.delay, 99)
+
+
+class TestAuthentication(KazooTestCase):
+
+    def _makeAuth(self, *args, **kwargs):
+        from kazoo.security import make_digest_acl
+        return make_digest_acl(*args, **kwargs)
+
+    def test_auth(self):
+        username = uuid.uuid4().hex
+        password = uuid.uuid4().hex
+
+        digest_auth = "%s:%s" % (username, password)
+        acl = self._makeAuth(username, password, all=True)
+
+        client = self._get_client()
+        client.start()
+        client.add_auth("digest", digest_auth)
+        client.default_acl = (acl,)
+
+        try:
+            client.create("/1")
+            client.create("/1/2")
+            client.ensure_path("/1/2/3")
+
+            eve = self._get_client()
+            eve.start()
+
+            self.assertRaises(NoAuthError, eve.get, "/1/2")
+
+            # try again with the wrong auth token
+            eve.add_auth("digest", "badbad:bad")
+
+            self.assertRaises(NoAuthError, eve.get, "/1/2")
+        finally:
+            # Ensure we remove the ACL protected nodes
+            client.delete("/1", recursive=True)
+            eve.stop()
+            eve.close()
+
+    def test_connect_auth(self):
+        username = uuid.uuid4().hex
+        password = uuid.uuid4().hex
+
+        digest_auth = "%s:%s" % (username, password)
+        acl = self._makeAuth(username, password, all=True)
+
+        client = self._get_client(auth_data=[('digest', digest_auth)])
+        client.start()
+        try:
+            client.create('/1', acl=(acl,))
+            # give ZK a chance to copy data to other node
+            time.sleep(0.1)
+            self.assertRaises(NoAuthError, self.client.get, "/1")
+        finally:
+            client.delete('/1')
+            client.stop()
+            client.close()
+
+    def test_unicode_auth(self):
+        username = u("xe4/\hm")
+        password = u("/\xe4hm")
+        digest_auth = "%s:%s" % (username, password)
+        acl = self._makeAuth(username, password, all=True)
+
+        client = self._get_client()
+        client.start()
+        client.add_auth("digest", digest_auth)
+        client.default_acl = (acl,)
+
+        try:
+            client.create("/1")
+            client.ensure_path("/1/2/3")
+
+            eve = self._get_client()
+            eve.start()
+
+            self.assertRaises(NoAuthError, eve.get, "/1/2")
+
+            # try again with the wrong auth token
+            eve.add_auth("digest", "badbad:bad")
+
+            self.assertRaises(NoAuthError, eve.get, "/1/2")
+        finally:
+            # Ensure we remove the ACL protected nodes
+            client.delete("/1", recursive=True)
+            eve.stop()
+            eve.close()
+
+    def test_invalid_auth(self):
+        client = self._get_client()
+        client.start()
+        self.assertRaises(TypeError, client.add_auth,
+                          'digest', ('user', 'pass'))
+        self.assertRaises(TypeError, client.add_auth,
+                          None, ('user', 'pass'))
+
+    def test_async_auth(self):
+        client = self._get_client()
+        client.start()
+        username = uuid.uuid4().hex
+        password = uuid.uuid4().hex
+        digest_auth = "%s:%s" % (username, password)
+        result = client.add_auth_async("digest", digest_auth)
+        self.assertTrue(result.get())
+
+    def test_async_auth_failure(self):
+        client = self._get_client()
+        client.start()
+        username = uuid.uuid4().hex
+        password = uuid.uuid4().hex
+        digest_auth = "%s:%s" % (username, password)
+
+        self.assertRaises(AuthFailedError, client.add_auth,
+                          "unknown-scheme", digest_auth)
+
+    def test_add_auth_on_reconnect(self):
+        client = self._get_client()
+        client.start()
+        client.add_auth("digest", "jsmith:jsmith")
+        client._connection._socket.shutdown(socket.SHUT_RDWR)
+        while not client.connected:
+            time.sleep(0.1)
+        self.assertTrue(("digest", "jsmith:jsmith") in client.auth_data)
+
+
+class TestConnection(KazooTestCase):
+
+    def test_chroot_warning(self):
+        k = self._get_nonchroot_client()
+        k.chroot = 'abba'
+        try:
+            with patch('warnings.warn') as mock_func:
+                k.start()
+                assert mock_func.called
+        finally:
+            k.stop()
+
+    def test_session_expire(self):
+        from kazoo.protocol.states import KazooState
+
+        cv = threading.Event()
+
+        def watch_events(event):
+            if event == KazooState.LOST:
+                cv.set()
+
+        self.client.add_listener(watch_events)
+        self.expire_session()
+        cv.wait(3)
+        assert cv.is_set()
+
+    def test_bad_session_expire(self):
+        from kazoo.protocol.states import KazooState
+
+        cv = threading.Event()
+        ab = threading.Event()
+
+        def watch_events(event):
+            if event == KazooState.LOST:
+                ab.set()
+                raise Exception("oops")
+                cv.set()
+
+        self.client.add_listener(watch_events)
+        self.expire_session()
+        ab.wait(0.5)
+        assert ab.is_set()
+        cv.wait(0.5)
+        assert not cv.is_set()
+
+    def test_state_listener(self):
+        from kazoo.protocol.states import KazooState
+        states = []
+        condition = threading.Condition()
+
+        def listener(state):
+            with condition:
+                states.append(state)
+                condition.notify_all()
+
+        self.client.stop()
+        eq_(self.client.state, KazooState.LOST)
+        self.client.add_listener(listener)
+        self.client.start(5)
+
+        with condition:
+            if not states:
+                condition.wait(5)
+
+        eq_(len(states), 1)
+        eq_(states[0], KazooState.CONNECTED)
+
+    def test_invalid_listener(self):
+        self.assertRaises(ConfigurationError, self.client.add_listener, 15)
+
+    def test_listener_only_called_on_real_state_change(self):
+        from kazoo.protocol.states import KazooState
+        self.assertTrue(self.client.state, KazooState.CONNECTED)
+        called = [False]
+        condition = threading.Event()
+
+        def listener(state):
+            called[0] = True
+            condition.set()
+
+        self.client.add_listener(listener)
+        self.client._make_state_change(KazooState.CONNECTED)
+        condition.wait(3)
+        self.assertFalse(called[0])
+
+    def test_no_connection(self):
+        client = self.client
+        client.stop()
+        self.assertFalse(client.connected)
+        self.assertTrue(client.client_id is None)
+        self.assertRaises(ConnectionClosedError, client.exists, '/')
+
+    def test_close_connecting_connection(self):
+        client = self.client
+        client.stop()
+        ev = threading.Event()
+
+        def close_on_connecting(state):
+            if state in (KazooState.CONNECTED, KazooState.LOST):
+                ev.set()
+
+        client.add_listener(close_on_connecting)
+        client.start()
+
+        # Wait until we connect
+        ev.wait(5)
+        ev.clear()
+        self.client._call(_CONNECTION_DROP, client.handler.async_result())
+
+        client.stop()
+
+        # ...and then wait until the connection is lost
+        ev.wait(5)
+
+        self.assertRaises(ConnectionClosedError,
+                          self.client.create, '/foobar')
+
+    def test_double_start(self):
+        self.assertTrue(self.client.connected)
+        self.client.start()
+        self.assertTrue(self.client.connected)
+
+    def test_double_stop(self):
+        self.client.stop()
+        self.assertFalse(self.client.connected)
+        self.client.stop()
+        self.assertFalse(self.client.connected)
+
+    def test_restart(self):
+        self.assertTrue(self.client.connected)
+        self.client.restart()
+        self.assertTrue(self.client.connected)
+
+    def test_closed(self):
+        client = self.client
+        client.stop()
+
+        write_pipe = client._connection._write_pipe
+
+        # close the connection to free the pipe
+        client.close()
+        eq_(client._connection._write_pipe, None)
+
+        # sneak in and patch client to simulate race between a thread
+        # calling stop(); close() and one running a command
+        oldstate = client._state
+        client._state = KeeperState.CONNECTED
+        client._connection._write_pipe = write_pipe
+        try:
+            # simulate call made after write pipe is closed
+            self.assertRaises(ConnectionClosedError, client.exists, '/')
+
+            # simualte call made after write pipe is set to None
+            client._connection._write_pipe = None
+            self.assertRaises(ConnectionClosedError, client.exists, '/')
+
+        finally:
+            # reset for teardown
+            client._state = oldstate
+            client._connection._write_pipe = None
+
+
+class TestClient(KazooTestCase):
+    def _getKazooState(self):
+        from kazoo.protocol.states import KazooState
+        return KazooState
+
+    def test_client_id(self):
+        client_id = self.client.client_id
+        self.assertEqual(type(client_id), tuple)
+        # make sure password is of correct length
+        self.assertEqual(len(client_id[1]), 16)
+
+    def test_connected(self):
+        client = self.client
+        self.assertTrue(client.connected)
+
+    def test_create(self):
+        client = self.client
+        path = client.create("/1")
+        eq_(path, "/1")
+        self.assertTrue(client.exists("/1"))
+
+    def test_create_on_broken_connection(self):
+        client = self.client
+        client.start()
+
+        client._state = KeeperState.EXPIRED_SESSION
+        self.assertRaises(SessionExpiredError, client.create,
+                          '/closedpath', b'bar')
+
+        client._state = KeeperState.AUTH_FAILED
+        self.assertRaises(AuthFailedError, client.create,
+                          '/closedpath', b'bar')
+
+        client._state = KeeperState.CONNECTING
+        self.assertRaises(SessionExpiredError, client.create,
+                          '/closedpath', b'bar')
+        client.stop()
+        client.close()
+
+        self.assertRaises(ConnectionClosedError, client.create,
+                          '/closedpath', b'bar')
+
+    def test_create_null_data(self):
+        client = self.client
+        client.create("/nulldata", None)
+        value, _ = client.get("/nulldata")
+        self.assertEqual(value, None)
+
+    def test_create_empty_string(self):
+        client = self.client
+        client.create("/empty", b"")
+        value, _ = client.get("/empty")
+        eq_(value, b"")
+
+    def test_create_unicode_path(self):
+        client = self.client
+        path = client.create(u("/ascii"))
+        eq_(path, u("/ascii"))
+        path = client.create(u("/\xe4hm"))
+        eq_(path, u("/\xe4hm"))
+
+    def test_create_async_returns_unchrooted_path(self):
+        client = self.client
+        path = client.create_async('/1').get()
+        eq_(path, "/1")
+
+    def test_create_invalid_path(self):
+        client = self.client
+        self.assertRaises(TypeError, client.create, ('a', ))
+        self.assertRaises(ValueError, client.create, ".")
+        self.assertRaises(ValueError, client.create, "/a/../b")
+        self.assertRaises(BadArgumentsError, client.create, "/b\x00")
+        self.assertRaises(BadArgumentsError, client.create, "/b\x1e")
+
+    def test_create_invalid_arguments(self):
+        from kazoo.security import OPEN_ACL_UNSAFE
+        single_acl = OPEN_ACL_UNSAFE[0]
+        client = self.client
+        self.assertRaises(TypeError, client.create, 'a', acl='all')
+        self.assertRaises(TypeError, client.create, 'a', acl=single_acl)
+        self.assertRaises(TypeError, client.create, 'a', value=['a'])
+        self.assertRaises(TypeError, client.create, 'a', ephemeral='yes')
+        self.assertRaises(TypeError, client.create, 'a', sequence='yes')
+        self.assertRaises(TypeError, client.create, 'a', makepath='yes')
+
+    def test_create_value(self):
+        client = self.client
+        client.create("/1", b"bytes")
+        data, stat = client.get("/1")
+        eq_(data, b"bytes")
+
+    def test_create_unicode_value(self):
+        client = self.client
+        self.assertRaises(TypeError, client.create, "/1", u("\xe4hm"))
+
+    def test_create_large_value(self):
+        client = self.client
+        kb_512 = b"a" * (512 * 1024)
+        client.create("/1", kb_512)
+        self.assertTrue(client.exists("/1"))
+        mb_2 = b"a" * (2 * 1024 * 1024)
+        self.assertRaises(ConnectionLoss, client.create, "/2", mb_2)
+
+    def test_create_acl_duplicate(self):
+        from kazoo.security import OPEN_ACL_UNSAFE
+        single_acl = OPEN_ACL_UNSAFE[0]
+        client = self.client
+        client.create("/1", acl=[single_acl, single_acl])
+        acls, stat = client.get_acls("/1")
+        # ZK >3.4 removes duplicate ACL entries
+        if TRAVIS_ZK_VERSION:
+            version = TRAVIS_ZK_VERSION
+        else:
+            version = client.server_version()
+        self.assertEqual(len(acls), 1 if version > (3, 4) else 2)
+
+    def test_create_acl_empty_list(self):
+        from kazoo.security import OPEN_ACL_UNSAFE
+        client = self.client
+        client.create("/1", acl=[])
+        acls, stat = client.get_acls("/1")
+        self.assertEqual(acls, OPEN_ACL_UNSAFE)
+
+    def test_version_no_connection(self):
+        @raises(ConnectionLoss)
+        def testit():
+            self.client.server_version()
+        self.client.stop()
+        testit()
+
+    def test_create_ephemeral(self):
+        client = self.client
+        client.create("/1", b"ephemeral", ephemeral=True)
+        data, stat = client.get("/1")
+        eq_(data, b"ephemeral")
+        eq_(stat.ephemeralOwner, client.client_id[0])
+
+    def test_create_no_ephemeral(self):
+        client = self.client
+        client.create("/1", b"val1")
+        data, stat = client.get("/1")
+        self.assertFalse(stat.ephemeralOwner)
+
+    def test_create_ephemeral_no_children(self):
+        from kazoo.exceptions import NoChildrenForEphemeralsError
+        client = self.client
+        client.create("/1", b"ephemeral", ephemeral=True)
+        self.assertRaises(NoChildrenForEphemeralsError,
+            client.create, "/1/2", b"val1")
+        self.assertRaises(NoChildrenForEphemeralsError,
+            client.create, "/1/2", b"val1", ephemeral=True)
+
+    def test_create_sequence(self):
+        client = self.client
+        client.create("/folder")
+        path = client.create("/folder/a", b"sequence", sequence=True)
+        eq_(path, "/folder/a0000000000")
+        path2 = client.create("/folder/a", b"sequence", sequence=True)
+        eq_(path2, "/folder/a0000000001")
+        path3 = client.create("/folder/", b"sequence", sequence=True)
+        eq_(path3, "/folder/0000000002")
+
+    def test_create_ephemeral_sequence(self):
+        basepath = "/" + uuid.uuid4().hex
+        realpath = self.client.create(basepath, b"sandwich", sequence=True,
+            ephemeral=True)
+        self.assertTrue(basepath != realpath and realpath.startswith(basepath))
+        data, stat = self.client.get(realpath)
+        eq_(data, b"sandwich")
+
+    def test_create_makepath(self):
+        self.client.create("/1/2", b"val1", makepath=True)
+        data, stat = self.client.get("/1/2")
+        eq_(data, b"val1")
+
+        self.client.create("/1/2/3/4/5", b"val2", makepath=True)
+        data, stat = self.client.get("/1/2/3/4/5")
+        eq_(data, b"val2")
+
+        self.assertRaises(NodeExistsError, self.client.create, "/1/2/3/4/5",
+            b"val2", makepath=True)
+
+    def test_create_makepath_incompatible_acls(self):
+        from kazoo.client import KazooClient
+        from kazoo.security import make_digest_acl_credential, CREATOR_ALL_ACL
+        credential = make_digest_acl_credential("username", "password")
+        alt_client = KazooClient(self.cluster[0].address + self.client.chroot,
+            max_retries=5, auth_data=[("digest", credential)])
+        alt_client.start()
+        alt_client.create("/1/2", b"val2", makepath=True, acl=CREATOR_ALL_ACL)
+
+        try:
+            self.assertRaises(NoAuthError, self.client.create, "/1/2/3/4/5",
+                b"val2", makepath=True)
+        finally:
+            alt_client.delete('/', recursive=True)
+            alt_client.stop()
+
+    def test_create_no_makepath(self):
+        self.assertRaises(NoNodeError, self.client.create, "/1/2", b"val1")
+        self.assertRaises(NoNodeError, self.client.create, "/1/2", b"val1",
+            makepath=False)
+
+        self.client.create("/1/2", b"val1", makepath=True)
+        self.assertRaises(NoNodeError, self.client.create, "/1/2/3/4", b"val1",
+            makepath=False)
+
+    def test_create_exists(self):
+        from kazoo.exceptions import NodeExistsError
+        client = self.client
+        path = client.create("/1")
+        self.assertRaises(NodeExistsError, client.create, path)
+
+    def test_create_get_set(self):
+        nodepath = "/" + uuid.uuid4().hex
+
+        self.client.create(nodepath, b"sandwich", ephemeral=True)
+
+        data, stat = self.client.get(nodepath)
+        eq_(data, b"sandwich")
+
+        newstat = self.client.set(nodepath, b"hats", stat.version)
+        self.assertTrue(newstat)
+        assert newstat.version > stat.version
+
+        # Some other checks of the ZnodeStat object we got
+        eq_(newstat.acl_version, stat.acl_version)
+        eq_(newstat.created, stat.ctime / 1000.0)
+        eq_(newstat.last_modified, newstat.mtime / 1000.0)
+        eq_(newstat.owner_session_id, stat.ephemeralOwner)
+        eq_(newstat.creation_transaction_id, stat.czxid)
+        eq_(newstat.last_modified_transaction_id, newstat.mzxid)
+        eq_(newstat.data_length, newstat.dataLength)
+        eq_(newstat.children_count, stat.numChildren)
+        eq_(newstat.children_version, stat.cversion)
+
+    def test_get_invalid_arguments(self):
+        client = self.client
+        self.assertRaises(TypeError, client.get, ('a', 'b'))
+        self.assertRaises(TypeError, client.get, 'a', watch=True)
+
+    def test_bad_argument(self):
+        client = self.client
+        client.ensure_path("/1")
+        self.assertRaises(TypeError, self.client.set, "/1", 1)
+
+    def test_ensure_path(self):
+        client = self.client
+        client.ensure_path("/1/2")
+        self.assertTrue(client.exists("/1/2"))
+
+        client.ensure_path("/1/2/3/4")
+        self.assertTrue(client.exists("/1/2/3/4"))
+
+    def test_sync(self):
+        client = self.client
+        self.assertTrue(client.sync('/'), '/')
+
+    def test_exists(self):
+        nodepath = "/" + uuid.uuid4().hex
+
+        exists = self.client.exists(nodepath)
+        eq_(exists, None)
+
+        self.client.create(nodepath, b"sandwich", ephemeral=True)
+        exists = self.client.exists(nodepath)
+        self.assertTrue(exists)
+        assert isinstance(exists.version, int)
+
+        multi_node_nonexistent = "/" + uuid.uuid4().hex + "/hats"
+        exists = self.client.exists(multi_node_nonexistent)
+        eq_(exists, None)
+
+    def test_exists_invalid_arguments(self):
+        client = self.client
+        self.assertRaises(TypeError, client.exists, ('a', 'b'))
+        self.assertRaises(TypeError, client.exists, 'a', watch=True)
+
+    def test_exists_watch(self):
+        nodepath = "/" + uuid.uuid4().hex
+        event = self.client.handler.event_object()
+
+        def w(watch_event):
+            eq_(watch_event.path, nodepath)
+            event.set()
+
+        exists = self.client.exists(nodepath, watch=w)
+        eq_(exists, None)
+
+        self.client.create(nodepath, ephemeral=True)
+
+        event.wait(1)
+        self.assertTrue(event.is_set())
+
+    def test_exists_watcher_exception(self):
+        nodepath = "/" + uuid.uuid4().hex
+        event = self.client.handler.event_object()
+
+        # if the watcher throws an exception, all we can really do is log it
+        def w(watch_event):
+            eq_(watch_event.path, nodepath)
+            event.set()
+
+            raise Exception("test exception in callback")
+
+        exists = self.client.exists(nodepath, watch=w)
+        eq_(exists, None)
+
+        self.client.create(nodepath, ephemeral=True)
+
+        event.wait(1)
+        self.assertTrue(event.is_set())
+
+    def test_create_delete(self):
+        nodepath = "/" + uuid.uuid4().hex
+
+        self.client.create(nodepath, b"zzz")
+
+        self.client.delete(nodepath)
+
+        exists = self.client.exists(nodepath)
+        eq_(exists, None)
+
+    def test_get_acls(self):
+        from kazoo.security import make_digest_acl
+        acl = make_digest_acl('user', 'pass', all=True)
+        client = self.client
+        try:
+            client.create('/a', acl=[acl])
+            self.assertTrue(acl in client.get_acls('/a')[0])
+        finally:
+            client.delete('/a')
+
+    def test_get_acls_invalid_arguments(self):
+        client = self.client
+        self.assertRaises(TypeError, client.get_acls, ('a', 'b'))
+
+    def test_set_acls(self):
+        from kazoo.security import make_digest_acl
+        acl = make_digest_acl('user', 'pass', all=True)
+        client = self.client
+        client.create('/a')
+        try:
+            client.set_acls('/a', [acl])
+            self.assertTrue(acl in client.get_acls('/a')[0])
+        finally:
+            client.delete('/a')
+
+    def test_set_acls_empty(self):
+        client = self.client
+        client.create('/a')
+        self.assertRaises(InvalidACLError, client.set_acls, '/a', [])
+
+    def test_set_acls_no_node(self):
+        from kazoo.security import OPEN_ACL_UNSAFE
+        client = self.client
+        self.assertRaises(NoNodeError, client.set_acls, '/a', OPEN_ACL_UNSAFE)
+
+    def test_set_acls_invalid_arguments(self):
+        from kazoo.security import OPEN_ACL_UNSAFE
+        single_acl = OPEN_ACL_UNSAFE[0]
+        client = self.client
+        self.assertRaises(TypeError, client.set_acls, ('a', 'b'), ())
+        self.assertRaises(TypeError, client.set_acls, 'a', single_acl)
+        self.assertRaises(TypeError, client.set_acls, 'a', 'all')
+        self.assertRaises(TypeError, client.set_acls, 'a', [single_acl], 'V1')
+
+    def test_set(self):
+        client = self.client
+        client.create('a', b'first')
+        stat = client.set('a', b'second')
+        data, stat2 = client.get('a')
+        self.assertEqual(data, b'second')
+        self.assertEqual(stat, stat2)
+
+    def test_set_null_data(self):
+        client = self.client
+        client.create("/nulldata", b"not none")
+        client.set("/nulldata", None)
+        value, _ = client.get("/nulldata")
+        self.assertEqual(value, None)
+
+    def test_set_empty_string(self):
+        client = self.client
+        client.create("/empty", b"not empty")
+        client.set("/empty", b"")
+        value, _ = client.get("/empty")
+        eq_(value, b"")
+
+    def test_set_invalid_arguments(self):
+        client = self.client
+        client.create('a', b'first')
+        self.assertRaises(TypeError, client.set, ('a', 'b'), b'value')
+        self.assertRaises(TypeError, client.set, 'a', ['v', 'w'])
+        self.assertRaises(TypeError, client.set, 'a', b'value', 'V1')
+
+    def test_delete(self):
+        client = self.client
+        client.ensure_path('/a/b')
+        self.assertTrue('b' in client.get_children('a'))
+        client.delete('/a/b')
+        self.assertFalse('b' in client.get_children('a'))
+
+    def test_delete_recursive(self):
+        client = self.client
+        client.ensure_path('/a/b/c')
+        client.ensure_path('/a/b/d')
+        client.delete('/a/b', recursive=True)
+        client.delete('/a/b/c', recursive=True)
+        self.assertFalse('b' in client.get_children('a'))
+
+    def test_delete_invalid_arguments(self):
+        client = self.client
+        client.ensure_path('/a/b')
+        self.assertRaises(TypeError, client.delete, '/a/b', recursive='all')
+        self.assertRaises(TypeError, client.delete, ('a', 'b'))
+        self.assertRaises(TypeError, client.delete, '/a/b', version='V1')
+
+    def test_get_children(self):
+        client = self.client
+        client.ensure_path('/a/b/c')
+        client.ensure_path('/a/b/d')
+        self.assertEqual(client.get_children('/a'), ['b'])
+        self.assertEqual(set(client.get_children('/a/b')), set(['c', 'd']))
+        self.assertEqual(client.get_children('/a/b/c'), [])
+
+    def test_get_children2(self):
+        client = self.client
+        client.ensure_path('/a/b')
+        children, stat = client.get_children('/a', include_data=True)
+        value, stat2 = client.get('/a')
+        self.assertEqual(children, ['b'])
+        self.assertEqual(stat2.version, stat.version)
+
+    def test_get_children2_many_nodes(self):
+        client = self.client
+        client.ensure_path('/a/b')
+        client.ensure_path('/a/c')
+        client.ensure_path('/a/d')
+        children, stat = client.get_children('/a', include_data=True)
+        value, stat2 = client.get('/a')
+        self.assertEqual(set(children), set(['b', 'c', 'd']))
+        self.assertEqual(stat2.version, stat.version)
+
+    def test_get_children_no_node(self):
+        client = self.client
+        self.assertRaises(NoNodeError, client.get_children, '/none')
+        self.assertRaises(NoNodeError, client.get_children,
+            '/none', include_data=True)
+
+    def test_get_children_invalid_path(self):
+        client = self.client
+        self.assertRaises(ValueError, client.get_children, '../a')
+
+    def test_get_children_invalid_arguments(self):
+        client = self.client
+        self.assertRaises(TypeError, client.get_children, ('a', 'b'))
+        self.assertRaises(TypeError, client.get_children, 'a', watch=True)
+        self.assertRaises(TypeError, client.get_children,
+            'a', include_data='yes')
+
+    def test_invalid_auth(self):
+        from kazoo.exceptions import AuthFailedError
+        from kazoo.protocol.states import KeeperState
+
+        client = self.client
+        client.stop()
+        client._state = KeeperState.AUTH_FAILED
+
+        @raises(AuthFailedError)
+        def testit():
+            client.get('/')
+        testit()
+
+    def test_client_state(self):
+        from kazoo.protocol.states import KeeperState
+        eq_(self.client.client_state, KeeperState.CONNECTED)
+
+    def test_update_host_list(self):
+        from kazoo.client import KazooClient
+        from kazoo.protocol.states import KeeperState
+        hosts = self.cluster[0].address
+        # create a client with only one server in its list
+        client = KazooClient(hosts=hosts)
+        client.start()
+
+        # try to change the chroot, not currently allowed
+        self.assertRaises(ConfigurationError,
+                          client.set_hosts, hosts + '/new_chroot')
+
+        # grow the cluster to 3
+        client.set_hosts(self.servers)
+
+        # shut down the first host
+        try:
+            self.cluster[0].stop()
+            time.sleep(5)
+            eq_(client.client_state, KeeperState.CONNECTED)
+        finally:
+            self.cluster[0].run()
+
+
+dummy_dict = {
+    'aversion': 1, 'ctime': 0, 'cversion': 1,
+    'czxid': 110, 'dataLength': 1, 'ephemeralOwner': 'ben',
+    'mtime': 1, 'mzxid': 1, 'numChildren': 0, 'pzxid': 1, 'version': 1
+}
+
+
+class TestClientTransactions(KazooTestCase):
+
+    def setUp(self):
+        KazooTestCase.setUp(self)
+        skip = False
+        if TRAVIS_ZK_VERSION and TRAVIS_ZK_VERSION < (3, 4):
+            skip = True
+        elif TRAVIS_ZK_VERSION and TRAVIS_ZK_VERSION >= (3, 4):
+            skip = False
+        else:
+            ver = self.client.server_version()
+            if ver[1] < 4:
+                skip = True
+        if skip:
+            raise SkipTest("Must use Zookeeper 3.4 or above")
+
+    def test_basic_create(self):
+        t = self.client.transaction()
+        t.create('/freddy')
+        t.create('/fred', ephemeral=True)
+        t.create('/smith', sequence=True)
+        results = t.commit()
+        eq_(results[0], '/freddy')
+        eq_(len(results), 3)
+        self.assertTrue(results[2].startswith('/smith0'))
+
+    def test_bad_creates(self):
+        args_list = [(True,), ('/smith', 0), ('/smith', b'', 'bleh'),
+                     ('/smith', b'', None, 'fred'),
+                     ('/smith', b'', None, True, 'fred')]
+
+        @raises(TypeError)
+        def testit(args):
+            t = self.client.transaction()
+            t.create(*args)
+
+        for args in args_list:
+            testit(args)
+
+    def test_default_acl(self):
+        from kazoo.security import make_digest_acl
+        username = uuid.uuid4().hex
+        password = uuid.uuid4().hex
+
+        digest_auth = "%s:%s" % (username, password)
+        acl = make_digest_acl(username, password, all=True)
+
+        self.client.add_auth("digest", digest_auth)
+        self.client.default_acl = (acl,)
+
+        t = self.client.transaction()
+        t.create('/freddy')
+        results = t.commit()
+        eq_(results[0], '/freddy')
+
+    def test_basic_delete(self):
+        self.client.create('/fred')
+        t = self.client.transaction()
+        t.delete('/fred')
+        results = t.commit()
+        eq_(results[0], True)
+
+    def test_bad_deletes(self):
+        args_list = [(True,), ('/smith', 'woops'), ]
+
+        @raises(TypeError)
+        def testit(args):
+            t = self.client.transaction()
+            t.delete(*args)
+
+        for args in args_list:
+            testit(args)
+
+    def test_set(self):
+        self.client.create('/fred', b'01')
+        t = self.client.transaction()
+        t.set_data('/fred', b'oops')
+        t.commit()
+        res = self.client.get('/fred')
+        eq_(res[0], b'oops')
+
+    def test_bad_sets(self):
+        args_list = [(42, 52), ('/smith', False), ('/smith', b'', 'oops')]
+
+        @raises(TypeError)
+        def testit(args):
+            t = self.client.transaction()
+            t.set_data(*args)
+
+        for args in args_list:
+            testit(args)
+
+    def test_check(self):
+        self.client.create('/fred')
+        version = self.client.get('/fred')[1].version
+        t = self.client.transaction()
+        t.check('/fred', version)
+        t.create('/blah')
+        results = t.commit()
+        eq_(results[0], True)
+        eq_(results[1], '/blah')
+
+    def test_bad_checks(self):
+        args_list = [(42, 52), ('/smith', 'oops')]
+
+        @raises(TypeError)
+        def testit(args):
+            t = self.client.transaction()
+            t.check(*args)
+
+        for args in args_list:
+            testit(args)
+
+    def test_bad_transaction(self):
+        from kazoo.exceptions import RolledBackError, NoNodeError
+        t = self.client.transaction()
+        t.create('/fred')
+        t.delete('/smith')
+        results = t.commit()
+        eq_(results[0].__class__, RolledBackError)
+        eq_(results[1].__class__, NoNodeError)
+
+    def test_bad_commit(self):
+        t = self.client.transaction()
+
+        @raises(ValueError)
+        def testit():
+            t.commit()
+
+        t.committed = True
+        testit()
+
+    def test_bad_context(self):
+        @raises(TypeError)
+        def testit():
+            with self.client.transaction() as t:
+                t.check(4232)
+        testit()
+
+    def test_context(self):
+        with self.client.transaction() as t:
+            t.create('/smith', b'32')
+        eq_(self.client.get('/smith')[0], b'32')
+
+
+class TestCallbacks(unittest.TestCase):
+    def test_session_callback_states(self):
+        from kazoo.protocol.states import KazooState, KeeperState
+        from kazoo.client import KazooClient
+
+        client = KazooClient()
+        client._handle = 1
+        client._live.set()
+
+        result = client._session_callback(KeeperState.CONNECTED)
+        eq_(result, None)
+
+        # Now with stopped
+        client._stopped.set()
+        result = client._session_callback(KeeperState.CONNECTED)
+        eq_(result, None)
+
+        # Test several state transitions
+        client._stopped.clear()
+        client.start_async = lambda: True
+        client._session_callback(KeeperState.CONNECTED)
+        eq_(client.state, KazooState.CONNECTED)
+
+        client._session_callback(KeeperState.AUTH_FAILED)
+        eq_(client.state, KazooState.LOST)
+
+        client._handle = 1
+        client._session_callback(-250)
+        eq_(client.state, KazooState.SUSPENDED)
+
+
+class TestNonChrootClient(KazooTestCase):
+
+    def test_create(self):
+        client = self._get_nonchroot_client()
+        self.assertEqual(client.chroot, '')
+        client.start()
+        node = uuid.uuid4().hex
+        path = client.create(node, ephemeral=True)
+        client.delete(path)
+        client.stop()
+
+    def test_unchroot(self):
+        client = self._get_nonchroot_client()
+        client.chroot = '/a'
+        self.assertEquals(client.unchroot('/a/b'), '/b')
+        self.assertEquals(client.unchroot('/b/c'), '/b/c')

+ 319 - 0
desktop/core/ext-py/kazoo-2.0/kazoo/tests/test_connection.py

@@ -0,0 +1,319 @@
+from collections import namedtuple
+import os
+import errno
+import threading
+import time
+import uuid
+import struct
+
+from nose import SkipTest
+from nose.tools import eq_
+from nose.tools import raises
+import mock
+
+from kazoo.exceptions import ConnectionLoss
+from kazoo.protocol.serialization import (
+    Connect,
+    int_struct,
+    write_string,
+)
+from kazoo.protocol.states import KazooState
+from kazoo.protocol.connection import _CONNECTION_DROP
+from kazoo.testing import KazooTestCase
+from kazoo.tests.util import wait
+from kazoo.tests.util import TRAVIS_ZK_VERSION
+
+
+class Delete(namedtuple('Delete', 'path version')):
+    type = 2
+
+    def serialize(self):
+        b = bytearray()
+        b.extend(write_string(self.path))
+        b.extend(int_struct.pack(self.version))
+        return b
+
+    @classmethod
+    def deserialize(self, bytes, offset):
+        raise ValueError("oh my")
+
+
+class TestConnectionHandler(KazooTestCase):
+    def test_bad_deserialization(self):
+        async_object = self.client.handler.async_result()
+        self.client._queue.append((Delete(self.client.chroot, -1), async_object))
+        os.write(self.client._connection._write_pipe, b'\0')
+
+        @raises(ValueError)
+        def testit():
+            async_object.get()
+        testit()
+
+    def test_with_bad_sessionid(self):
+        ev = threading.Event()
+
+        def expired(state):
+            if state == KazooState.CONNECTED:
+                ev.set()
+
+        password = os.urandom(16)
+        client = self._get_client(client_id=(82838284824, password))
+        client.add_listener(expired)
+        client.start()
+        try:
+            ev.wait(15)
+            eq_(ev.is_set(), True)
+        finally:
+            client.stop()
+
+    def test_connection_read_timeout(self):
+        client = self.client
+        ev = threading.Event()
+        path = "/" + uuid.uuid4().hex
+        handler = client.handler
+        _select = handler.select
+        _socket = client._connection._socket
+
+        def delayed_select(*args, **kwargs):
+            result = _select(*args, **kwargs)
+            if len(args[0]) == 1 and _socket in args[0]:
+                # for any socket read, simulate a timeout
+                return [], [], []
+            return result
+
+        def back(state):
+            if state == KazooState.CONNECTED:
+                ev.set()
+
+        client.add_listener(back)
+        client.create(path, b"1")
+        try:
+            handler.select = delayed_select
+            self.assertRaises(ConnectionLoss, client.get, path)
+        finally:
+            handler.select = _select
+        # the client reconnects automatically
+        ev.wait(5)
+        eq_(ev.is_set(), True)
+        eq_(client.get(path)[0], b"1")
+
+    def test_connection_write_timeout(self):
+        client = self.client
+        ev = threading.Event()
+        path = "/" + uuid.uuid4().hex
+        handler = client.handler
+        _select = handler.select
+        _socket = client._connection._socket
+
+        def delayed_select(*args, **kwargs):
+            result = _select(*args, **kwargs)
+            if _socket in args[1]:
+                # for any socket write, simulate a timeout
+                return [], [], []
+            return result
+
+        def back(state):
+            if state == KazooState.CONNECTED:
+                ev.set()
+        client.add_listener(back)
+
+        try:
+            handler.select = delayed_select
+            self.assertRaises(ConnectionLoss, client.create, path)
+        finally:
+            handler.select = _select
+        # the client reconnects automatically
+        ev.wait(5)
+        eq_(ev.is_set(), True)
+        eq_(client.exists(path), None)
+
+    def test_connection_deserialize_fail(self):
+        client = self.client
+        ev = threading.Event()
+        path = "/" + uuid.uuid4().hex
+        handler = client.handler
+        _select = handler.select
+        _socket = client._connection._socket
+
+        def delayed_select(*args, **kwargs):
+            result = _select(*args, **kwargs)
+            if _socket in args[1]:
+                # for any socket write, simulate a timeout
+                return [], [], []
+            return result
+
+        def back(state):
+            if state == KazooState.CONNECTED:
+                ev.set()
+        client.add_listener(back)
+
+        deserialize_ev = threading.Event()
+
+        def bad_deserialize(bytes, offset):
+            deserialize_ev.set()
+            raise struct.error()
+
+        # force the connection to die but, on reconnect, cause the
+        # server response to be non-deserializable. ensure that the client
+        # continues to retry. This partially reproduces a rare bug seen
+        # in production.
+
+        with mock.patch.object(Connect, 'deserialize') as mock_deserialize:
+            mock_deserialize.side_effect = bad_deserialize
+            try:
+                handler.select = delayed_select
+                self.assertRaises(ConnectionLoss, client.create, path)
+            finally:
+                handler.select = _select
+            # the client reconnects automatically but the first attempt will
+            # hit a deserialize failure. wait for that.
+            deserialize_ev.wait(5)
+            eq_(deserialize_ev.is_set(), True)
+
+        # this time should succeed
+        ev.wait(5)
+        eq_(ev.is_set(), True)
+        eq_(client.exists(path), None)
+
+    def test_connection_close(self):
+        self.assertRaises(Exception, self.client.close)
+        self.client.stop()
+        self.client.close()
+
+        # should be able to restart
+        self.client.start()
+
+    def test_connection_pipe(self):
+        client = self.client
+        read_pipe = client._connection._read_pipe
+        write_pipe = client._connection._write_pipe
+
+        assert read_pipe is not None
+        assert write_pipe is not None
+
+        # stop client and pipe should not yet be closed
+        client.stop()
+        assert read_pipe is not None
+        assert write_pipe is not None
+        os.fstat(read_pipe)
+        os.fstat(write_pipe)
+
+        # close client, and pipes should be
+        client.close()
+
+        try:
+            os.fstat(read_pipe)
+        except OSError as e:
+            if not e.errno == errno.EBADF:
+                raise
+        else:
+            self.fail("Expected read_pipe to be closed")
+
+        try:
+            os.fstat(write_pipe)
+        except OSError as e:
+            if not e.errno == errno.EBADF:
+                raise
+        else:
+            self.fail("Expected write_pipe to be closed")
+
+        # start client back up. should get a new, valid pipe
+        client.start()
+        read_pipe = client._connection._read_pipe
+        write_pipe = client._connection._write_pipe
+
+        assert read_pipe is not None
+        assert write_pipe is not None
+        os.fstat(read_pipe)
+        os.fstat(write_pipe)
+
+    def test_dirty_pipe(self):
+        client = self.client
+        read_pipe = client._connection._read_pipe
+        write_pipe = client._connection._write_pipe
+
+        # add a stray byte to the pipe and ensure that doesn't
+        # blow up client. simulates case where some error leaves
+        # a byte in the pipe which doesn't correspond to the
+        # request queue.
+        os.write(write_pipe, b'\0')
+
+        # eventually this byte should disappear from pipe
+        wait(lambda: client.handler.select([read_pipe], [], [], 0)[0] == [])
+
+
+class TestConnectionDrop(KazooTestCase):
+    def test_connection_dropped(self):
+        ev = threading.Event()
+
+        def back(state):
+            if state == KazooState.CONNECTED:
+                ev.set()
+
+        # create a node with a large value and stop the ZK node
+        path = "/" + uuid.uuid4().hex
+        self.client.create(path)
+        self.client.add_listener(back)
+        result = self.client.set_async(path, b'a' * 1000 * 1024)
+        self.client._call(_CONNECTION_DROP, None)
+
+        self.assertRaises(ConnectionLoss, result.get)
+        # we have a working connection to a new node
+        ev.wait(30)
+        eq_(ev.is_set(), True)
+
+
+class TestReadOnlyMode(KazooTestCase):
+
+    def setUp(self):
+        self.setup_zookeeper(read_only=True)
+        skip = False
+        if TRAVIS_ZK_VERSION and TRAVIS_ZK_VERSION < (3, 4):
+            skip = True
+        elif TRAVIS_ZK_VERSION and TRAVIS_ZK_VERSION >= (3, 4):
+            skip = False
+        else:
+            ver = self.client.server_version()
+            if ver[1] < 4:
+                skip = True
+        if skip:
+            raise SkipTest("Must use Zookeeper 3.4 or above")
+
+    def tearDown(self):
+        self.client.stop()
+
+    def test_read_only(self):
+        from kazoo.exceptions import NotReadOnlyCallError
+        from kazoo.protocol.states import KeeperState
+
+        client = self.client
+        states = []
+        ev = threading.Event()
+
+        @client.add_listener
+        def listen(state):
+            states.append(state)
+            if client.client_state == KeeperState.CONNECTED_RO:
+                ev.set()
+        try:
+            self.cluster[1].stop()
+            self.cluster[2].stop()
+            ev.wait(6)
+            eq_(ev.is_set(), True)
+            eq_(client.client_state, KeeperState.CONNECTED_RO)
+
+            # Test read only command
+            eq_(client.get_children('/'), [])
+
+            # Test error with write command
+            @raises(NotReadOnlyCallError)
+            def testit():
+                client.create('/fred')
+            testit()
+
+            # Wait for a ping
+            time.sleep(15)
+        finally:
+            client.remove_listener(listen)
+            self.cluster[1].run()
+            self.cluster[2].run()

+ 35 - 0
desktop/core/ext-py/kazoo-2.0/kazoo/tests/test_counter.py

@@ -0,0 +1,35 @@
+import uuid
+
+from nose.tools import eq_
+
+from kazoo.testing import KazooTestCase
+
+
+class KazooCounterTests(KazooTestCase):
+
+    def _makeOne(self, **kw):
+        path = "/" + uuid.uuid4().hex
+        return self.client.Counter(path, **kw)
+
+    def test_int_counter(self):
+        counter = self._makeOne()
+        eq_(counter.value, 0)
+        counter += 2
+        counter + 1
+        eq_(counter.value, 3)
+        counter -= 3
+        counter - 1
+        eq_(counter.value, -1)
+
+    def test_float_counter(self):
+        counter = self._makeOne(default=0.0)
+        eq_(counter.value, 0.0)
+        counter += 2.1
+        eq_(counter.value, 2.1)
+        counter -= 3.1
+        eq_(counter.value, -1.0)
+
+    def test_errors(self):
+        counter = self._makeOne()
+        self.assertRaises(TypeError, counter.__add__, 2.1)
+        self.assertRaises(TypeError, counter.__add__, b"a")

+ 139 - 0
desktop/core/ext-py/kazoo-2.0/kazoo/tests/test_election.py

@@ -0,0 +1,139 @@
+import uuid
+import sys
+import threading
+
+from nose.tools import eq_
+
+from kazoo.testing import KazooTestCase
+from kazoo.tests.util import wait
+
+
+class UniqueError(Exception):
+    """Error raised only by test leader function
+    """
+
+
+class KazooElectionTests(KazooTestCase):
+    def setUp(self):
+        super(KazooElectionTests, self).setUp()
+        self.path = "/" + uuid.uuid4().hex
+
+        self.condition = threading.Condition()
+
+        # election contenders set these when elected. The exit event is set by
+        # the test to make the leader exit.
+        self.leader_id = None
+        self.exit_event = None
+
+        # tests set this before the event to make the leader raise an error
+        self.raise_exception = False
+
+        # set by a worker thread when an unexpected error is hit.
+        # better way to do this?
+        self.thread_exc_info = None
+
+    def _spawn_contender(self, contender_id, election):
+        thread = threading.Thread(target=self._election_thread,
+            args=(contender_id, election))
+        thread.daemon = True
+        thread.start()
+        return thread
+
+    def _election_thread(self, contender_id, election):
+        try:
+            election.run(self._leader_func, contender_id)
+        except UniqueError:
+            if not self.raise_exception:
+                self.thread_exc_info = sys.exc_info()
+        except Exception:
+            self.thread_exc_info = sys.exc_info()
+        else:
+            if self.raise_exception:
+                e = Exception("expected leader func to raise exception")
+                self.thread_exc_info = (Exception, e, None)
+
+    def _leader_func(self, name):
+        exit_event = threading.Event()
+        with self.condition:
+            self.exit_event = exit_event
+            self.leader_id = name
+            self.condition.notify_all()
+
+        exit_event.wait(45)
+        if self.raise_exception:
+            raise UniqueError("expected error in the leader function")
+
+    def _check_thread_error(self):
+        if self.thread_exc_info:
+            t, o, tb = self.thread_exc_info
+            raise t(o)
+
+    def test_election(self):
+        elections = {}
+        threads = {}
+        for _ in range(3):
+            contender = "c" + uuid.uuid4().hex
+            elections[contender] = self.client.Election(self.path, contender)
+            threads[contender] = self._spawn_contender(contender,
+                elections[contender])
+
+        # wait for a leader to be elected
+        times = 0
+        with self.condition:
+            while not self.leader_id:
+                self.condition.wait(5)
+                times += 1
+                if times > 5:
+                    raise Exception("Still not a leader: lid: %s",
+                                    self.leader_id)
+
+        election = self.client.Election(self.path)
+
+        # make sure all contenders are in the pool
+        wait(lambda: len(election.contenders()) == len(elections))
+        contenders = election.contenders()
+
+        eq_(set(contenders), set(elections.keys()))
+
+        # first one in list should be leader
+        first_leader = contenders[0]
+        eq_(first_leader, self.leader_id)
+
+        # tell second one to cancel election. should never get elected.
+        elections[contenders[1]].cancel()
+
+        # make leader exit. third contender should be elected.
+        self.exit_event.set()
+        with self.condition:
+            while self.leader_id == first_leader:
+                self.condition.wait(45)
+        eq_(self.leader_id, contenders[2])
+        self._check_thread_error()
+
+        # make first contender re-enter the race
+        threads[first_leader].join()
+        threads[first_leader] = self._spawn_contender(first_leader,
+            elections[first_leader])
+
+        # contender set should now be the current leader plus the first leader
+        wait(lambda: len(election.contenders()) == 2)
+        contenders = election.contenders()
+        eq_(set(contenders), set([self.leader_id, first_leader]))
+
+        # make current leader raise an exception. first should be reelected
+        self.raise_exception = True
+        self.exit_event.set()
+        with self.condition:
+            while self.leader_id != first_leader:
+                self.condition.wait(45)
+        eq_(self.leader_id, first_leader)
+        self._check_thread_error()
+
+        self.exit_event.set()
+        for thread in threads.values():
+            thread.join()
+        self._check_thread_error()
+
+    def test_bad_func(self):
+        election = self.client.Election(self.path)
+        self.assertRaises(ValueError, election.run, "not a callable")

+ 22 - 0
desktop/core/ext-py/kazoo-2.0/kazoo/tests/test_exceptions.py

@@ -0,0 +1,22 @@
+from unittest import TestCase
+
+
+class ExceptionsTestCase(TestCase):
+
+    def _get(self):
+        from kazoo import exceptions
+        return exceptions
+
+    def test_backwards_alias(self):
+        module = self._get()
+        self.assertTrue(getattr(module, 'NoNodeException'))
+        self.assertTrue(module.NoNodeException, module.NoNodeError)
+
+    def test_exceptions_code(self):
+        module = self._get()
+        exc_8 = module.EXCEPTIONS[-8]
+        self.assertTrue(isinstance(exc_8(), module.BadArgumentsError))
+
+    def test_invalid_code(self):
+        module = self._get()
+        self.assertRaises(RuntimeError, module.EXCEPTIONS.__getitem__, 666)

+ 160 - 0
desktop/core/ext-py/kazoo-2.0/kazoo/tests/test_gevent_handler.py

@@ -0,0 +1,160 @@
+import unittest
+
+from nose import SkipTest
+from nose.tools import eq_
+from nose.tools import raises
+
+from kazoo.client import KazooClient
+from kazoo.exceptions import NoNodeError
+from kazoo.protocol.states import Callback
+from kazoo.testing import KazooTestCase
+from kazoo.tests import test_client
+
+
+class TestGeventHandler(unittest.TestCase):
+
+    def setUp(self):
+        try:
+            import gevent
+        except ImportError:
+            raise SkipTest('gevent not available.')
+
+    def _makeOne(self, *args):
+        from kazoo.handlers.gevent import SequentialGeventHandler
+        return SequentialGeventHandler(*args)
+
+    def _getAsync(self, *args):
+        from kazoo.handlers.gevent import AsyncResult
+        return AsyncResult
+
+    def _getEvent(self):
+        from gevent.event import Event
+        return Event
+
+    def test_proper_threading(self):
+        h = self._makeOne()
+        h.start()
+        assert isinstance(h.event_object(), self._getEvent())
+
+    def test_matching_async(self):
+        h = self._makeOne()
+        h.start()
+        async = self._getAsync()
+        assert isinstance(h.async_result(), async)
+
+    def test_exception_raising(self):
+        h = self._makeOne()
+
+        @raises(h.timeout_exception)
+        def testit():
+            raise h.timeout_exception("This is a timeout")
+        testit()
+
+    def test_exception_in_queue(self):
+        h = self._makeOne()
+        h.start()
+        ev = self._getEvent()()
+
+        def func():
+            ev.set()
+            raise ValueError('bang')
+
+        call1 = Callback('completion', func, ())
+        h.dispatch_callback(call1)
+        ev.wait()
+
+    def test_queue_empty_exception(self):
+        from gevent.queue import Empty
+        h = self._makeOne()
+        h.start()
+        ev = self._getEvent()()
+
+        def func():
+            ev.set()
+            raise Empty()
+
+        call1 = Callback('completion', func, ())
+        h.dispatch_callback(call1)
+        ev.wait()
+
+
+class TestBasicGeventClient(KazooTestCase):
+
+    def setUp(self):
+        try:
+            import gevent
+        except ImportError:
+            raise SkipTest('gevent not available.')
+        KazooTestCase.setUp(self)
+
+    def _makeOne(self, *args):
+        from kazoo.handlers.gevent import SequentialGeventHandler
+        return SequentialGeventHandler(*args)
+
+    def _getEvent(self):
+        from gevent.event import Event
+        return Event
+
+    def test_start(self):
+        client = self._get_client(handler=self._makeOne())
+        client.start()
+        self.assertEqual(client.state, 'CONNECTED')
+        client.stop()
+
+    def test_start_stop_double(self):
+        client = self._get_client(handler=self._makeOne())
+        client.start()
+        self.assertEqual(client.state, 'CONNECTED')
+        client.handler.start()
+        client.handler.stop()
+        client.stop()
+
+    def test_basic_commands(self):
+        client = self._get_client(handler=self._makeOne())
+        client.start()
+        self.assertEqual(client.state, 'CONNECTED')
+        client.create('/anode', 'fred')
+        eq_(client.get('/anode')[0], 'fred')
+        eq_(client.delete('/anode'), True)
+        eq_(client.exists('/anode'), None)
+        client.stop()
+
+    def test_failures(self):
+        client = self._get_client(handler=self._makeOne())
+        client.start()
+        self.assertRaises(NoNodeError, client.get, '/none')
+        client.stop()
+
+    def test_data_watcher(self):
+        client = self._get_client(handler=self._makeOne())
+        client.start()
+        client.ensure_path('/some/node')
+        ev = self._getEvent()()
+
+        @client.DataWatch('/some/node')
+        def changed(d, stat):
+            ev.set()
+
+        ev.wait()
+        ev.clear()
+        client.set('/some/node', 'newvalue')
+        ev.wait()
+        client.stop()
+
+
+class TestGeventClient(test_client.TestClient):
+
+    def setUp(self):
+        try:
+            import gevent
+        except ImportError:
+            raise SkipTest('gevent not available.')
+        KazooTestCase.setUp(self)
+
+    def _makeOne(self, *args):
+        from kazoo.handlers.gevent import SequentialGeventHandler
+        return SequentialGeventHandler(*args)
+
+    def _get_client(self, **kwargs):
+        kwargs["handler"] = self._makeOne()
+        return KazooClient(self.hosts, **kwargs)

+ 517 - 0
desktop/core/ext-py/kazoo-2.0/kazoo/tests/test_lock.py

@@ -0,0 +1,517 @@
+import uuid
+import threading
+
+from nose.tools import eq_, ok_
+
+from kazoo.exceptions import CancelledError
+from kazoo.exceptions import LockTimeout
+from kazoo.testing import KazooTestCase
+from kazoo.tests.util import wait
+
+
+class KazooLockTests(KazooTestCase):
+    def setUp(self):
+        super(KazooLockTests, self).setUp()
+        self.lockpath = "/" + uuid.uuid4().hex
+
+        self.condition = threading.Condition()
+        self.released = threading.Event()
+        self.active_thread = None
+        self.cancelled_threads = []
+
+    def _thread_lock_acquire_til_event(self, name, lock, event):
+        try:
+            with lock:
+                with self.condition:
+                    eq_(self.active_thread, None)
+                    self.active_thread = name
+                    self.condition.notify_all()
+
+                event.wait()
+
+                with self.condition:
+                    eq_(self.active_thread, name)
+                    self.active_thread = None
+                    self.condition.notify_all()
+            self.released.set()
+        except CancelledError:
+            with self.condition:
+                self.cancelled_threads.append(name)
+                self.condition.notify_all()
+
+    def test_lock_one(self):
+        lock_name = uuid.uuid4().hex
+        lock = self.client.Lock(self.lockpath, lock_name)
+        event = threading.Event()
+
+        thread = threading.Thread(target=self._thread_lock_acquire_til_event,
+            args=(lock_name, lock, event))
+        thread.start()
+
+        lock2_name = uuid.uuid4().hex
+        anotherlock = self.client.Lock(self.lockpath, lock2_name)
+
+        # wait for any contender to show up on the lock
+        wait(anotherlock.contenders)
+        eq_(anotherlock.contenders(), [lock_name])
+
+        with self.condition:
+            while self.active_thread != lock_name:
+                self.condition.wait()
+
+        # release the lock
+        event.set()
+
+        with self.condition:
+            while self.active_thread:
+                self.condition.wait()
+        self.released.wait()
+        thread.join()
+
+    def test_lock(self):
+        threads = []
+        names = ["contender" + str(i) for i in range(5)]
+
+        contender_bits = {}
+
+        for name in names:
+            e = threading.Event()
+
+            l = self.client.Lock(self.lockpath, name)
+            t = threading.Thread(target=self._thread_lock_acquire_til_event,
+                args=(name, l, e))
+            contender_bits[name] = (t, e)
+            threads.append(t)
+
+        # acquire the lock ourselves first to make the others line up
+        lock = self.client.Lock(self.lockpath, "test")
+        lock.acquire()
+
+        for t in threads:
+            t.start()
+
+        # wait for everyone to line up on the lock
+        wait(lambda: len(lock.contenders()) == 6)
+        contenders = lock.contenders()
+
+        eq_(contenders[0], "test")
+        contenders = contenders[1:]
+        remaining = list(contenders)
+
+        # release the lock and contenders should claim it in order
+        lock.release()
+
+        for contender in contenders:
+            thread, event = contender_bits[contender]
+
+            with self.condition:
+                while not self.active_thread:
+                    self.condition.wait()
+                eq_(self.active_thread, contender)
+
+            eq_(lock.contenders(), remaining)
+            remaining = remaining[1:]
+
+            event.set()
+
+            with self.condition:
+                while self.active_thread:
+                    self.condition.wait()
+        for thread in threads:
+            thread.join()
+
+    def test_lock_reconnect(self):
+        event = threading.Event()
+        other_lock = self.client.Lock(self.lockpath, 'contender')
+        thread = threading.Thread(target=self._thread_lock_acquire_til_event,
+                                  args=('contender', other_lock, event))
+
+        # acquire the lock ourselves first to make the contender line up
+        lock = self.client.Lock(self.lockpath, "test")
+        lock.acquire()
+
+        thread.start()
+        # wait for the contender to line up on the lock
+        wait(lambda: len(lock.contenders()) == 2)
+        eq_(lock.contenders(), ['test', 'contender'])
+
+        self.expire_session()
+
+        lock.release()
+
+        with self.condition:
+            while not self.active_thread:
+                self.condition.wait()
+            eq_(self.active_thread, 'contender')
+
+        event.set()
+        thread.join()
+
+    def test_lock_non_blocking(self):
+        lock_name = uuid.uuid4().hex
+        lock = self.client.Lock(self.lockpath, lock_name)
+        event = threading.Event()
+
+        thread = threading.Thread(target=self._thread_lock_acquire_til_event,
+            args=(lock_name, lock, event))
+        thread.start()
+
+        lock1 = self.client.Lock(self.lockpath, lock_name)
+
+        # wait for the thread to acquire the lock
+        with self.condition:
+            if not self.active_thread:
+                self.condition.wait(5)
+
+        ok_(not lock1.acquire(blocking=False))
+        eq_(lock.contenders(), [lock_name])  # just one - itself
+
+        event.set()
+        thread.join()
+
+    def test_lock_fail_first_call(self):
+        event1 = threading.Event()
+        lock1 = self.client.Lock(self.lockpath, "one")
+        thread1 = threading.Thread(target=self._thread_lock_acquire_til_event,
+            args=("one", lock1, event1))
+        thread1.start()
+
+        # wait for this thread to acquire the lock
+        with self.condition:
+            if not self.active_thread:
+                self.condition.wait(5)
+                eq_(self.active_thread, "one")
+        eq_(lock1.contenders(), ["one"])
+        event1.set()
+        thread1.join()
+
+    def test_lock_cancel(self):
+        event1 = threading.Event()
+        lock1 = self.client.Lock(self.lockpath, "one")
+        thread1 = threading.Thread(target=self._thread_lock_acquire_til_event,
+            args=("one", lock1, event1))
+        thread1.start()
+
+        # wait for this thread to acquire the lock
+        with self.condition:
+            if not self.active_thread:
+                self.condition.wait(5)
+                eq_(self.active_thread, "one")
+
+        client2 = self._get_client()
+        client2.start()
+        event2 = threading.Event()
+        lock2 = client2.Lock(self.lockpath, "two")
+        thread2 = threading.Thread(target=self._thread_lock_acquire_til_event,
+            args=("two", lock2, event2))
+        thread2.start()
+
+        # this one should block in acquire. check that it is a contender
+        wait(lambda: len(lock2.contenders()) > 1)
+        eq_(lock2.contenders(), ["one", "two"])
+
+        lock2.cancel()
+        with self.condition:
+            if not "two" in self.cancelled_threads:
+                self.condition.wait()
+                assert "two" in self.cancelled_threads
+
+        eq_(lock2.contenders(), ["one"])
+
+        thread2.join()
+        event1.set()
+        thread1.join()
+        client2.stop()
+
+    def test_lock_double_calls(self):
+        lock1 = self.client.Lock(self.lockpath, "one")
+        lock1.acquire()
+        lock1.acquire()
+        lock1.release()
+        lock1.release()
+
+    def test_lock_reacquire(self):
+        lock = self.client.Lock(self.lockpath, "one")
+        lock.acquire()
+        lock.release()
+        lock.acquire()
+        lock.release()
+
+    def test_lock_timeout(self):
+        timeout = 3
+        e = threading.Event()
+        started = threading.Event()
+
+        # In the background thread, acquire the lock and wait thrice the time
+        # that the main thread is going to wait to acquire the lock.
+        lock1 = self.client.Lock(self.lockpath, "one")
+
+        def _thread(lock, event, timeout):
+            with lock:
+                started.set()
+                event.wait(timeout)
+                if not event.isSet():
+                    # Eventually fail to avoid hanging the tests
+                    self.fail("lock2 never timed out")
+
+        t = threading.Thread(target=_thread, args=(lock1, e, timeout * 3))
+        t.start()
+
+        # Start the main thread's kazoo client and try to acquire the lock
+        # but give up after `timeout` seconds
+        client2 = self._get_client()
+        client2.start()
+        started.wait(5)
+        self.assertTrue(started.isSet())
+        lock2 = client2.Lock(self.lockpath, "two")
+        try:
+            lock2.acquire(timeout=timeout)
+        except LockTimeout:
+            # A timeout is the behavior we're expecting, since the background
+            # thread should still be holding onto the lock
+            pass
+        else:
+            self.fail("Main thread unexpectedly acquired the lock")
+        finally:
+            # Cleanup
+            e.set()
+            t.join()
+            client2.stop()
+
+
+class TestSemaphore(KazooTestCase):
+    def setUp(self):
+        super(TestSemaphore, self).setUp()
+        self.lockpath = "/" + uuid.uuid4().hex
+
+        self.condition = threading.Condition()
+        self.released = threading.Event()
+        self.active_thread = None
+        self.cancelled_threads = []
+
+    def test_basic(self):
+        sem1 = self.client.Semaphore(self.lockpath)
+        sem1.acquire()
+        sem1.release()
+
+    def test_lock_one(self):
+        sem1 = self.client.Semaphore(self.lockpath, max_leases=1)
+        sem2 = self.client.Semaphore(self.lockpath, max_leases=1)
+        started = threading.Event()
+        event = threading.Event()
+
+        sem1.acquire()
+
+        def sema_one():
+            started.set()
+            with sem2:
+                event.set()
+
+        thread = threading.Thread(target=sema_one, args=())
+        thread.start()
+        started.wait(10)
+
+        self.assertFalse(event.is_set())
+
+        sem1.release()
+        event.wait(10)
+        self.assert_(event.is_set())
+        thread.join()
+
+    def test_non_blocking(self):
+        sem1 = self.client.Semaphore(
+            self.lockpath, identifier='sem1', max_leases=2)
+        sem2 = self.client.Semaphore(
+            self.lockpath, identifier='sem2', max_leases=2)
+        sem3 = self.client.Semaphore(
+            self.lockpath, identifier='sem3', max_leases=2)
+
+        sem1.acquire()
+        sem2.acquire()
+        ok_(not sem3.acquire(blocking=False))
+        eq_(set(sem1.lease_holders()), set(['sem1', 'sem2']))
+        sem2.release()
+        # the next line isn't required, but avoids timing issues in tests
+        sem3.acquire()
+        eq_(set(sem1.lease_holders()), set(['sem1', 'sem3']))
+        sem1.release()
+        sem3.release()
+
+    def test_non_blocking_release(self):
+        sem1 = self.client.Semaphore(
+            self.lockpath, identifier='sem1', max_leases=1)
+        sem2 = self.client.Semaphore(
+            self.lockpath, identifier='sem2', max_leases=1)
+        sem1.acquire()
+        sem2.acquire(blocking=False)
+
+        # make sure there's no shutdown / cleanup error
+        sem1.release()
+        sem2.release()
+
+    def test_holders(self):
+        started = threading.Event()
+        event = threading.Event()
+
+        def sema_one():
+            with self.client.Semaphore(self.lockpath, 'fred', max_leases=1):
+                started.set()
+                event.wait()
+
+        thread = threading.Thread(target=sema_one, args=())
+        thread.start()
+        started.wait()
+        sem1 = self.client.Semaphore(self.lockpath)
+        holders = sem1.lease_holders()
+        eq_(holders, ['fred'])
+        event.set()
+        thread.join()
+
+    def test_semaphore_cancel(self):
+        sem1 = self.client.Semaphore(self.lockpath, 'fred', max_leases=1)
+        sem2 = self.client.Semaphore(self.lockpath, 'george', max_leases=1)
+        sem1.acquire()
+        started = threading.Event()
+        event = threading.Event()
+
+        def sema_one():
+            started.set()
+            try:
+                with sem2:
+                    started.set()
+            except CancelledError:
+                event.set()
+
+        thread = threading.Thread(target=sema_one, args=())
+        thread.start()
+        started.wait()
+        eq_(sem1.lease_holders(), ['fred'])
+        eq_(event.is_set(), False)
+        sem2.cancel()
+        event.wait()
+        eq_(event.is_set(), True)
+        thread.join()
+
+    def test_multiple_acquire_and_release(self):
+        sem1 = self.client.Semaphore(self.lockpath, 'fred', max_leases=1)
+        sem1.acquire()
+        sem1.acquire()
+
+        eq_(True, sem1.release())
+        eq_(False, sem1.release())
+
+    def test_handle_session_loss(self):
+        expire_semaphore = self.client.Semaphore(self.lockpath, 'fred',
+                                                 max_leases=1)
+
+        client = self._get_client()
+        client.start()
+        lh_semaphore = client.Semaphore(self.lockpath, 'george', max_leases=1)
+        lh_semaphore.acquire()
+
+        started = threading.Event()
+        event = threading.Event()
+        event2 = threading.Event()
+
+        def sema_one():
+            started.set()
+            with expire_semaphore:
+                event.set()
+                event2.wait()
+
+        thread = threading.Thread(target=sema_one, args=())
+        thread.start()
+
+        started.wait()
+        eq_(lh_semaphore.lease_holders(), ['george'])
+
+        # Fired in a separate thread to make sure we can see the effect
+        expired = threading.Event()
+
+        def expire():
+            self.expire_session()
+            expired.set()
+
+        thread = threading.Thread(target=expire, args=())
+        thread.start()
+        expire_semaphore.wake_event.wait()
+        expired.wait()
+
+        lh_semaphore.release()
+        client.stop()
+
+        event.wait(5)
+        eq_(expire_semaphore.lease_holders(), ['fred'])
+        event2.set()
+        thread.join()
+
+    def test_inconsistent_max_leases(self):
+        sem1 = self.client.Semaphore(self.lockpath, max_leases=1)
+        sem2 = self.client.Semaphore(self.lockpath, max_leases=2)
+
+        sem1.acquire()
+        self.assertRaises(ValueError, sem2.acquire)
+
+    def test_inconsistent_max_leases_other_data(self):
+        sem1 = self.client.Semaphore(self.lockpath, max_leases=1)
+        sem2 = self.client.Semaphore(self.lockpath, max_leases=2)
+
+        self.client.ensure_path(self.lockpath)
+        self.client.set(self.lockpath, b'a$')
+
+        sem1.acquire()
+        # sem2 thinks it's ok to have two lease holders
+        ok_(sem2.acquire(blocking=False))
+
+    def test_reacquire(self):
+        lock = self.client.Semaphore(self.lockpath)
+        lock.acquire()
+        lock.release()
+        lock.acquire()
+        lock.release()
+
+    def test_acquire_after_cancelled(self):
+        lock = self.client.Semaphore(self.lockpath)
+        self.assertTrue(lock.acquire())
+        self.assertTrue(lock.release())
+        lock.cancel()
+        self.assertTrue(lock.cancelled)
+        self.assertTrue(lock.acquire())
+
+    def test_timeout(self):
+        timeout = 3
+        e = threading.Event()
+        started = threading.Event()
+
+        # In the background thread, acquire the lock and wait thrice the time
+        # that the main thread is going to wait to acquire the lock.
+        sem1 = self.client.Semaphore(self.lockpath, "one")
+
+        def _thread(sem, event, timeout):
+            with sem:
+                started.set()
+                event.wait(timeout)
+                if not event.isSet():
+                    # Eventually fail to avoid hanging the tests
+                    self.fail("sem2 never timed out")
+
+        t = threading.Thread(target=_thread, args=(sem1, e, timeout * 3))
+        t.start()
+
+        # Start the main thread's kazoo client and try to acquire the lock
+        # but give up after `timeout` seconds
+        client2 = self._get_client()
+        client2.start()
+        started.wait(5)
+        self.assertTrue(started.isSet())
+        sem2 = client2.Semaphore(self.lockpath, "two")
+        try:
+            sem2.acquire(timeout=timeout)
+        except LockTimeout:
+            # A timeout is the behavior we're expecting, since the background
+            # thread will still be holding onto the lock
+            e.set()
+        finally:
+            # Cleanup
+            t.join()
+            client2.stop()

+ 92 - 0
desktop/core/ext-py/kazoo-2.0/kazoo/tests/test_partitioner.py

@@ -0,0 +1,92 @@
+import uuid
+import time
+
+from nose.tools import eq_
+
+from kazoo.testing import KazooTestCase
+from kazoo.recipe.partitioner import PartitionState
+
+
+class KazooPartitionerTests(KazooTestCase):
+    def setUp(self):
+        super(KazooPartitionerTests, self).setUp()
+        self.path = "/" + uuid.uuid4().hex
+
+    def test_party_of_one(self):
+        partitioner = self.client.SetPartitioner(
+            self.path, set=(1, 2, 3), time_boundary=0.2)
+        partitioner.wait_for_acquire(14)
+        eq_(partitioner.state, PartitionState.ACQUIRED)
+        eq_(list(partitioner), [1, 2, 3])
+        partitioner.finish()
+
+    def test_party_of_two(self):
+        partitioners = [self.client.SetPartitioner(self.path, (1, 2),
+                        identifier="p%s" % i, time_boundary=0.2)
+                        for i in range(2)]
+
+        partitioners[0].wait_for_acquire(14)
+        partitioners[1].wait_for_acquire(14)
+        eq_(list(partitioners[0]), [1])
+        eq_(list(partitioners[1]), [2])
+        partitioners[0].finish()
+        time.sleep(0.1)
+        eq_(partitioners[1].release, True)
+        partitioners[1].finish()
+
+    def test_party_expansion(self):
+        partitioners = [self.client.SetPartitioner(self.path, (1, 2, 3),
+                        identifier="p%s" % i, time_boundary=0.2)
+                        for i in range(2)]
+
+        partitioners[0].wait_for_acquire(14)
+        partitioners[1].wait_for_acquire(14)
+        eq_(partitioners[0].state, PartitionState.ACQUIRED)
+        eq_(partitioners[1].state, PartitionState.ACQUIRED)
+
+        eq_(list(partitioners[0]), [1, 3])
+        eq_(list(partitioners[1]), [2])
+
+        # Add another partition, wait till they settle
+        partitioners.append(self.client.SetPartitioner(self.path, (1, 2, 3),
+                            identifier="p2", time_boundary=0.2))
+        time.sleep(0.1)
+        eq_(partitioners[0].release, True)
+        for p in partitioners[:-1]:
+            p.release_set()
+
+        for p in partitioners:
+            p.wait_for_acquire(14)
+
+        eq_(list(partitioners[0]), [1])
+        eq_(list(partitioners[1]), [2])
+        eq_(list(partitioners[2]), [3])
+
+        for p in partitioners:
+            p.finish()
+
+    def test_more_members_than_set_items(self):
+        partitioners = [self.client.SetPartitioner(self.path, (1,),
+                        identifier="p%s" % i, time_boundary=0.2)
+                        for i in range(2)]
+
+        partitioners[0].wait_for_acquire(14)
+        partitioners[1].wait_for_acquire(14)
+        eq_(partitioners[0].state, PartitionState.ACQUIRED)
+        eq_(partitioners[1].state, PartitionState.ACQUIRED)
+
+        eq_(list(partitioners[0]), [1])
+        eq_(list(partitioners[1]), [])
+
+        for p in partitioners:
+            p.finish()
+
+    def test_party_session_failure(self):
+        partitioner = self.client.SetPartitioner(
+            self.path, set=(1, 2, 3), time_boundary=0.2)
+        partitioner.wait_for_acquire(14)
+        eq_(partitioner.state, PartitionState.ACQUIRED)
+        # simulate session failure
+        partitioner._fail_out()
+        partitioner.release_set()
+        self.assertTrue(partitioner.failed)

+ 84 - 0
desktop/core/ext-py/kazoo-2.0/kazoo/tests/test_party.py

@@ -0,0 +1,84 @@
+import uuid
+
+from nose.tools import eq_
+
+from kazoo.testing import KazooTestCase
+
+
+class KazooPartyTests(KazooTestCase):
+    def setUp(self):
+        super(KazooPartyTests, self).setUp()
+        self.path = "/" + uuid.uuid4().hex
+
+    def test_party(self):
+        parties = [self.client.Party(self.path, "p%s" % i)
+                   for i in range(5)]
+
+        one_party = parties[0]
+
+        eq_(list(one_party), [])
+        eq_(len(one_party), 0)
+
+        participants = set()
+        for party in parties:
+            party.join()
+            participants.add(party.data.decode('utf-8'))
+
+            eq_(set(party), participants)
+            eq_(len(party), len(participants))
+
+        for party in parties:
+            party.leave()
+            participants.remove(party.data.decode('utf-8'))
+
+            eq_(set(party), participants)
+            eq_(len(party), len(participants))
+
+    def test_party_reuse_node(self):
+        party = self.client.Party(self.path, "p1")
+        self.client.ensure_path(self.path)
+        self.client.create(party.create_path)
+        party.join()
+        self.assertTrue(party.participating)
+        party.leave()
+        self.assertFalse(party.participating)
+        self.assertEqual(len(party), 0)
+
+    def test_party_vanishing_node(self):
+        party = self.client.Party(self.path, "p1")
+        party.join()
+        self.assertTrue(party.participating)
+        self.client.delete(party.create_path)
+        party.leave()
+        self.assertFalse(party.participating)
+        self.assertEqual(len(party), 0)
+
+
+class KazooShallowPartyTests(KazooTestCase):
+    def setUp(self):
+        super(KazooShallowPartyTests, self).setUp()
+        self.path = "/" + uuid.uuid4().hex
+
+    def test_party(self):
+        parties = [self.client.ShallowParty(self.path, "p%s" % i)
+                   for i in range(5)]
+
+        one_party = parties[0]
+
+        eq_(list(one_party), [])
+        eq_(len(one_party), 0)
+
+        participants = set()
+        for party in parties:
+            party.join()
+            participants.add(party.data.decode('utf-8'))
+
+            eq_(set(party), participants)
+            eq_(len(party), len(participants))
+
+        for party in parties:
+            party.leave()
+            participants.remove(party.data.decode('utf-8'))
+
+            eq_(set(party), participants)
+            eq_(len(party), len(participants))

+ 98 - 0
desktop/core/ext-py/kazoo-2.0/kazoo/tests/test_paths.py

@@ -0,0 +1,98 @@
+import sys
+from unittest import TestCase
+
+from kazoo.protocol import paths
+
+
+if sys.version_info > (3, ):  # pragma: nocover
+    def u(s):
+        return s
+else:  # pragma: nocover
+    def u(s):
+        return unicode(s, "unicode_escape")
+
+
+class NormPathTestCase(TestCase):
+
+    def test_normpath(self):
+        self.assertEqual(paths.normpath('/a/b'), '/a/b')
+
+    def test_normpath_empty(self):
+        self.assertEqual(paths.normpath(''), '')
+
+    def test_normpath_unicode(self):
+        self.assertEqual(paths.normpath(u('/\xe4/b')), u('/\xe4/b'))
+
+    def test_normpath_dots(self):
+        self.assertEqual(paths.normpath('/a./b../c'), '/a./b../c')
+
+    def test_normpath_slash(self):
+        self.assertEqual(paths.normpath('/'), '/')
+
+    def test_normpath_multiple_slashes(self):
+        self.assertEqual(paths.normpath('//'), '/')
+        self.assertEqual(paths.normpath('//a/b'), '/a/b')
+        self.assertEqual(paths.normpath('/a//b//'), '/a/b')
+        self.assertEqual(paths.normpath('//a////b///c/'), '/a/b/c')
+
+    def test_normpath_relative(self):
+        self.assertRaises(ValueError, paths.normpath, './a/b')
+        self.assertRaises(ValueError, paths.normpath, '/a/../b')
+
+
+class JoinTestCase(TestCase):
+
+    def test_join(self):
+        self.assertEqual(paths.join('/a'), '/a')
+        self.assertEqual(paths.join('/a', 'b/'), '/a/b/')
+        self.assertEqual(paths.join('/a', 'b', 'c'), '/a/b/c')
+
+    def test_join_empty(self):
+        self.assertEqual(paths.join(''), '')
+        self.assertEqual(paths.join('', 'a', 'b'), 'a/b')
+        self.assertEqual(paths.join('/a', '', 'b/', 'c'), '/a/b/c')
+
+    def test_join_absolute(self):
+        self.assertEqual(paths.join('/a/b', '/c'), '/c')
+
+
+class IsAbsTestCase(TestCase):
+
+    def test_isabs(self):
+        self.assertTrue(paths.isabs('/'))
+        self.assertTrue(paths.isabs('/a'))
+        self.assertTrue(paths.isabs('/a//b/c'))
+        self.assertTrue(paths.isabs('//a/b'))
+
+    def test_isabs_false(self):
+        self.assertFalse(paths.isabs(''))
+        self.assertFalse(paths.isabs('a/'))
+        self.assertFalse(paths.isabs('a/../'))
+
+
+class BaseNameTestCase(TestCase):
+
+    def test_basename(self):
+        self.assertEquals(paths.basename(''), '')
+        self.assertEquals(paths.basename('/'), '')
+        self.assertEquals(paths.basename('//a'), 'a')
+        self.assertEquals(paths.basename('//a/'), '')
+        self.assertEquals(paths.basename('/a/b.//c..'), 'c..')
+
+
+class PrefixRootTestCase(TestCase):
+
+    def test_prefix_root(self):
+        self.assertEquals(paths._prefix_root('/a/', 'b/c'), '/a/b/c')
+        self.assertEquals(paths._prefix_root('/a/b', 'c/d'), '/a/b/c/d')
+        self.assertEquals(paths._prefix_root('/a', '/b/c'), '/a/b/c')
+        self.assertEquals(paths._prefix_root('/a', '//b/c.'), '/a/b/c.')
+
+
+class NormRootTestCase(TestCase):
+
+    def test_norm_root(self):
+        self.assertEquals(paths._norm_root(''), '/')
+        self.assertEquals(paths._norm_root('/'), '/')
+        self.assertEquals(paths._norm_root('//a'), '/a')
+        self.assertEquals(paths._norm_root('//a./b'), '/a./b')

+ 179 - 0
desktop/core/ext-py/kazoo-2.0/kazoo/tests/test_queue.py

@@ -0,0 +1,179 @@
+import uuid
+
+from nose import SkipTest
+from nose.tools import eq_, ok_
+
+from kazoo.testing import KazooTestCase
+from kazoo.tests.util import TRAVIS_ZK_VERSION
+
+
+class KazooQueueTests(KazooTestCase):
+
+    def _makeOne(self):
+        path = "/" + uuid.uuid4().hex
+        return self.client.Queue(path)
+
+    def test_queue_validation(self):
+        queue = self._makeOne()
+        self.assertRaises(TypeError, queue.put, {})
+        self.assertRaises(TypeError, queue.put, b"one", b"100")
+        self.assertRaises(TypeError, queue.put, b"one", 10.0)
+        self.assertRaises(ValueError, queue.put, b"one", -100)
+        self.assertRaises(ValueError, queue.put, b"one", 100000)
+
+    def test_empty_queue(self):
+        queue = self._makeOne()
+        eq_(len(queue), 0)
+        self.assertTrue(queue.get() is None)
+        eq_(len(queue), 0)
+
+    def test_queue(self):
+        queue = self._makeOne()
+        queue.put(b"one")
+        queue.put(b"two")
+        queue.put(b"three")
+        eq_(len(queue), 3)
+
+        eq_(queue.get(), b"one")
+        eq_(queue.get(), b"two")
+        eq_(queue.get(), b"three")
+        eq_(len(queue), 0)
+
+    def test_priority(self):
+        queue = self._makeOne()
+        queue.put(b"four", priority=101)
+        queue.put(b"one", priority=0)
+        queue.put(b"two", priority=0)
+        queue.put(b"three", priority=10)
+
+        eq_(queue.get(), b"one")
+        eq_(queue.get(), b"two")
+        eq_(queue.get(), b"three")
+        eq_(queue.get(), b"four")
+
+
+class KazooLockingQueueTests(KazooTestCase):
+
+    def setUp(self):
+        KazooTestCase.setUp(self)
+        skip = False
+        if TRAVIS_ZK_VERSION and TRAVIS_ZK_VERSION < (3, 4):
+            skip = True
+        elif TRAVIS_ZK_VERSION and TRAVIS_ZK_VERSION >= (3, 4):
+            skip = False
+        else:
+            ver = self.client.server_version()
+            if ver[1] < 4:
+                skip = True
+        if skip:
+            raise SkipTest("Must use Zookeeper 3.4 or above")
+
+    def _makeOne(self):
+        path = "/" + uuid.uuid4().hex
+        return self.client.LockingQueue(path)
+
+    def test_queue_validation(self):
+        queue = self._makeOne()
+        self.assertRaises(TypeError, queue.put, {})
+        self.assertRaises(TypeError, queue.put, b"one", b"100")
+        self.assertRaises(TypeError, queue.put, b"one", 10.0)
+        self.assertRaises(ValueError, queue.put, b"one", -100)
+        self.assertRaises(ValueError, queue.put, b"one", 100000)
+        self.assertRaises(TypeError, queue.put_all, {})
+        self.assertRaises(TypeError, queue.put_all, [{}])
+        self.assertRaises(TypeError, queue.put_all, [b"one"], b"100")
+        self.assertRaises(TypeError, queue.put_all, [b"one"], 10.0)
+        self.assertRaises(ValueError, queue.put_all, [b"one"], -100)
+        self.assertRaises(ValueError, queue.put_all, [b"one"], 100000)
+
+    def test_empty_queue(self):
+        queue = self._makeOne()
+        eq_(len(queue), 0)
+        self.assertTrue(queue.get(0) is None)
+        eq_(len(queue), 0)
+
+    def test_queue(self):
+        queue = self._makeOne()
+        queue.put(b"one")
+        queue.put_all([b"two", b"three"])
+        eq_(len(queue), 3)
+
+        ok_(not queue.consume())
+        ok_(not queue.holds_lock())
+        eq_(queue.get(1), b"one")
+        ok_(queue.holds_lock())
+        # Without consuming, should return the same element
+        eq_(queue.get(1), b"one")
+        ok_(queue.consume())
+        ok_(not queue.holds_lock())
+        eq_(queue.get(1), b"two")
+        ok_(queue.holds_lock())
+        ok_(queue.consume())
+        ok_(not queue.holds_lock())
+        eq_(queue.get(1), b"three")
+        ok_(queue.holds_lock())
+        ok_(queue.consume())
+        ok_(not queue.holds_lock())
+        ok_(not queue.consume())
+        eq_(len(queue), 0)
+
+    def test_consume(self):
+        queue = self._makeOne()
+
+        queue.put(b"one")
+        ok_(not queue.consume())
+        queue.get(.1)
+        ok_(queue.consume())
+        ok_(not queue.consume())
+
+    def test_holds_lock(self):
+        queue = self._makeOne()
+
+        ok_(not queue.holds_lock())
+        queue.put(b"one")
+        queue.get(.1)
+        ok_(queue.holds_lock())
+        queue.consume()
+        ok_(not queue.holds_lock())
+
+    def test_priority(self):
+        queue = self._makeOne()
+        queue.put(b"four", priority=101)
+        queue.put(b"one", priority=0)
+        queue.put(b"two", priority=0)
+        queue.put(b"three", priority=10)
+
+        eq_(queue.get(1), b"one")
+        ok_(queue.consume())
+        eq_(queue.get(1), b"two")
+        ok_(queue.consume())
+        eq_(queue.get(1), b"three")
+        ok_(queue.consume())
+        eq_(queue.get(1), b"four")
+        ok_(queue.consume())
+
+    def test_concurrent_execution(self):
+        queue = self._makeOne()
+        value1 = []
+        value2 = []
+        value3 = []
+        event1 = self.client.handler.event_object()
+        event2 = self.client.handler.event_object()
+        event3 = self.client.handler.event_object()
+
+        def get_concurrently(value, event):
+            q = self.client.LockingQueue(queue.path)
+            value.append(q.get(.1))
+            event.set()
+
+        self.client.handler.spawn(get_concurrently, value1, event1)
+        self.client.handler.spawn(get_concurrently, value2, event2)
+        self.client.handler.spawn(get_concurrently, value3, event3)
+        queue.put(b"one")
+        event1.wait(.2)
+        event2.wait(.2)
+        event3.wait(.2)
+
+        result = value1 + value2 + value3
+        eq_(result.count(b"one"), 1)
+        eq_(result.count(None), 2)

+ 77 - 0
desktop/core/ext-py/kazoo-2.0/kazoo/tests/test_retry.py

@@ -0,0 +1,77 @@
+import unittest
+
+from nose.tools import eq_
+
+
+class TestRetrySleeper(unittest.TestCase):
+
+    def _pass(self):
+        pass
+
+    def _fail(self, times=1):
+        from kazoo.retry import ForceRetryError
+        scope = dict(times=0)
+
+        def inner():
+            if scope['times'] >= times:
+                pass
+            else:
+                scope['times'] += 1
+                raise ForceRetryError('Failed!')
+        return inner
+
+    def _makeOne(self, *args, **kwargs):
+        from kazoo.retry import KazooRetry
+        return KazooRetry(*args, **kwargs)
+
+    def test_reset(self):
+        retry = self._makeOne(delay=0, max_tries=2)
+        retry(self._fail())
+        eq_(retry._attempts, 1)
+        retry.reset()
+        eq_(retry._attempts, 0)
+
+    def test_too_many_tries(self):
+        from kazoo.retry import RetryFailedError
+        retry = self._makeOne(delay=0)
+        self.assertRaises(RetryFailedError, retry, self._fail(times=999))
+        eq_(retry._attempts, 1)
+
+    def test_maximum_delay(self):
+        def sleep_func(_time):
+            pass
+
+        retry = self._makeOne(delay=10, max_tries=100, sleep_func=sleep_func)
+        retry(self._fail(times=10))
+        self.assertTrue(retry._cur_delay < 4000, retry._cur_delay)
+        # gevent's sleep function is picky about the type
+        eq_(type(retry._cur_delay), float)
+
+    def test_copy(self):
+        _sleep = lambda t: None
+        retry = self._makeOne(sleep_func=_sleep)
+        rcopy = retry.copy()
+        self.assertTrue(rcopy.sleep_func is _sleep)
+
+
+class TestKazooRetry(unittest.TestCase):
+
+    def _makeOne(self, **kw):
+        from kazoo.retry import KazooRetry
+        return KazooRetry(**kw)
+
+    def test_connection_closed(self):
+        from kazoo.exceptions import ConnectionClosedError
+        retry = self._makeOne()
+
+        def testit():
+            raise ConnectionClosedError()
+        self.assertRaises(ConnectionClosedError, retry, testit)
+
+    def test_session_expired(self):
+        from kazoo.exceptions import SessionExpiredError
+        retry = self._makeOne(max_tries=1)
+
+        def testit():
+            raise SessionExpiredError()
+        self.assertRaises(Exception, retry, testit)

+ 40 - 0
desktop/core/ext-py/kazoo-2.0/kazoo/tests/test_security.py

@@ -0,0 +1,40 @@
+import unittest
+
+from nose.tools import eq_
+from kazoo.security import Permissions
+
+
+class TestACL(unittest.TestCase):
+    def _makeOne(self, *args, **kwargs):
+        from kazoo.security import make_acl
+        return make_acl(*args, **kwargs)
+
+    def test_read_acl(self):
+        acl = self._makeOne("digest", ":", read=True)
+        eq_(acl.perms & Permissions.READ, Permissions.READ)
+
+    def test_all_perms(self):
+        acl = self._makeOne("digest", ":", read=True, write=True,
+                            create=True, delete=True, admin=True)
+        for perm in [Permissions.READ, Permissions.CREATE, Permissions.WRITE,
+                     Permissions.DELETE, Permissions.ADMIN]:
+            eq_(acl.perms & perm, perm)
+
+    def test_perm_listing(self):
+        from kazoo.security import ACL
+        f = ACL(15, 'fred')
+        self.assert_('READ' in f.acl_list)
+        self.assert_('WRITE' in f.acl_list)
+        self.assert_('CREATE' in f.acl_list)
+        self.assert_('DELETE' in f.acl_list)
+
+        f = ACL(16, 'fred')
+        self.assert_('ADMIN' in f.acl_list)
+
+        f = ACL(31, 'george')
+        self.assert_('ALL' in f.acl_list)
+
+    def test_perm_repr(self):
+        from kazoo.security import ACL
+        f = ACL(16, 'fred')
+        self.assert_("ACL(perms=16, acl_list=['ADMIN']" in repr(f))

+ 326 - 0
desktop/core/ext-py/kazoo-2.0/kazoo/tests/test_threading_handler.py

@@ -0,0 +1,326 @@
+import threading
+import unittest
+
+import mock
+from nose.tools import assert_raises
+from nose.tools import eq_
+from nose.tools import raises
+
+
+class TestThreadingHandler(unittest.TestCase):
+    def _makeOne(self, *args):
+        from kazoo.handlers.threading import SequentialThreadingHandler
+        return SequentialThreadingHandler(*args)
+
+    def _getAsync(self, *args):
+        from kazoo.handlers.threading import AsyncResult
+        return AsyncResult
+
+    def test_proper_threading(self):
+        h = self._makeOne()
+        h.start()
+        # In Python 3.3 _Event is gone, before Event is function
+        event_class = getattr(threading, '_Event', threading.Event)
+        assert isinstance(h.event_object(), event_class)
+
+    def test_matching_async(self):
+        h = self._makeOne()
+        h.start()
+        async = self._getAsync()
+        assert isinstance(h.async_result(), async)
+
+    def test_exception_raising(self):
+        h = self._makeOne()
+
+        @raises(h.timeout_exception)
+        def testit():
+            raise h.timeout_exception("This is a timeout")
+        testit()
+
+    def test_double_start_stop(self):
+        h = self._makeOne()
+        h.start()
+        self.assertTrue(h._running)
+        h.start()
+        h.stop()
+        h.stop()
+        self.assertFalse(h._running)
+
+
+class TestThreadingAsync(unittest.TestCase):
+    def _makeOne(self, *args):
+        from kazoo.handlers.threading import AsyncResult
+        return AsyncResult(*args)
+
+    def _makeHandler(self):
+        from kazoo.handlers.threading import SequentialThreadingHandler
+        return SequentialThreadingHandler()
+
+    def test_ready(self):
+        mock_handler = mock.Mock()
+        async = self._makeOne(mock_handler)
+
+        eq_(async.ready(), False)
+        async.set('val')
+        eq_(async.ready(), True)
+        eq_(async.successful(), True)
+        eq_(async.exception, None)
+
+    def test_callback_queued(self):
+        mock_handler = mock.Mock()
+        mock_handler.completion_queue = mock.Mock()
+        async = self._makeOne(mock_handler)
+
+        async.rawlink(lambda a: a)
+        async.set('val')
+
+        assert mock_handler.completion_queue.put.called
+
+    def test_set_exception(self):
+        mock_handler = mock.Mock()
+        mock_handler.completion_queue = mock.Mock()
+        async = self._makeOne(mock_handler)
+        async.rawlink(lambda a: a)
+        async.set_exception(ImportError('Error occured'))
+
+        assert isinstance(async.exception, ImportError)
+        assert mock_handler.completion_queue.put.called
+
+    def test_get_wait_while_setting(self):
+        mock_handler = mock.Mock()
+        async = self._makeOne(mock_handler)
+
+        lst = []
+        bv = threading.Event()
+        cv = threading.Event()
+
+        def wait_for_val():
+            bv.set()
+            val = async.get()
+            lst.append(val)
+            cv.set()
+        th = threading.Thread(target=wait_for_val)
+        th.start()
+        bv.wait()
+
+        async.set('fred')
+        cv.wait()
+        eq_(lst, ['fred'])
+        th.join()
+
+    def test_get_with_nowait(self):
+        mock_handler = mock.Mock()
+        async = self._makeOne(mock_handler)
+        timeout = self._makeHandler().timeout_exception
+
+        @raises(timeout)
+        def test_it():
+            async.get(block=False)
+        test_it()
+
+        @raises(timeout)
+        def test_nowait():
+            async.get_nowait()
+        test_nowait()
+
+    def test_get_with_exception(self):
+        mock_handler = mock.Mock()
+        async = self._makeOne(mock_handler)
+
+        lst = []
+        bv = threading.Event()
+        cv = threading.Event()
+
+        def wait_for_val():
+            bv.set()
+            try:
+                val = async.get()
+            except ImportError:
+                lst.append('oops')
+            else:
+                lst.append(val)
+            cv.set()
+        th = threading.Thread(target=wait_for_val)
+        th.start()
+        bv.wait()
+
+        async.set_exception(ImportError)
+        cv.wait()
+        eq_(lst, ['oops'])
+        th.join()
+
+    def test_wait(self):
+        mock_handler = mock.Mock()
+        async = self._makeOne(mock_handler)
+
+        lst = []
+        bv = threading.Event()
+        cv = threading.Event()
+
+        def wait_for_val():
+            bv.set()
+            try:
+                val = async.wait(10)
+            except ImportError:
+                lst.append('oops')
+            else:
+                lst.append(val)
+            cv.set()
+        th = threading.Thread(target=wait_for_val)
+        th.start()
+        bv.wait(10)
+
+        async.set("fred")
+        cv.wait(15)
+        eq_(lst, [True])
+        th.join()
+
+    def test_set_before_wait(self):
+        mock_handler = mock.Mock()
+        async = self._makeOne(mock_handler)
+
+        lst = []
+        cv = threading.Event()
+        async.set('fred')
+
+        def wait_for_val():
+            val = async.get()
+            lst.append(val)
+            cv.set()
+        th = threading.Thread(target=wait_for_val)
+        th.start()
+        cv.wait()
+        eq_(lst, ['fred'])
+        th.join()
+
+    def test_set_exc_before_wait(self):
+        mock_handler = mock.Mock()
+        async = self._makeOne(mock_handler)
+
+        lst = []
+        cv = threading.Event()
+        async.set_exception(ImportError)
+
+        def wait_for_val():
+            try:
+                val = async.get()
+            except ImportError:
+                lst.append('ooops')
+            else:
+                lst.append(val)
+            cv.set()
+        th = threading.Thread(target=wait_for_val)
+        th.start()
+        cv.wait()
+        eq_(lst, ['ooops'])
+        th.join()
+
+    def test_linkage(self):
+        mock_handler = mock.Mock()
+        async = self._makeOne(mock_handler)
+        cv = threading.Event()
+
+        lst = []
+
+        def add_on():
+            lst.append(True)
+
+        def wait_for_val():
+            async.get()
+            cv.set()
+
+        th = threading.Thread(target=wait_for_val)
+        th.start()
+
+        async.rawlink(add_on)
+        async.set('fred')
+        assert mock_handler.completion_queue.put.called
+        async.unlink(add_on)
+        cv.wait()
+        eq_(async.value, 'fred')
+        th.join()
+
+    def test_linkage_not_ready(self):
+        mock_handler = mock.Mock()
+        async = self._makeOne(mock_handler)
+
+        lst = []
+
+        def add_on():
+            lst.append(True)
+
+        async.set('fred')
+        assert not mock_handler.completion_queue.called
+        async.rawlink(add_on)
+        assert mock_handler.completion_queue.put.called
+
+    def test_link_and_unlink(self):
+        mock_handler = mock.Mock()
+        async = self._makeOne(mock_handler)
+
+        lst = []
+
+        def add_on():
+            lst.append(True)
+
+        async.rawlink(add_on)
+        assert not mock_handler.completion_queue.put.called
+        async.unlink(add_on)
+        async.set('fred')
+        assert not mock_handler.completion_queue.put.called
+
+    def test_captured_exception(self):
+        from kazoo.handlers.utils import capture_exceptions
+
+        mock_handler = mock.Mock()
+        async = self._makeOne(mock_handler)
+
+        @capture_exceptions(async)
+        def exceptional_function():
+            return 1/0
+
+        exceptional_function()
+
+        assert_raises(ZeroDivisionError, async.get)
+
+    def test_no_capture_exceptions(self):
+        from kazoo.handlers.utils import capture_exceptions
+
+        mock_handler = mock.Mock()
+        async = self._makeOne(mock_handler)
+
+        lst = []
+
+        def add_on():
+            lst.append(True)
+
+        async.rawlink(add_on)
+
+        @capture_exceptions(async)
+        def regular_function():
+            return True
+
+        regular_function()
+
+        assert not mock_handler.completion_queue.put.called
+
+    def test_wraps(self):
+        from kazoo.handlers.utils import wrap
+
+        mock_handler = mock.Mock()
+        async = self._makeOne(mock_handler)
+
+        lst = []
+
+        def add_on(result):
+            lst.append(result.get())
+
+        async.rawlink(add_on)
+
+        @wrap(async)
+        def regular_function():
+            return 'hello'
+
+        assert regular_function() == 'hello'
+        assert mock_handler.completion_queue.put.called
+        assert async.get() == 'hello'

+ 489 - 0
desktop/core/ext-py/kazoo-2.0/kazoo/tests/test_watchers.py

@@ -0,0 +1,489 @@
+import time
+import threading
+import uuid
+
+from nose.tools import eq_
+from nose.tools import raises
+
+from kazoo.exceptions import KazooException
+from kazoo.protocol.states import EventType
+from kazoo.testing import KazooTestCase
+
+
+class KazooDataWatcherTests(KazooTestCase):
+    def setUp(self):
+        super(KazooDataWatcherTests, self).setUp()
+        self.path = "/" + uuid.uuid4().hex
+        self.client.ensure_path(self.path)
+
+    def test_data_watcher(self):
+        update = threading.Event()
+        data = [True]
+
+        # Make it a non-existent path
+        self.path += 'f'
+
+        @self.client.DataWatch(self.path)
+        def changed(d, stat):
+            data.pop()
+            data.append(d)
+            update.set()
+
+        update.wait(10)
+        eq_(data, [None])
+        update.clear()
+
+        self.client.create(self.path, b'fred')
+        update.wait(10)
+        eq_(data[0], b'fred')
+        update.clear()
+
+    def test_data_watcher_once(self):
+        update = threading.Event()
+        data = [True]
+
+        # Make it a non-existent path
+        self.path += 'f'
+
+        dwatcher = self.client.DataWatch(self.path)
+
+        @dwatcher
+        def changed(d, stat):
+            data.pop()
+            data.append(d)
+            update.set()
+
+        update.wait(10)
+        eq_(data, [None])
+        update.clear()
+
+        @raises(KazooException)
+        def test_it():
+            @dwatcher
+            def func(d, stat):
+                data.pop()
+        test_it()
+
+    def test_data_watcher_with_event(self):
+        # Test that the data watcher gets passed the event, if it
+        # accepts three arguments
+        update = threading.Event()
+        data = [True]
+
+        # Make it a non-existent path
+        self.path += 'f'
+
+        @self.client.DataWatch(self.path)
+        def changed(d, stat, event):
+            data.pop()
+            data.append(event)
+            update.set()
+
+        update.wait(10)
+        eq_(data, [None])
+        update.clear()
+
+        self.client.create(self.path, b'fred')
+        update.wait(10)
+        eq_(data[0].type, EventType.CREATED)
+        update.clear()
+
+    def test_func_style_data_watch(self):
+        update = threading.Event()
+        data = [True]
+
+        # Make it a non-existent path
+        path = self.path + 'f'
+
+        def changed(d, stat):
+            data.pop()
+            data.append(d)
+            update.set()
+        self.client.DataWatch(path, changed)
+
+        update.wait(10)
+        eq_(data, [None])
+        update.clear()
+
+        self.client.create(path, b'fred')
+        update.wait(10)
+        eq_(data[0], b'fred')
+        update.clear()
+
+    def test_datawatch_across_session_expire(self):
+        update = threading.Event()
+        data = [True]
+
+        @self.client.DataWatch(self.path)
+        def changed(d, stat):
+            data.pop()
+            data.append(d)
+            update.set()
+
+        update.wait(10)
+        eq_(data, [b""])
+        update.clear()
+
+        self.expire_session()
+        self.client.retry(self.client.set, self.path, b'fred')
+        update.wait(25)
+        eq_(data[0], b'fred')
+
+    def test_func_stops(self):
+        update = threading.Event()
+        data = [True]
+
+        self.path += "f"
+
+        fail_through = []
+
+        @self.client.DataWatch(self.path)
+        def changed(d, stat):
+            data.pop()
+            data.append(d)
+            update.set()
+            if fail_through:
+                return False
+
+        update.wait(10)
+        eq_(data, [None])
+        update.clear()
+
+        fail_through.append(True)
+        self.client.create(self.path, b'fred')
+        update.wait(10)
+        eq_(data[0], b'fred')
+        update.clear()
+
+        self.client.set(self.path, b'asdfasdf')
+        update.wait(0.2)
+        eq_(data[0], b'fred')
+
+        d, stat = self.client.get(self.path)
+        eq_(d, b'asdfasdf')
+
+    def test_no_such_node(self):
+        args = []
+
+        @self.client.DataWatch("/some/path")
+        def changed(d, stat):
+            args.extend([d, stat])
+
+        eq_(args, [None, None])
+
+    def test_bad_watch_func2(self):
+        counter = 0
+
+        @self.client.DataWatch(self.path)
+        def changed(d, stat):
+            if counter > 0:
+                raise Exception("oops")
+
+        raises(Exception)(changed)
+
+        counter += 1
+        self.client.set(self.path, b'asdfasdf')
+
+    def test_watcher_evaluating_to_false(self):
+        class WeirdWatcher(list):
+            def __call__(self, *args):
+                self.called = True
+        watcher = WeirdWatcher()
+        self.client.DataWatch(self.path, watcher)
+        self.client.set(self.path, b'mwahaha')
+        self.assertTrue(watcher.called)
+
+    def test_watcher_repeat_delete(self):
+        a = []
+        ev = threading.Event()
+
+        self.client.delete(self.path)
+
+        @self.client.DataWatch(self.path)
+        def changed(val, stat):
+            a.append(val)
+            ev.set()
+
+        eq_(a, [None])
+        ev.wait(10)
+        ev.clear()
+        self.client.create(self.path, b'blah')
+        ev.wait(10)
+        eq_(ev.is_set(), True)
+        ev.clear()
+        eq_(a, [None, b'blah'])
+        self.client.delete(self.path)
+        ev.wait(10)
+        eq_(ev.is_set(), True)
+        ev.clear()
+        eq_(a, [None, b'blah', None])
+        self.client.create(self.path, b'blah')
+        ev.wait(10)
+        eq_(ev.is_set(), True)
+        ev.clear()
+        eq_(a, [None, b'blah', None, b'blah'])
+
+    def test_watcher_with_closing(self):
+        a = []
+        ev = threading.Event()
+
+        self.client.delete(self.path)
+
+        @self.client.DataWatch(self.path)
+        def changed(val, stat):
+            a.append(val)
+            ev.set()
+        eq_(a, [None])
+
+        b = False
+        try:
+            self.client.stop()
+        except:
+            b = True
+        eq_(b, False)
+
+
+class KazooChildrenWatcherTests(KazooTestCase):
+    def setUp(self):
+        super(KazooChildrenWatcherTests, self).setUp()
+        self.path = "/" + uuid.uuid4().hex
+        self.client.ensure_path(self.path)
+
+    def test_child_watcher(self):
+        update = threading.Event()
+        all_children = ['fred']
+
+        @self.client.ChildrenWatch(self.path)
+        def changed(children):
+            while all_children:
+                all_children.pop()
+            all_children.extend(children)
+            update.set()
+
+        update.wait(10)
+        eq_(all_children, [])
+        update.clear()
+
+        self.client.create(self.path + '/' + 'smith')
+        update.wait(10)
+        eq_(all_children, ['smith'])
+        update.clear()
+
+        self.client.create(self.path + '/' + 'george')
+        update.wait(10)
+        eq_(sorted(all_children), ['george', 'smith'])
+
+    def test_child_watcher_once(self):
+        update = threading.Event()
+        all_children = ['fred']
+
+        cwatch = self.client.ChildrenWatch(self.path)
+
+        @cwatch
+        def changed(children):
+            while all_children:
+                all_children.pop()
+            all_children.extend(children)
+            update.set()
+
+        update.wait(10)
+        eq_(all_children, [])
+        update.clear()
+
+        @raises(KazooException)
+        def test_it():
+            @cwatch
+            def changed_again(children):
+                update.set()
+        test_it()
+
+    def test_child_watcher_with_event(self):
+        update = threading.Event()
+        events = [True]
+
+        @self.client.ChildrenWatch(self.path, send_event=True)
+        def changed(children, event):
+            events.pop()
+            events.append(event)
+            update.set()
+
+        update.wait(10)
+        eq_(events, [None])
+        update.clear()
+
+        self.client.create(self.path + '/' + 'smith')
+        update.wait(10)
+        eq_(events[0].type, EventType.CHILD)
+        update.clear()
+
+    def test_func_style_child_watcher(self):
+        update = threading.Event()
+        all_children = ['fred']
+
+        def changed(children):
+            while all_children:
+                all_children.pop()
+            all_children.extend(children)
+            update.set()
+
+        self.client.ChildrenWatch(self.path, changed)
+
+        update.wait(10)
+        eq_(all_children, [])
+        update.clear()
+
+        self.client.create(self.path + '/' + 'smith')
+        update.wait(10)
+        eq_(all_children, ['smith'])
+        update.clear()
+
+        self.client.create(self.path + '/' + 'george')
+        update.wait(10)
+        eq_(sorted(all_children), ['george', 'smith'])
+
+    def test_func_stops(self):
+        update = threading.Event()
+        all_children = ['fred']
+
+        fail_through = []
+
+        @self.client.ChildrenWatch(self.path)
+        def changed(children):
+            while all_children:
+                all_children.pop()
+            all_children.extend(children)
+            update.set()
+            if fail_through:
+                return False
+
+        update.wait(10)
+        eq_(all_children, [])
+        update.clear()
+
+        fail_through.append(True)
+        self.client.create(self.path + '/' + 'smith')
+        update.wait(10)
+        eq_(all_children, ['smith'])
+        update.clear()
+
+        self.client.create(self.path + '/' + 'george')
+        update.wait(0.5)
+        eq_(all_children, ['smith'])
+
+    def test_child_watch_session_loss(self):
+        update = threading.Event()
+        all_children = ['fred']
+
+        @self.client.ChildrenWatch(self.path)
+        def changed(children):
+            while all_children:
+                all_children.pop()
+            all_children.extend(children)
+            update.set()
+
+        update.wait(10)
+        eq_(all_children, [])
+        update.clear()
+
+        self.client.create(self.path + '/' + 'smith')
+        update.wait(10)
+        eq_(all_children, ['smith'])
+        update.clear()
+        self.expire_session()
+
+        self.client.retry(self.client.create,
+                          self.path + '/' + 'george')
+        update.wait(20)
+        eq_(sorted(all_children), ['george', 'smith'])
+
+    def test_child_stop_on_session_loss(self):
+        update = threading.Event()
+        all_children = ['fred']
+
+        @self.client.ChildrenWatch(self.path, allow_session_lost=False)
+        def changed(children):
+            while all_children:
+                all_children.pop()
+            all_children.extend(children)
+            update.set()
+
+        update.wait(10)
+        eq_(all_children, [])
+        update.clear()
+
+        self.client.create(self.path + '/' + 'smith')
+        update.wait(10)
+        eq_(all_children, ['smith'])
+        update.clear()
+        self.expire_session()
+
+        self.client.retry(self.client.create,
+                          self.path + '/' + 'george')
+        update.wait(4)
+        eq_(update.is_set(), False)
+        eq_(all_children, ['smith'])
+
+        children = self.client.get_children(self.path)
+        eq_(sorted(children), ['george', 'smith'])
+
+    def test_bad_children_watch_func(self):
+        counter = 0
+
+        @self.client.ChildrenWatch(self.path)
+        def changed(children):
+            if counter > 0:
+                raise Exception("oops")
+
+        raises(Exception)(changed)
+        counter += 1
+        self.client.create(self.path + '/' + 'smith')
+
+
+class KazooPatientChildrenWatcherTests(KazooTestCase):
+    def setUp(self):
+        super(KazooPatientChildrenWatcherTests, self).setUp()
+        self.path = "/" + uuid.uuid4().hex
+
+    def _makeOne(self, *args, **kwargs):
+        from kazoo.recipe.watchers import PatientChildrenWatch
+        return PatientChildrenWatch(*args, **kwargs)
+
+    def test_watch(self):
+        self.client.ensure_path(self.path)
+        watcher = self._makeOne(self.client, self.path, 0.1)
+        result = watcher.start()
+        children, asy = result.get()
+        eq_(len(children), 0)
+        eq_(asy.ready(), False)
+
+        self.client.create(self.path + '/' + 'fred')
+        asy.get(timeout=1)
+        eq_(asy.ready(), True)
+
+    def test_exception(self):
+        from kazoo.exceptions import NoNodeError
+        watcher = self._makeOne(self.client, self.path, 0.1)
+        result = watcher.start()
+
+        @raises(NoNodeError)
+        def testit():
+            result.get()
+        testit()
+
+    def test_watch_iterations(self):
+        self.client.ensure_path(self.path)
+        watcher = self._makeOne(self.client, self.path, 0.5)
+        result = watcher.start()
+        eq_(result.ready(), False)
+
+        time.sleep(0.08)
+        self.client.create(self.path + '/' + uuid.uuid4().hex)
+        eq_(result.ready(), False)
+        time.sleep(0.08)
+        eq_(result.ready(), False)
+        self.client.create(self.path + '/' + uuid.uuid4().hex)
+        time.sleep(0.08)
+        eq_(result.ready(), False)
+
+        children, asy = result.get()
+        eq_(len(children), 2)

+ 126 - 0
desktop/core/ext-py/kazoo-2.0/kazoo/tests/util.py

@@ -0,0 +1,126 @@
+##############################################################################
+#
+# Copyright Zope Foundation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+
+import logging
+import os
+import time
+
+TRAVIS = os.environ.get('TRAVIS', False)
+TRAVIS_ZK_VERSION = TRAVIS and os.environ.get('ZOOKEEPER_VERSION', None)
+if TRAVIS_ZK_VERSION:
+    TRAVIS_ZK_VERSION = tuple([int(n) for n in TRAVIS_ZK_VERSION.split('.')])
+
+
+class Handler(logging.Handler):
+
+    def __init__(self, *names, **kw):
+        logging.Handler.__init__(self)
+        self.names = names
+        self.records = []
+        self.setLoggerLevel(**kw)
+
+    def setLoggerLevel(self, level=1):
+        self.level = level
+        self.oldlevels = {}
+
+    def emit(self, record):
+        self.records.append(record)
+
+    def clear(self):
+        del self.records[:]
+
+    def install(self):
+        for name in self.names:
+            logger = logging.getLogger(name)
+            self.oldlevels[name] = logger.level
+            logger.setLevel(self.level)
+            logger.addHandler(self)
+
+    def uninstall(self):
+        for name in self.names:
+            logger = logging.getLogger(name)
+            logger.setLevel(self.oldlevels[name])
+            logger.removeHandler(self)
+
+    def __str__(self):
+        return '\n'.join(
+            [("%s %s\n  %s" %
+              (record.name, record.levelname,
+               '\n'.join([line
+                          for line in record.getMessage().split('\n')
+                          if line.strip()])
+               )
+              )
+              for record in self.records]
+              )
+
+
+class InstalledHandler(Handler):
+
+    def __init__(self, *names, **kw):
+        Handler.__init__(self, *names, **kw)
+        self.install()
+
+
+class Wait(object):
+
+    class TimeOutWaitingFor(Exception):
+        "A test condition timed out"
+
+    timeout = 9
+    wait = .01
+
+    def __init__(self, timeout=None, wait=None, exception=None,
+                 getnow=(lambda: time.time), getsleep=(lambda: time.sleep)):
+
+        if timeout is not None:
+            self.timeout = timeout
+
+        if wait is not None:
+            self.wait = wait
+
+        if exception is not None:
+            self.TimeOutWaitingFor = exception
+
+        self.getnow = getnow
+        self.getsleep = getsleep
+
+    def __call__(self, func=None, timeout=None, wait=None, message=None):
+        if func is None:
+            return lambda func: self(func, timeout, wait, message)
+
+        if func():
+            return
+
+        now = self.getnow()
+        sleep = self.getsleep()
+        if timeout is None:
+            timeout = self.timeout
+        if wait is None:
+            wait = self.wait
+        wait = float(wait)
+
+        deadline = now() + timeout
+        while 1:
+            sleep(wait)
+            if func():
+                return
+            if now() > deadline:
+                raise self.TimeOutWaitingFor(
+                    message or
+                    getattr(func, '__doc__') or
+                    getattr(func, '__name__')
+                    )
+
+wait = Wait()

+ 3 - 0
desktop/core/ext-py/kazoo-2.0/requirements.txt

@@ -0,0 +1,3 @@
+coverage==3.7.1
+mock==1.0.1
+nose==1.3.1

+ 1 - 0
desktop/core/ext-py/kazoo-2.0/requirements_gevent.txt

@@ -0,0 +1 @@
+greenlet==0.4.2

+ 4 - 0
desktop/core/ext-py/kazoo-2.0/requirements_sphinx.txt

@@ -0,0 +1,4 @@
+Jinja2==2.7.2
+Pygments==1.6
+Sphinx==1.2.2
+docutils==0.11

+ 14 - 0
desktop/core/ext-py/kazoo-2.0/setup.cfg

@@ -0,0 +1,14 @@
+[easy_install]
+
+[egg_info]
+tag_build = 
+tag_date = 0
+tag_svn_revision = 0
+
+[nosetests]
+cover-package = kazoo
+nocapture = 1
+cover-erase = 1
+where = kazoo
+cover-inclusive = 1
+

+ 73 - 0
desktop/core/ext-py/kazoo-2.0/setup.py

@@ -0,0 +1,73 @@
+__version__ = '2.0'
+
+import os
+import sys
+
+from setuptools import setup, find_packages
+
+here = os.path.abspath(os.path.dirname(__file__))
+with open(os.path.join(here, 'README.rst')) as f:
+    README = f.read()
+with open(os.path.join(here, 'CHANGES.rst')) as f:
+    CHANGES = f.read()
+
+PYTHON3 = sys.version_info > (3, )
+PYPY = getattr(sys, 'pypy_version_info', False) and True or False
+
+install_requires = []
+
+tests_require = install_requires + [
+    'coverage',
+    'mock',
+    'nose',
+]
+
+if not (PYTHON3 or PYPY):
+    tests_require += [
+        'gevent',
+    ]
+
+on_rtd = os.environ.get('READTHEDOCS', None) == 'True'
+if on_rtd:
+    install_requires.extend([
+        'gevent',
+    ])
+
+setup(
+    name='kazoo',
+    version=__version__,
+    description='Higher Level Zookeeper Client',
+    long_description=README + '\n\n' + CHANGES,
+    classifiers=[
+        "Development Status :: 5 - Production/Stable",
+        "License :: OSI Approved :: Apache Software License",
+        "Intended Audience :: Developers",
+        "Operating System :: OS Independent",
+        "Programming Language :: Python",
+        "Programming Language :: Python :: 2",
+        "Programming Language :: Python :: 2.6",
+        "Programming Language :: Python :: 2.7",
+        "Programming Language :: Python :: 3",
+        "Programming Language :: Python :: 3.3",
+        "Programming Language :: Python :: 3.4",
+        "Programming Language :: Python :: Implementation :: CPython",
+        "Programming Language :: Python :: Implementation :: PyPy",
+        "Topic :: Communications",
+        "Topic :: System :: Distributed Computing",
+        "Topic :: System :: Networking",
+    ],
+    keywords='zookeeper lock leader configuration',
+    author="Kazoo team",
+    author_email="python-zk@googlegroups.com",
+    url="https://kazoo.readthedocs.org",
+    license="Apache 2.0",
+    packages=find_packages(),
+    test_suite="kazoo.tests",
+    include_package_data=True,
+    zip_safe=False,
+    install_requires=install_requires,
+    tests_require=tests_require,
+    extras_require={
+        'test': tests_require,
+    },
+)