from . import mesh
from .utils import (inherit_doc, getUTC, sanitize_packet)
from .base import (flags, to_base_58, from_base_58)
try:
from .cbase import protocol
except:
from .base import protocol
from collections import namedtuple
default_protocol = protocol('sync', "Plaintext") # SSL")
[docs]class sync_socket(mesh.mesh_socket):
"""This class is used to sync dictionaries between programs. It extends
:py:class:`py2p.mesh.mesh_socket`
Because of this inheritance, this can also be used as an alert network
This also implements and optional leasing system by default. This leasing
system means that if node A sets a key, node B cannot overwrite the value
at that key for an hour.
This may be turned off by adding ``leasing=False`` to the constructor.
"""
[docs] def __init__(self, addr, port, prot=default_protocol, out_addr=None,
debug_level=0, leasing=True):
protocol_used = protocol(prot[0] + str(int(leasing)), prot[1])
self.__leasing = leasing
super(sync_socket, self).__init__(
addr, port, protocol_used, out_addr, debug_level)
self.data = {}
self.metadata = {}
self.register_handler(self.__handle_store)
def __store(self, key, new_data, new_meta, error=True):
"""Private API method for storing data. You have permission to store
something if:
- The network is not enforcing leases, or
- There is no value at that key, or
- The lease on that key has lapsed (not been set in the last hour), or
- You are the owner of that key
Args:
key: The key you wish to store data at
new_data: The data you wish to store in said key
new_meta: The metadata associated with this storage
error: A boolean which says whether to raise a
:py:class:`KeyError` if you can't store there
Raises:
KeyError: If someone else has a lease at this value, and ``error``
is ``True``
"""
meta = self.metadata.get(key, None)
if ((not meta) or
(meta.owner == new_meta.owner) or
(meta.timestamp < getUTC() - 3600) or
(meta.timestamp == new_meta.timestamp and
meta.owner > new_meta.owner) or
(meta.timestamp < new_meta.timestamp and
not self.__leasing)):
if new_data not in ('', b''):
self.metadata[key] = new_meta
self.data[key] = new_data
else:
del self.data[key]
del self.metadata[key]
elif error:
raise KeyError("You don't have permission to change this yet")
[docs] def _send_handshake_response(self, handler):
"""Shortcut method to send a handshake response. This method is
extracted from :py:meth:`.__handle_handshake` in order to allow
cleaner inheritence from :py:class:`py2p.sync.sync_socket`
"""
super(sync_socket, self)._send_handshake_response(handler)
for key in self:
meta = self.metadata[key]
handler.send(flags.whisper, flags.store, key, self[key],
meta.owner, to_base_58(meta.timestamp))
def __handle_store(self, msg, handler):
"""This callback is used to deal with data storage signals. Its two
primary jobs are:
- store data in a given key
- delete data in a given key
Args:
msg: A :py:class:`~py2p.base.message`
handler: A :py:class:`~py2p.mesh.mesh_connection`
Returns:
Either ``True`` or ``None``
"""
packets = msg.packets
if packets[0] == flags.store:
meta = metatuple(msg.sender, msg.time)
if len(packets) == 5:
if self.data.get(packets[1]):
return
meta = metatuple(packets[3], from_base_58(packets[4]))
self.__store(packets[1], packets[2], meta, error=False)
return True
def __setitem__(self, key, data):
"""Updates the value at a given key.
Args:
key: The key that you wish to update. Must be a :py:class:`str`
or :py:class:`bytes`-like object
value: The value you wish to put at this key. Must be a
:py:class:`str` or :py:class:`bytes`-like object
Raises:
KeyError: If you do not have the lease for this slot. Lease is
given automatically for one hour if the slot is open.
"""
new_meta = metatuple(self.id, getUTC())
key = sanitize_packet(key)
data = sanitize_packet(data)
self.__store(key, data, new_meta)
if data in ('', b''):
self.send(key, '', type=flags.store)
else:
self.send(key, data, type=flags.store)
@inherit_doc(__setitem__)
[docs] def set(self, key, data):
self.__setitem__(key, data)
[docs] def update(self, update_dict):
"""Equivalent to :py:meth:`dict.update`
This calls :py:meth:`.sync_socket.__setitem__` for each key/value
pair in the given dictionary.
Args:
update_dict: A :py:class:`dict`-like object to extract key/value
pairs from. Key and value be a :py:class:`str`
or :py:class:`bytes`-like object
Raises:
KeyError: If you do not have the lease for this slot. Lease is
given automatically for one hour if the slot is open.
"""
for key in update_dict:
value = update_dict[key]
self.__setitem__(key, value)
def __getitem__(self, key):
"""Looks up the value at a given key.
Args:
key: The key that you wish to check. Must be a :py:class:`str` or
:py:class:`bytes`-like object
Returns:
The value at said key
Raises:
KeyError: If there is no value assigned at that key
"""
key = sanitize_packet(key)
return self.data[key]
[docs] def get(self, key, ifError=None):
"""Retrieves the value at a given key.
Args:
key: The key that you wish to check. Must be a :py:class:`str` or
:py:class:`bytes`-like object
ifError: The value you wish to return on exception (default: ``None``)
Returns:
The value at said key, or the value at ifError if there's an Exception
"""
key = sanitize_packet(key)
return self.data.get(key, ifError)
def __len__(self):
return len(self.data)
def __delitem__(self, key):
self[key] = ''
[docs] def keys(self):
"""Returns an iterator of the underlying :py:class:`dict`s keys"""
if hasattr(self.data, 'iterkeys'):
return (key for key in self.data.keys() if key in self.data)
return (key for key in tuple(self.data.keys()) if key in self.data)
@inherit_doc(keys)
[docs] def __iter__(self):
return self.keys()
[docs] def values(self):
"""Returns an iterator of the underlying :py:class:`dict`s values"""
return (self[key] for key in self.keys())
[docs] def items(self):
"""Returns an iterator of the underlying :py:class:`dict`s items"""
return ((key, self[key]) for key in self.keys())
[docs] def pop(self, key, *args):
"""Returns a value, with the side effect of deleting that association
Args:
Key: The key you wish to look up. Must be a :py:class:`str`
or :py:class:`bytes`-like object
ifError: The value you wish to return on Exception
(default: raise an Exception)
Returns:
The value of the supplied key, or ``ifError``
Raises:
KeyError: If the key does not have an associated value
"""
if len(args):
ret = self.get(key, args[0])
if ret != args[0]:
del self[key]
else:
ret = self[key]
del self[key]
return ret
[docs] def popitem(self):
"""Returns an association, with the side effect of deleting that
association
Returns:
An arbitrary association
"""
key, value = next(self.items())
del self[key]
return (key, value)
[docs] def copy(self):
"""Returns a :py:class:`dict` copy of this synchronized hash table"""
return self.data.copy()