From b0f32d002eaa0acf1b6674f673dd661e02da91b5 Mon Sep 17 00:00:00 2001 From: Peter Gaultney Date: Thu, 28 May 2020 14:01:17 -0500 Subject: [PATCH] 1.1.0 - naming improvements and added utilties --- CHANGES.md | 7 ++++++ examples/ddb_batch_get.py | 39 +++++++++++++++++++++++++++++++ xoto3/__about__.py | 2 +- xoto3/dynamodb/batch_get.py | 14 ++++++----- xoto3/dynamodb/query.py | 25 +++++++++++++++++++- xoto3/dynamodb/update/__init__.py | 2 +- xoto3/dynamodb/utils/index.py | 7 ++++++ 7 files changed, 87 insertions(+), 9 deletions(-) create mode 100755 examples/ddb_batch_get.py diff --git a/CHANGES.md b/CHANGES.md index 215c60c..17593d1 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,10 @@ +### 1.1.0 + +- Added `require_index` utility to `dynamodb.query` +- New, clearer name `page` to replace `From` in `dynamodb.query`. The + previous name remains but is a deprecated alias. +- Made `dynamodb.batch_get.items_only` a bit more generic. + ### 1.0.3 Fixed install_requires for Python > 3.6 diff --git a/examples/ddb_batch_get.py b/examples/ddb_batch_get.py new file mode 100755 index 0000000..966ed63 --- /dev/null +++ b/examples/ddb_batch_get.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python +"""This little example script only supports BatchGet on tables with +simple (non-composite) keys (i.e., the base index is HASH only, not +HASH+RANGE) for the sake of keeping the CLI manageable. + +However, BatchGetItem itself supports HASH+RANGE keys just fine, where +a key would look something like `dict(activity_group='XOi', +id='job-1234')`. +""" +import argparse +from pprint import pprint + +import boto3 + +from xoto3.dynamodb.batch_get import BatchGetItem, items_only + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("table_name") + parser.add_argument("ids", nargs="+") + parser.add_argument("--key-name", default="id", help="the name of your hash key attribute") + args = parser.parse_args() + + table = boto3.resource("dynamodb").Table(args.table_name) + + for item in items_only( # we don't care about keys, nor items that aren't found + BatchGetItem( + table, + # make a proper ItemKey (a dict) for each of the things you're looking to get + ({args.key_name: id} for id in args.ids), + # this is a memory-efficient generator but you can pass a list or tuple of dicts too + ) + ): + pprint(item) + + +if __name__ == "__main__": + main() diff --git a/xoto3/__about__.py b/xoto3/__about__.py index 7045f28..383496e 100644 --- a/xoto3/__about__.py +++ b/xoto3/__about__.py @@ -1,4 +1,4 @@ """xoto3""" -__version__ = "1.0.3" +__version__ = "1.1.0" __author__ = "Peter Gaultney" __author_email__ = "pgaultney@xoi.io" diff --git a/xoto3/dynamodb/batch_get.py b/xoto3/dynamodb/batch_get.py index 7c4757f..2ac5321 100644 --- a/xoto3/dynamodb/batch_get.py +++ b/xoto3/dynamodb/batch_get.py @@ -43,7 +43,7 @@ def BatchGetItem( KeyItemPairs, e.g. `KeyItemPair(key={'id': 'petros'}, val={'id': 'petros', 'age': 88})`. - If all you want is the non-empty items, wrap this call in the + If all you want is the non-empty (existing) items, wrap this call in the provided `items_only` utility. """ @@ -243,13 +243,15 @@ def _kv_tuple_to_key(kv_tuple, key_names): return {key_names[i]: kv_tuple[i] for i in range(len(key_names))} -def items_only(key_item_pairs: ty.Iterable[KeyItemPair]) -> ty.Iterable[Item]: - """Use with BatchGetItemsByKeys if you just want the items that were +def items_only( + key_item_pairs: ty.Iterable[ty.Union[KeyItemPair, KeyTupleItemPair]] +) -> ty.Iterable[Item]: + """Use with BatchGetItem if you just want the items that were found instead of the full iterable of all the keys you requested alongside their respective item or empty dict if the item wasn't found. """ - for kip in key_item_pairs: - if kip.item: - yield kip.item + for key, item in key_item_pairs: + if item: + yield item diff --git a/xoto3/dynamodb/query.py b/xoto3/dynamodb/query.py index 8c0261b..1c59578 100644 --- a/xoto3/dynamodb/query.py +++ b/xoto3/dynamodb/query.py @@ -8,6 +8,7 @@ from .types import Index, KeyAttributeType, TableQuery from .utils.index import hash_key_name, range_key_name +from .utils.index import find_index, require_index # noqa # included only for cleaner imports def single_partition(index: Index, partition_value: KeyAttributeType) -> TableQuery: @@ -54,13 +55,35 @@ def tx_query(query: TableQuery) -> TableQuery: return tx_query -def From(last_evaluated_key: dict): +def page(last_evaluated_key: dict): + """Resume a query on the page represented by the LastEvaluatedKey you + previously received. + + Note that there are pagination utilities in `paginate` if you don't + actually need to maintain this state (e.g., across client calls in a + RESTful service) and simply want to iterate through all the results. + """ + def tx_query(query: TableQuery) -> TableQuery: return dict(query, ExclusiveStartKey=last_evaluated_key) if last_evaluated_key else query return tx_query +From = page +"""Deprecated name - prefer 'page' + +The name From overlaps with SQL parlance about selecting a table, +which is absolutely not what we're doing here. This was intended +as shorthand for 'starting from', but even that overlaps with the +concepts of 'greater than or equal' or 'less than or equal' for a +range query. + +'page' makes it clearer, hopefully, that what is in view is +specifically a pagination of a previous query. +""" + + def within_range( index: Index, *, gte: Optional[KeyAttributeType] = None, lte: Optional[KeyAttributeType] = None ): diff --git a/xoto3/dynamodb/update/__init__.py b/xoto3/dynamodb/update/__init__.py index 5f7e52b..50fda7e 100644 --- a/xoto3/dynamodb/update/__init__.py +++ b/xoto3/dynamodb/update/__init__.py @@ -1,4 +1,4 @@ from .builders import build_update # noqa -from .diff import build_update_diff # noqa +from .diff import build_update_diff, select_attributes_for_set_and_remove # noqa from .core import UpdateItem, DiffedUpdateItem # noqa from .versioned import versioned_diffed_update_item, VersionedUpdateFailure # noqa diff --git a/xoto3/dynamodb/utils/index.py b/xoto3/dynamodb/utils/index.py index d641955..b0c6d01 100644 --- a/xoto3/dynamodb/utils/index.py +++ b/xoto3/dynamodb/utils/index.py @@ -40,3 +40,10 @@ def find_index(table: TableResource, hash_key: str, range_key: str) -> Optional[ if hash_key_name(index) == hash_key and range_key_name(index) == range_key: return index return None + + +def require_index(table: TableResource, hash_key: str, range_key: str) -> Index: + """Raises if the index is not found. A common pattern.""" + index = find_index(table, hash_key, range_key) + assert index, f"Index ({hash_key}, {range_key}) was not found in table {table.name}" + return index