From 2486f6a07921e0869b089a993698085428550c74 Mon Sep 17 00:00:00 2001 From: Peter Gaultney Date: Tue, 5 Oct 2021 15:26:40 -0500 Subject: [PATCH] most generic single item writer for write_versioned API --- CHANGES.md | 6 +++ .../dynamodb/write_versioned/api2_test.py | 20 ++++++++++ xoto3/__about__.py | 2 +- xoto3/dynamodb/write_versioned/__init__.py | 9 ++++- xoto3/dynamodb/write_versioned/api2.py | 37 +++++++++++++++++++ 5 files changed, 72 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 59b07c3..4236dbb 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,9 @@ +## 1.16.0 + +- `write_item` single item write helper for the `write_versioned` + higher level DynamoDB transaction API. Suitable for any form of + transactional write to a single item (creation/update/deletion). + ### 1.15.1 Fixes to `versioned_transact_write_items`: diff --git a/tests/xoto3/dynamodb/write_versioned/api2_test.py b/tests/xoto3/dynamodb/write_versioned/api2_test.py index b3b8cb8..6179aaf 100644 --- a/tests/xoto3/dynamodb/write_versioned/api2_test.py +++ b/tests/xoto3/dynamodb/write_versioned/api2_test.py @@ -10,6 +10,7 @@ update_existing, update_if_exists, versioned_transact_write_items, + write_item, ) from .conftest import mock_next_run @@ -96,3 +97,22 @@ def test_api2_create_or_update(): create_or_update(table, lambda x: dict(key, foo=1), key), **mock_next_run(vt), ) assert table.require(key)(vt)["foo"] == 1 + + +def test_api2_write_item_creates(): + vt, table = _fake_table("felicity", "id") + key = dict(id="goodbye") + vt = versioned_transact_write_items( + write_item(table, lambda ox: dict(key, bar=8), key), **mock_next_run(vt), + ) + assert table.require(key)(vt)["bar"] == 8 + + +def test_api2_write_item_deletes(): + vt, table = _fake_table("felicity", "id") + key = dict(id="goodbye") + vt = table.presume(key, dict(key, baz=9))(vt) + vt = versioned_transact_write_items( + write_item(table, lambda x: None, key), **mock_next_run(vt), + ) + assert table.get(key)(vt) is None diff --git a/xoto3/__about__.py b/xoto3/__about__.py index 815b7fa..e587d41 100644 --- a/xoto3/__about__.py +++ b/xoto3/__about__.py @@ -1,4 +1,4 @@ """xoto3""" -__version__ = "1.15.1" +__version__ = "1.16.0" __author__ = "Peter Gaultney" __author_email__ = "pgaultney@xoi.io" diff --git a/xoto3/dynamodb/write_versioned/__init__.py b/xoto3/dynamodb/write_versioned/__init__.py index d373a59..f96024c 100644 --- a/xoto3/dynamodb/write_versioned/__init__.py +++ b/xoto3/dynamodb/write_versioned/__init__.py @@ -4,7 +4,14 @@ here) is not guaranteed to remain, so don't do that. Import this module and use only what it exposes. """ -from .api2 import ItemTable, TypedTable, create_or_update, update_existing, update_if_exists # noqa +from .api2 import ( # noqa + ItemTable, + TypedTable, + create_or_update, + update_existing, + update_if_exists, + write_item, +) from .errors import ( # noqa ItemUndefinedException, TableSchemaUnknownError, diff --git a/xoto3/dynamodb/write_versioned/api2.py b/xoto3/dynamodb/write_versioned/api2.py index 5f2ad25..e46617a 100644 --- a/xoto3/dynamodb/write_versioned/api2.py +++ b/xoto3/dynamodb/write_versioned/api2.py @@ -132,6 +132,14 @@ def _item_ident(item: Item) -> Item: return TypedTable(table_name, deepcopy, _item_ident, item_name=item_name) +# The following are simple single-item-write helpers with various type +# signatures for different semantics around your expectations for +# whether an item already exists, what to do if it doesn't, and +# whether the item is guaranteed to exist at the end of a successful +# call. They are provided simply as a convenience - they do nothing +# that an individual application could not do on its own. + + def update_if_exists( table: TypedTable[T], updater: Callable[[T], T], key: ItemKey, ) -> TransactionBuilder: @@ -168,3 +176,32 @@ def create_or_update_trans(vt: VersionedTransaction) -> VersionedTransaction: return table.put(creator_updater(table.get(key)(vt)))(vt) return create_or_update_trans + + +def write_item( + table: TypedTable[T], writer: Callable[[Optional[T]], Optional[T]], key: ItemKey +) -> TransactionBuilder: + """The most general purpose single-item-write abstraction, with + necessarily weak type constraints on the writer implementation. + + Note that in DynamoDB parlance, a write can be either a Put or a + Delete, and our usage of that terminology here parallels + theirs. Creation, Updating, and Deleting are all in view here. + + Specifically, your writer function (as with all our other helpers + defined here) should return _exactly_ what it intends to have + represented in the database at the end of the transaction. If you + wish to make no change, simply return the unmodified item. If you + wish to _delete_ an item, return None - this indicates that you + want the value of the item to be null, i.e. deleted from the + table. + """ + + def write_single_item(vt: VersionedTransaction) -> VersionedTransaction: + resulting_item = writer(table.get(key)(vt)) + if resulting_item is None: + return table.delete(key)(vt) + else: + return table.put(resulting_item)(vt) + + return write_single_item