from .datatype import Datatype
from . import TYPES
from collections import Mapping
from riak.util import lazy_property
class TypedMapView(Mapping):
'''
Implements a sort of view over a :class:`Map`, filtered by the embedded
datatype.
'''
def __init__(self, parent, datatype):
self.map = parent
self.datatype = datatype
# Mapping API
def __getitem__(self, key):
'''
Fetches an item from the parent :class:`Map` scoped by this view's
datatype.
:param key: the key of the item
:type key: str
:rtype: :class:`~aioriak.datatypes.Datatype`
'''
return self.map[(key, self.datatype)]
def __iter__(self):
'''
Iterates over all keys in the :class:`Map` scoped by this view's
datatype.
'''
for key in self.map.value:
name, datatype = key
if datatype == self.datatype:
yield name
def __len__(self):
'''
Returns the number of keys in this map scoped by this view's datatype.
'''
return len(iter(self))
def __contains__(self, key):
'''
Determines whether the given key with this view's datatype is in the
parent :class:`Map`.
'''
return (key, self.datatype) in self.map
# From the MutableMapping API
def __delitem__(self, key):
'''
Removes the key with this view's datatype from the parent :class:`Map`.
'''
del self.map[(key, self.datatype)]
[docs]class Map(Mapping, Datatype):
'''
A convergent datatype that acts as a key-value datastructure. Keys
are pairs of ``(name, datatype)`` where ``name`` is a string and
``datatype`` is the datatype name. Values are other convergent
datatypes, represented by any concrete type in this module.
You cannot set values in the map directly (it does not implement
``__setitem__``), but you may add new empty values or access
non-existing values directly via bracket syntax. If a key is not in the
original value of the map when accessed, fetching the key will cause
its associated value to be created.::
map[('name', 'register')]
Keys and their associated values may be deleted from the map as
you would in a dict::
del map[('emails', 'set')]
Convenience accessors exist that partition the map's keys by
datatype and implement the collections.Mapping
behavior as well as supporting deletion::
map.sets['emails']
map.registers['name']
del map.counters['likes']
'''
type_name = 'map'
_type_error_msg = "Map must be a dict with (name, type) keys"
def _default_value(self):
return dict()
def _post_init(self):
self._removes = set()
self._updates = {}
@lazy_property
def counters(self):
'''
Filters keys in the map to only those of counter types.
Example::
map.counters['views'].increment()
del map.counters['points']
'''
return TypedMapView(self, 'counter')
@lazy_property
def flags(self):
'''
Filters keys in the map to only those of flag types. Example::
map.flags['confirmed'].enable()
del map.flags['attending']
'''
return TypedMapView(self, 'flag')
@lazy_property
def maps(self):
'''
Filters keys in the map to only those of map types. Example::
map.maps['emails'].registers['home'].set("user@example.com")
del map.maps['spam']
'''
return TypedMapView(self, 'map')
@lazy_property
def registers(self):
'''
Filters keys in the map to only those of register types. Example::
map.registers['username'].set_value("riak-user")
del map.registers['access_key']
'''
return TypedMapView(self, 'register')
@lazy_property
def sets(self):
'''
Filters keys in the map to only those of set types. Example::
map.sets['friends'].add("brett")
del map.sets['favorites']
'''
return TypedMapView(self, 'set')
def __contains__(self, key):
'''
A map contains a key if that key exists in the original value
or has been added or mutated.
:rtype: bool
'''
self._check_key(key)
return (key in self._value) or (key in self._updates)
# collections.Mapping API
def __getitem__(self, key):
'''
Fetches a convergent datatype at the given key.
.. note: If the key is not in the map, a new empty datatype
will be inserted at that key and returned. If the key was
previously deleted, that mutation will be discarded.
:param key: the key of the value to fetch
:type key: tuple
:rtype: :class:`Datatype` matching the datatype in the key
'''
self._check_key(key)
if key in self._value:
return self._value[key]
else:
# If the key does not exist, we assume they are wanting to
# create a new one with that name/type.
if key not in self._updates:
self._updates[key] = TYPES[key[1]](context=self.context)
return self._updates[key]
def __iter__(self):
'''
Iterates over the *immutable* original value of the map.
'''
return iter(self.value)
def __len__(self):
'''
Returns the size of the original value of the map.
'''
return len(self._value)
def __delitem__(self, key):
'''
Deletes a key from the map. If you have previously mutated the
datatype associated with this key, those mutations will be
discarded.
.. note: You may delete keys that are not entries in the map.
If the Riak server does not find the entry in the set, an
error may be returned to the client. For safety, always
submit removal operations with a context.
:param key: the key to remove
:type key: tuple
'''
# NB: deleting a key only marks it deleted, and you can delete
# things that don't appear in the value!
self._check_key(key)
self._require_context()
self._removes.add(key)
def _check_key(self, key):
'''
Ensures well-formedness of a key.
'''
if not len(key) == 2:
raise TypeError('invalid key: %r' % key)
elif key[1] not in TYPES:
raise TypeError('invalid datatype: %s' % key[1])
# Datatype API
@Datatype.value.getter
def value(self):
'''
Returns a copy of the original map's value. Nested values are
pure Python values as returned by :attr:`Datatype.value` from
the nested types.
:rtype: dict
'''
pvalue = {}
for key in self._value:
pvalue[key] = self._value[key].value
return pvalue
@Datatype.modified.getter
def modified(self):
'''
Whether the map has staged local modifications.
'''
if self._removes:
return True
for v in self._value:
if self._value[v].modified:
return True
for v in self._updates:
if self._updates[v].modified:
return True
return False
def to_op(self):
'''
Extracts the modification operation(s) from the map.
:rtype: list, None
'''
removes = [('remove', r) for r in self._removes]
value_updates = list(self._extract_updates(self._value))
new_updates = list(self._extract_updates(self._updates))
all_updates = removes + value_updates + new_updates
if all_updates:
return all_updates
else:
return None
def _check_type(self, value):
for key in value:
try:
self._check_key(key)
except:
return False
return True
def _coerce_value(self, new_value):
cvalue = {}
for key in new_value:
cvalue[key] = TYPES[key[1]](value=new_value[key],
context=self._context)
return cvalue
def _extract_updates(self, d):
for key in d:
if d[key].modified:
yield ('update', key, d[key].to_op())
TYPES[Map.type_name] = Map