| Index: third_party/boto/boto/dynamodb2/table.py
|
| ===================================================================
|
| --- third_party/boto/boto/dynamodb2/table.py (revision 33376)
|
| +++ third_party/boto/boto/dynamodb2/table.py (working copy)
|
| @@ -7,7 +7,8 @@
|
| from boto.dynamodb2.items import Item
|
| from boto.dynamodb2.layer1 import DynamoDBConnection
|
| from boto.dynamodb2.results import ResultSet, BatchGetResultSet
|
| -from boto.dynamodb2.types import Dynamizer, FILTER_OPERATORS, QUERY_OPERATORS
|
| +from boto.dynamodb2.types import (Dynamizer, FILTER_OPERATORS, QUERY_OPERATORS,
|
| + STRING)
|
| from boto.exception import JSONResponseError
|
|
|
|
|
| @@ -170,7 +171,7 @@
|
| ... ],
|
| ... throughput={
|
| ... 'read':10,
|
| - ... 'write":10,
|
| + ... 'write':10,
|
| ... }),
|
| ... ])
|
|
|
| @@ -232,18 +233,29 @@
|
| )
|
| return table
|
|
|
| - def _introspect_schema(self, raw_schema):
|
| + def _introspect_schema(self, raw_schema, raw_attributes=None):
|
| """
|
| Given a raw schema structure back from a DynamoDB response, parse
|
| out & build the high-level Python objects that represent them.
|
| """
|
| schema = []
|
| + sane_attributes = {}
|
|
|
| + if raw_attributes:
|
| + for field in raw_attributes:
|
| + sane_attributes[field['AttributeName']] = field['AttributeType']
|
| +
|
| for field in raw_schema:
|
| + data_type = sane_attributes.get(field['AttributeName'], STRING)
|
| +
|
| if field['KeyType'] == 'HASH':
|
| - schema.append(HashKey(field['AttributeName']))
|
| + schema.append(
|
| + HashKey(field['AttributeName'], data_type=data_type)
|
| + )
|
| elif field['KeyType'] == 'RANGE':
|
| - schema.append(RangeKey(field['AttributeName']))
|
| + schema.append(
|
| + RangeKey(field['AttributeName'], data_type=data_type)
|
| + )
|
| else:
|
| raise exceptions.UnknownSchemaFieldError(
|
| "%s was seen, but is unknown. Please report this at "
|
| @@ -280,7 +292,7 @@
|
| )
|
|
|
| name = field['IndexName']
|
| - kwargs['parts'] = self._introspect_schema(field['KeySchema'])
|
| + kwargs['parts'] = self._introspect_schema(field['KeySchema'], None)
|
| indexes.append(index_klass(name, **kwargs))
|
|
|
| return indexes
|
| @@ -319,7 +331,8 @@
|
| if not self.schema:
|
| # Since we have the data, build the schema.
|
| raw_schema = result['Table'].get('KeySchema', [])
|
| - self.schema = self._introspect_schema(raw_schema)
|
| + raw_attributes = result['Table'].get('AttributeDefinitions', [])
|
| + self.schema = self._introspect_schema(raw_schema, raw_attributes)
|
|
|
| if not self.indexes:
|
| # Build the index information as well.
|
| @@ -488,6 +501,8 @@
|
| attributes_to_get=attributes,
|
| consistent_read=consistent
|
| )
|
| + if 'Item' not in item_data:
|
| + raise exceptions.ItemNotFound("Item %s couldn't be found." % kwargs)
|
| item = Item(self)
|
| item.load(item_data)
|
| return item
|
| @@ -526,7 +541,7 @@
|
| """
|
| try:
|
| self.get_item(**kwargs)
|
| - except JSONResponseError:
|
| + except (JSONResponseError, exceptions.ItemNotFound):
|
| return False
|
|
|
| return True
|
| @@ -633,17 +648,36 @@
|
| self.connection.update_item(self.table_name, raw_key, item_data, **kwargs)
|
| return True
|
|
|
| - def delete_item(self, **kwargs):
|
| + def delete_item(self, expected=None, conditional_operator=None, **kwargs):
|
| """
|
| - Deletes an item in DynamoDB.
|
| + Deletes a single item. You can perform a conditional delete operation
|
| + that deletes the item if it exists, or if it has an expected attribute
|
| + value.
|
|
|
| + Conditional deletes are useful for only deleting items if specific
|
| + conditions are met. If those conditions are met, DynamoDB performs
|
| + the delete. Otherwise, the item is not deleted.
|
| +
|
| + To specify the expected attribute values of the item, you can pass a
|
| + dictionary of conditions to ``expected``. Each condition should follow
|
| + the pattern ``<attributename>__<comparison_operator>=<value_to_expect>``.
|
| +
|
| **IMPORTANT** - Be careful when using this method, there is no undo.
|
|
|
| To specify the key of the item you'd like to get, you can specify the
|
| key attributes as kwargs.
|
|
|
| - Returns ``True`` on success.
|
| + Optionally accepts an ``expected`` parameter which is a dictionary of
|
| + expected attribute value conditions.
|
|
|
| + Optionally accepts a ``conditional_operator`` which applies to the
|
| + expected attribute value conditions:
|
| +
|
| + + `AND` - If all of the conditions evaluate to true (default)
|
| + + `OR` - True if at least one condition evaluates to true
|
| +
|
| + Returns ``True`` on success, ``False`` on failed conditional delete.
|
| +
|
| Example::
|
|
|
| # A simple hash key.
|
| @@ -661,9 +695,21 @@
|
| ... })
|
| True
|
|
|
| + # Conditional delete
|
| + >>> users.delete_item(username='johndoe',
|
| + ... expected={'balance__eq': 0})
|
| + True
|
| """
|
| + expected = self._build_filters(expected, using=FILTER_OPERATORS)
|
| raw_key = self._encode_keys(kwargs)
|
| - self.connection.delete_item(self.table_name, raw_key)
|
| +
|
| + try:
|
| + self.connection.delete_item(self.table_name, raw_key,
|
| + expected=expected,
|
| + conditional_operator=conditional_operator)
|
| + except exceptions.ConditionalCheckFailedException:
|
| + return False
|
| +
|
| return True
|
|
|
| def get_key_fields(self):
|
| @@ -742,6 +788,9 @@
|
| An internal method for taking query/scan-style ``**kwargs`` & turning
|
| them into the raw structure DynamoDB expects for filtering.
|
| """
|
| + if filter_kwargs is None:
|
| + return
|
| +
|
| filters = {}
|
|
|
| for field_and_op, value in filter_kwargs.items():
|
| @@ -801,17 +850,34 @@
|
| def query(self, limit=None, index=None, reverse=False, consistent=False,
|
| attributes=None, max_page_size=None, **filter_kwargs):
|
| """
|
| + **WARNING:** This method is provided **strictly** for
|
| + backward-compatibility. It returns results in an incorrect order.
|
| +
|
| + If you are writing new code, please use ``Table.query_2``.
|
| + """
|
| + reverse = not reverse
|
| + return self.query_2(limit=limit, index=index, reverse=reverse,
|
| + consistent=consistent, attributes=attributes,
|
| + max_page_size=max_page_size, **filter_kwargs)
|
| +
|
| + def query_2(self, limit=None, index=None, reverse=False,
|
| + consistent=False, attributes=None, max_page_size=None,
|
| + query_filter=None, conditional_operator=None,
|
| + **filter_kwargs):
|
| + """
|
| Queries for a set of matching items in a DynamoDB table.
|
|
|
| Queries can be performed against a hash key, a hash+range key or
|
| - against any data stored in your local secondary indexes.
|
| + against any data stored in your local secondary indexes. Query filters
|
| + can be used to filter on arbitrary fields.
|
|
|
| **Note** - You can not query against arbitrary fields within the data
|
| - stored in DynamoDB.
|
| + stored in DynamoDB unless you specify ``query_filter`` values.
|
|
|
| To specify the filters of the items you'd like to get, you can specify
|
| the filters as kwargs. Each filter kwarg should follow the pattern
|
| - ``<fieldname>__<filter_operation>=<value_to_look_for>``.
|
| + ``<fieldname>__<filter_operation>=<value_to_look_for>``. Query filters
|
| + are specified in the same way.
|
|
|
| Optionally accepts a ``limit`` parameter, which should be an integer
|
| count of the total number of items to return. (Default: ``None`` -
|
| @@ -822,7 +888,7 @@
|
| (Default: ``None``)
|
|
|
| Optionally accepts a ``reverse`` parameter, which will present the
|
| - results in reverse order. (Default: ``None`` - normal order)
|
| + results in reverse order. (Default: ``False`` - normal order)
|
|
|
| Optionally accepts a ``consistent`` parameter, which should be a
|
| boolean. If you provide ``True``, it will force a consistent read of
|
| @@ -840,6 +906,15 @@
|
| the scan from drowning out other queries. (Default: ``None`` -
|
| fetch as many as DynamoDB will return)
|
|
|
| + Optionally accepts a ``query_filter`` which is a dictionary of filter
|
| + conditions against any arbitrary field in the returned data.
|
| +
|
| + Optionally accepts a ``conditional_operator`` which applies to the
|
| + query filter conditions:
|
| +
|
| + + `AND` - True if all filter conditions evaluate to true (default)
|
| + + `OR` - True if at least one filter condition evaluates to true
|
| +
|
| Returns a ``ResultSet``, which transparently handles the pagination of
|
| results you get back.
|
|
|
| @@ -878,12 +953,29 @@
|
| 'John'
|
| 'Fred'
|
|
|
| + # Filter by non-indexed field(s)
|
| + >>> results = users.query(
|
| + ... last_name__eq='Doe',
|
| + ... reverse=True,
|
| + ... query_filter={
|
| + ... 'first_name__beginswith': 'A'
|
| + ... }
|
| + ... )
|
| + >>> for res in results:
|
| + ... print res['first_name'] + ' ' + res['last_name']
|
| + 'Alice Doe'
|
| +
|
| """
|
| if self.schema:
|
| - if len(self.schema) == 1 and len(filter_kwargs) <= 1:
|
| - raise exceptions.QueryError(
|
| - "You must specify more than one key to filter on."
|
| - )
|
| + if len(self.schema) == 1:
|
| + if len(filter_kwargs) <= 1:
|
| + if not self.global_indexes or not len(self.global_indexes):
|
| + # If the schema only has one field, there's <= 1 filter
|
| + # param & no Global Secondary Indexes, this is user
|
| + # error. Bail early.
|
| + raise exceptions.QueryError(
|
| + "You must specify more than one key to filter on."
|
| + )
|
|
|
| if attributes is not None:
|
| select = 'SPECIFIC_ATTRIBUTES'
|
| @@ -901,20 +993,26 @@
|
| 'consistent': consistent,
|
| 'select': select,
|
| 'attributes_to_get': attributes,
|
| + 'query_filter': query_filter,
|
| + 'conditional_operator': conditional_operator,
|
| })
|
| results.to_call(self._query, **kwargs)
|
| return results
|
|
|
| - def query_count(self, index=None, consistent=False, **filter_kwargs):
|
| + def query_count(self, index=None, consistent=False, conditional_operator=None,
|
| + query_filter=None, scan_index_forward=True, limit=None,
|
| + **filter_kwargs):
|
| """
|
| Queries the exact count of matching items in a DynamoDB table.
|
|
|
| Queries can be performed against a hash key, a hash+range key or
|
| - against any data stored in your local secondary indexes.
|
| + against any data stored in your local secondary indexes. Query filters
|
| + can be used to filter on arbitrary fields.
|
|
|
| To specify the filters of the items you'd like to get, you can specify
|
| the filters as kwargs. Each filter kwarg should follow the pattern
|
| - ``<fieldname>__<filter_operation>=<value_to_look_for>``.
|
| + ``<fieldname>__<filter_operation>=<value_to_look_for>``. Query filters
|
| + are specified in the same way.
|
|
|
| Optionally accepts an ``index`` parameter, which should be a string of
|
| name of the local secondary index you want to query against.
|
| @@ -925,9 +1023,34 @@
|
| the data (more expensive). (Default: ``False`` - use eventually
|
| consistent reads)
|
|
|
| + Optionally accepts a ``query_filter`` which is a dictionary of filter
|
| + conditions against any arbitrary field in the returned data.
|
| +
|
| + Optionally accepts a ``conditional_operator`` which applies to the
|
| + query filter conditions:
|
| +
|
| + + `AND` - True if all filter conditions evaluate to true (default)
|
| + + `OR` - True if at least one filter condition evaluates to true
|
| +
|
| Returns an integer which represents the exact amount of matched
|
| items.
|
|
|
| + :type scan_index_forward: boolean
|
| + :param scan_index_forward: Specifies ascending (true) or descending
|
| + (false) traversal of the index. DynamoDB returns results reflecting
|
| + the requested order determined by the range key. If the data type
|
| + is Number, the results are returned in numeric order. For String,
|
| + the results are returned in order of ASCII character code values.
|
| + For Binary, DynamoDB treats each byte of the binary data as
|
| + unsigned when it compares binary values.
|
| +
|
| + If ScanIndexForward is not specified, the results are returned in
|
| + ascending order.
|
| +
|
| + :type limit: integer
|
| + :param limit: The maximum number of items to evaluate (not necessarily
|
| + the number of matching items).
|
| +
|
| Example::
|
|
|
| # Look for last names equal to "Doe".
|
| @@ -949,18 +1072,27 @@
|
| using=QUERY_OPERATORS
|
| )
|
|
|
| + built_query_filter = self._build_filters(
|
| + query_filter,
|
| + using=FILTER_OPERATORS
|
| + )
|
| +
|
| raw_results = self.connection.query(
|
| self.table_name,
|
| index_name=index,
|
| consistent_read=consistent,
|
| select='COUNT',
|
| key_conditions=key_conditions,
|
| + query_filter=built_query_filter,
|
| + conditional_operator=conditional_operator,
|
| + limit=limit,
|
| + scan_index_forward=scan_index_forward,
|
| )
|
| return int(raw_results.get('Count', 0))
|
|
|
| def _query(self, limit=None, index=None, reverse=False, consistent=False,
|
| exclusive_start_key=None, select=None, attributes_to_get=None,
|
| - **filter_kwargs):
|
| + query_filter=None, conditional_operator=None, **filter_kwargs):
|
| """
|
| The internal method that performs the actual queries. Used extensively
|
| by ``ResultSet`` to perform each (paginated) request.
|
| @@ -968,12 +1100,15 @@
|
| kwargs = {
|
| 'limit': limit,
|
| 'index_name': index,
|
| - 'scan_index_forward': reverse,
|
| 'consistent_read': consistent,
|
| 'select': select,
|
| - 'attributes_to_get': attributes_to_get
|
| + 'attributes_to_get': attributes_to_get,
|
| + 'conditional_operator': conditional_operator,
|
| }
|
|
|
| + if reverse:
|
| + kwargs['scan_index_forward'] = False
|
| +
|
| if exclusive_start_key:
|
| kwargs['exclusive_start_key'] = {}
|
|
|
| @@ -987,6 +1122,11 @@
|
| using=QUERY_OPERATORS
|
| )
|
|
|
| + kwargs['query_filter'] = self._build_filters(
|
| + query_filter,
|
| + using=FILTER_OPERATORS
|
| + )
|
| +
|
| raw_results = self.connection.query(
|
| self.table_name,
|
| **kwargs
|
| @@ -1013,13 +1153,14 @@
|
| }
|
|
|
| def scan(self, limit=None, segment=None, total_segments=None,
|
| - max_page_size=None, attributes=None, **filter_kwargs):
|
| + max_page_size=None, attributes=None, conditional_operator=None,
|
| + **filter_kwargs):
|
| """
|
| Scans across all items within a DynamoDB table.
|
|
|
| Scans can be performed against a hash key or a hash+range key. You can
|
| additionally filter the results after the table has been read but
|
| - before the response is returned.
|
| + before the response is returned by using query filters.
|
|
|
| To specify the filters of the items you'd like to get, you can specify
|
| the filters as kwargs. Each filter kwarg should follow the pattern
|
| @@ -1084,12 +1225,14 @@
|
| 'segment': segment,
|
| 'total_segments': total_segments,
|
| 'attributes': attributes,
|
| + 'conditional_operator': conditional_operator,
|
| })
|
| results.to_call(self._scan, **kwargs)
|
| return results
|
|
|
| def _scan(self, limit=None, exclusive_start_key=None, segment=None,
|
| - total_segments=None, attributes=None, **filter_kwargs):
|
| + total_segments=None, attributes=None, conditional_operator=None,
|
| + **filter_kwargs):
|
| """
|
| The internal method that performs the actual scan. Used extensively
|
| by ``ResultSet`` to perform each (paginated) request.
|
| @@ -1099,6 +1242,7 @@
|
| 'segment': segment,
|
| 'total_segments': total_segments,
|
| 'attributes_to_get': attributes,
|
| + 'conditional_operator': conditional_operator,
|
| }
|
|
|
| if exclusive_start_key:
|
| @@ -1176,7 +1320,7 @@
|
| # We pass the keys to the constructor instead, so it can maintain it's
|
| # own internal state as to what keys have been processed.
|
| results = BatchGetResultSet(keys=keys, max_batch_get=self.max_batch_get)
|
| - results.to_call(self._batch_get, consistent=False)
|
| + results.to_call(self._batch_get, consistent=consistent)
|
| return results
|
|
|
| def _batch_get(self, keys, consistent=False):
|
|
|