diff planemo/lib/python3.7/site-packages/boto/dynamodb2/items.py @ 0:d30785e31577 draft

"planemo upload commit 6eee67778febed82ddd413c3ca40b3183a3898f1"
author guerler
date Fri, 31 Jul 2020 00:18:57 -0400
parents
children
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/planemo/lib/python3.7/site-packages/boto/dynamodb2/items.py	Fri Jul 31 00:18:57 2020 -0400
@@ -0,0 +1,473 @@
+from copy import deepcopy
+
+
+class NEWVALUE(object):
+    # A marker for new data added.
+    pass
+
+
+class Item(object):
+    """
+    An object representing the item data within a DynamoDB table.
+
+    An item is largely schema-free, meaning it can contain any data. The only
+    limitation is that it must have data for the fields in the ``Table``'s
+    schema.
+
+    This object presents a dictionary-like interface for accessing/storing
+    data. It also tries to intelligently track how data has changed throughout
+    the life of the instance, to be as efficient as possible about updates.
+
+    Empty items, or items that have no data, are considered falsey.
+
+    """
+    def __init__(self, table, data=None, loaded=False):
+        """
+        Constructs an (unsaved) ``Item`` instance.
+
+        To persist the data in DynamoDB, you'll need to call the ``Item.save``
+        (or ``Item.partial_save``) on the instance.
+
+        Requires a ``table`` parameter, which should be a ``Table`` instance.
+        This is required, as DynamoDB's API is focus around all operations
+        being table-level. It's also for persisting schema around many objects.
+
+        Optionally accepts a ``data`` parameter, which should be a dictionary
+        of the fields & values of the item. Alternatively, an ``Item`` instance
+        may be provided from which to extract the data.
+
+        Optionally accepts a ``loaded`` parameter, which should be a boolean.
+        ``True`` if it was preexisting data loaded from DynamoDB, ``False`` if
+        it's new data from the user. Default is ``False``.
+
+        Example::
+
+            >>> users = Table('users')
+            >>> user = Item(users, data={
+            ...     'username': 'johndoe',
+            ...     'first_name': 'John',
+            ...     'date_joined': 1248o61592,
+            ... })
+
+            # Change existing data.
+            >>> user['first_name'] = 'Johann'
+            # Add more data.
+            >>> user['last_name'] = 'Doe'
+            # Delete data.
+            >>> del user['date_joined']
+
+            # Iterate over all the data.
+            >>> for field, val in user.items():
+            ...     print "%s: %s" % (field, val)
+            username: johndoe
+            first_name: John
+            date_joined: 1248o61592
+
+        """
+        self.table = table
+        self._loaded = loaded
+        self._orig_data = {}
+        self._data = data
+        self._dynamizer = table._dynamizer
+
+        if isinstance(self._data, Item):
+            self._data = self._data._data
+        if self._data is None:
+            self._data = {}
+
+        if self._loaded:
+            self._orig_data = deepcopy(self._data)
+
+    def __getitem__(self, key):
+        return self._data.get(key, None)
+
+    def __setitem__(self, key, value):
+        self._data[key] = value
+
+    def __delitem__(self, key):
+        if not key in self._data:
+            return
+
+        del self._data[key]
+
+    def keys(self):
+        return self._data.keys()
+
+    def values(self):
+        return self._data.values()
+
+    def items(self):
+        return self._data.items()
+
+    def get(self, key, default=None):
+        return self._data.get(key, default)
+
+    def __iter__(self):
+        for key in self._data:
+            yield self._data[key]
+
+    def __contains__(self, key):
+        return key in self._data
+
+    def __bool__(self):
+        return bool(self._data)
+
+    __nonzero__ = __bool__
+
+    def _determine_alterations(self):
+        """
+        Checks the ``-orig_data`` against the ``_data`` to determine what
+        changes to the data are present.
+
+        Returns a dictionary containing the keys ``adds``, ``changes`` &
+        ``deletes``, containing the updated data.
+        """
+        alterations = {
+            'adds': {},
+            'changes': {},
+            'deletes': [],
+        }
+
+        orig_keys = set(self._orig_data.keys())
+        data_keys = set(self._data.keys())
+
+        # Run through keys we know are in both for changes.
+        for key in orig_keys.intersection(data_keys):
+            if self._data[key] != self._orig_data[key]:
+                if self._is_storable(self._data[key]):
+                    alterations['changes'][key] = self._data[key]
+                else:
+                    alterations['deletes'].append(key)
+
+        # Run through additions.
+        for key in data_keys.difference(orig_keys):
+            if self._is_storable(self._data[key]):
+                alterations['adds'][key] = self._data[key]
+
+        # Run through deletions.
+        for key in orig_keys.difference(data_keys):
+            alterations['deletes'].append(key)
+
+        return alterations
+
+    def needs_save(self, data=None):
+        """
+        Returns whether or not the data has changed on the ``Item``.
+
+        Optionally accepts a ``data`` argument, which accepts the output from
+        ``self._determine_alterations()`` if you've already called it. Typically
+        unnecessary to do. Default is ``None``.
+
+        Example:
+
+            >>> user.needs_save()
+            False
+            >>> user['first_name'] = 'Johann'
+            >>> user.needs_save()
+            True
+
+        """
+        if data is None:
+            data = self._determine_alterations()
+
+        needs_save = False
+
+        for kind in ['adds', 'changes', 'deletes']:
+            if len(data[kind]):
+                needs_save = True
+                break
+
+        return needs_save
+
+    def mark_clean(self):
+        """
+        Marks an ``Item`` instance as no longer needing to be saved.
+
+        Example:
+
+            >>> user.needs_save()
+            False
+            >>> user['first_name'] = 'Johann'
+            >>> user.needs_save()
+            True
+            >>> user.mark_clean()
+            >>> user.needs_save()
+            False
+
+        """
+        self._orig_data = deepcopy(self._data)
+
+    def mark_dirty(self):
+        """
+        DEPRECATED: Marks an ``Item`` instance as needing to be saved.
+
+        This method is no longer necessary, as the state tracking on ``Item``
+        has been improved to automatically detect proper state.
+        """
+        return
+
+    def load(self, data):
+        """
+        This is only useful when being handed raw data from DynamoDB directly.
+        If you have a Python datastructure already, use the ``__init__`` or
+        manually set the data instead.
+
+        Largely internal, unless you know what you're doing or are trying to
+        mix the low-level & high-level APIs.
+        """
+        self._data = {}
+
+        for field_name, field_value in data.get('Item', {}).items():
+            self[field_name] = self._dynamizer.decode(field_value)
+
+        self._loaded = True
+        self._orig_data = deepcopy(self._data)
+
+    def get_keys(self):
+        """
+        Returns a Python-style dict of the keys/values.
+
+        Largely internal.
+        """
+        key_fields = self.table.get_key_fields()
+        key_data = {}
+
+        for key in key_fields:
+            key_data[key] = self[key]
+
+        return key_data
+
+    def get_raw_keys(self):
+        """
+        Returns a DynamoDB-style dict of the keys/values.
+
+        Largely internal.
+        """
+        raw_key_data = {}
+
+        for key, value in self.get_keys().items():
+            raw_key_data[key] = self._dynamizer.encode(value)
+
+        return raw_key_data
+
+    def build_expects(self, fields=None):
+        """
+        Builds up a list of expecations to hand off to DynamoDB on save.
+
+        Largely internal.
+        """
+        expects = {}
+
+        if fields is None:
+            fields = list(self._data.keys()) + list(self._orig_data.keys())
+
+        # Only uniques.
+        fields = set(fields)
+
+        for key in fields:
+            expects[key] = {
+                'Exists': True,
+            }
+            value = None
+
+            # Check for invalid keys.
+            if not key in self._orig_data and not key in self._data:
+                raise ValueError("Unknown key %s provided." % key)
+
+            # States:
+            # * New field (only in _data)
+            # * Unchanged field (in both _data & _orig_data, same data)
+            # * Modified field (in both _data & _orig_data, different data)
+            # * Deleted field (only in _orig_data)
+            orig_value = self._orig_data.get(key, NEWVALUE)
+            current_value = self._data.get(key, NEWVALUE)
+
+            if orig_value == current_value:
+                # Existing field unchanged.
+                value = current_value
+            else:
+                if key in self._data:
+                    if not key in self._orig_data:
+                        # New field.
+                        expects[key]['Exists'] = False
+                    else:
+                        # Existing field modified.
+                        value = orig_value
+                else:
+                   # Existing field deleted.
+                    value = orig_value
+
+            if value is not None:
+                expects[key]['Value'] = self._dynamizer.encode(value)
+
+        return expects
+
+    def _is_storable(self, value):
+        # We need to prevent ``None``, empty string & empty set from
+        # heading to DDB, but allow false-y values like 0 & False make it.
+        if not value:
+            if not value in (0, 0.0, False):
+                return False
+
+        return True
+
+    def prepare_full(self):
+        """
+        Runs through all fields & encodes them to be handed off to DynamoDB
+        as part of an ``save`` (``put_item``) call.
+
+        Largely internal.
+        """
+        # This doesn't save on its own. Rather, we prepare the datastructure
+        # and hand-off to the table to handle creation/update.
+        final_data = {}
+
+        for key, value in self._data.items():
+            if not self._is_storable(value):
+                continue
+
+            final_data[key] = self._dynamizer.encode(value)
+
+        return final_data
+
+    def prepare_partial(self):
+        """
+        Runs through **ONLY** the changed/deleted fields & encodes them to be
+        handed off to DynamoDB as part of an ``partial_save`` (``update_item``)
+        call.
+
+        Largely internal.
+        """
+        # This doesn't save on its own. Rather, we prepare the datastructure
+        # and hand-off to the table to handle creation/update.
+        final_data = {}
+        fields = set()
+        alterations = self._determine_alterations()
+
+        for key, value in alterations['adds'].items():
+            final_data[key] = {
+                'Action': 'PUT',
+                'Value': self._dynamizer.encode(self._data[key])
+            }
+            fields.add(key)
+
+        for key, value in alterations['changes'].items():
+            final_data[key] = {
+                'Action': 'PUT',
+                'Value': self._dynamizer.encode(self._data[key])
+            }
+            fields.add(key)
+
+        for key in alterations['deletes']:
+            final_data[key] = {
+                'Action': 'DELETE',
+            }
+            fields.add(key)
+
+        return final_data, fields
+
+    def partial_save(self):
+        """
+        Saves only the changed data to DynamoDB.
+
+        Extremely useful for high-volume/high-write data sets, this allows
+        you to update only a handful of fields rather than having to push
+        entire items. This prevents many accidental overwrite situations as
+        well as saves on the amount of data to transfer over the wire.
+
+        Returns ``True`` on success, ``False`` if no save was performed or
+        the write failed.
+
+        Example::
+
+            >>> user['last_name'] = 'Doh!'
+            # Only the last name field will be sent to DynamoDB.
+            >>> user.partial_save()
+
+        """
+        key = self.get_keys()
+        # Build a new dict of only the data we're changing.
+        final_data, fields = self.prepare_partial()
+
+        if not final_data:
+            return False
+
+        # Remove the key(s) from the ``final_data`` if present.
+        # They should only be present if this is a new item, in which
+        # case we shouldn't be sending as part of the data to update.
+        for fieldname, value in key.items():
+            if fieldname in final_data:
+                del final_data[fieldname]
+
+                try:
+                    # It's likely also in ``fields``, so remove it there too.
+                    fields.remove(fieldname)
+                except KeyError:
+                    pass
+
+        # Build expectations of only the fields we're planning to update.
+        expects = self.build_expects(fields=fields)
+        returned = self.table._update_item(key, final_data, expects=expects)
+        # Mark the object as clean.
+        self.mark_clean()
+        return returned
+
+    def save(self, overwrite=False):
+        """
+        Saves all data to DynamoDB.
+
+        By default, this attempts to ensure that none of the underlying
+        data has changed. If any fields have changed in between when the
+        ``Item`` was constructed & when it is saved, this call will fail so
+        as not to cause any data loss.
+
+        If you're sure possibly overwriting data is acceptable, you can pass
+        an ``overwrite=True``. If that's not acceptable, you may be able to use
+        ``Item.partial_save`` to only write the changed field data.
+
+        Optionally accepts an ``overwrite`` parameter, which should be a
+        boolean. If you provide ``True``, the item will be forcibly overwritten
+        within DynamoDB, even if another process changed the data in the
+        meantime. (Default: ``False``)
+
+        Returns ``True`` on success, ``False`` if no save was performed.
+
+        Example::
+
+            >>> user['last_name'] = 'Doh!'
+            # All data on the Item is sent to DynamoDB.
+            >>> user.save()
+
+            # If it fails, you can overwrite.
+            >>> user.save(overwrite=True)
+
+        """
+        if not self.needs_save() and not overwrite:
+            return False
+
+        final_data = self.prepare_full()
+        expects = None
+
+        if overwrite is False:
+            # Build expectations about *all* of the data.
+            expects = self.build_expects()
+
+        returned = self.table._put_item(final_data, expects=expects)
+        # Mark the object as clean.
+        self.mark_clean()
+        return returned
+
+    def delete(self):
+        """
+        Deletes the item's data to DynamoDB.
+
+        Returns ``True`` on success.
+
+        Example::
+
+            # Buh-bye now.
+            >>> user.delete()
+
+        """
+        key_data = self.get_keys()
+        return self.table.delete_item(**key_data)