Skip to content

Commit b981260

Browse files
committed
Don't require upstream patches for boto3
The AWS Python SDK, [boto3], has [resource] objects that provide high-level interfaces to AWS services. The [DynamoDB resource] greatly simplifies marshalling and unmarshalling data. We rely on the resource method for [TransactWriteItems] among others that are absent from boto3. We opened PR boto/boto3#4010 to add that method. The resource methods are synthesized at runtime from a data file. Fortunately, boto3 has a [Loader] mechanism that allows the user to add extra data files, and the [loader search path] is configurable. In order to not depend upon our upstream PR for boto3, we distribute the extra data files and fix up the loader search path by putting it in a [.pth file] which Python executes automatically during startup. FIXME: The .pth file method does not currently work when we are doing editable installs. It _should_ work with editable installs if we factor the data files and the .pth file to a separate package that we add as a dependency. [boto3]: https://github.com/boto/boto3 [resource]: https://boto3.amazonaws.com/v1/documentation/api/latest/guide/resources.html [DynamoDB resource]: https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/dynamodb.html#resources [TransactWriteItems]: https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_TransactWriteItems.html [Loader]: https://botocore.amazonaws.com/v1/documentation/api/latest/reference/loaders.html [loader search path]: https://botocore.amazonaws.com/v1/documentation/api/latest/reference/loaders.html#the-search-path [.pth file]: https://docs.python.org/3/library/site.html
1 parent c6a4b29 commit b981260

File tree

7 files changed

+64
-32
lines changed

7 files changed

+64
-32
lines changed

README.md

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,3 @@ Use optimistic locking to put DynamoDB records with auto-incrementing attributes
66

77
- https://aws.amazon.com/blogs/aws/new-amazon-dynamodb-transactions/
88
- https://bitesizedserverless.com/bite/reliable-auto-increments-in-dynamodb/
9-
10-
## FIXME
11-
12-
This package currently depends on code that is in a pull request for boto3 that is not yet merged or released.
13-
See https://github.com/boto/boto3/pull/4010.

boto3_missing.pth

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
import boto3_missing; boto3_missing.install()

boto3_missing/__init__.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# Copyright © 2023 United States Government as represented by the
2+
# Administrator of the National Aeronautics and Space Administration.
3+
# All Rights Reserved.
4+
"""Add missing boto3 SDK data.
5+
6+
See
7+
https://botocore.amazonaws.com/v1/documentation/api/latest/reference/loaders.html,
8+
https://github.com/boto/boto3/pull/4010
9+
"""
10+
11+
from os import environ, pathsep
12+
13+
from . import data
14+
15+
16+
def install():
17+
new_path = [*data.__path__]
18+
if orig_path := environ.get("AWS_DATA_PATH"):
19+
new_path.extend(orig_path.split(pathsep))
20+
environ["AWS_DATA_PATH"] = pathsep.join(new_path)
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"merge": {
3+
"service": {
4+
"actions": {
5+
"GetItem": {
6+
"request": { "operation": "GetItem" }
7+
},
8+
"PutItem": {
9+
"request": { "operation": "PutItem" }
10+
},
11+
"Query": {
12+
"request": { "operation": "Query" }
13+
},
14+
"Scan": {
15+
"request": { "operation": "Scan" }
16+
},
17+
"TransactWriteItems": {
18+
"request": { "operation": "TransactWriteItems" }
19+
}
20+
}
21+
}
22+
}
23+
}

dynamodb_autoincrement.py

Lines changed: 8 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@
99

1010
from mypy_boto3_dynamodb.service_resource import DynamoDBServiceResource
1111

12+
# FIXME: remove instances of 'type: ignore[attr-defined]' below once
13+
# boto3-missing becomes unnecessary.
14+
1215

1316
PrimitiveDynamoDBValues = Optional[Union[str, int, float, Decimal, bool]]
1417
DynamoDBValues = Union[
@@ -33,18 +36,6 @@ class BaseDynamoDBAutoIncrement(ABC):
3336
def next(self, item: DynamoDBItem) -> tuple[Iterable[dict[str, Any]], str]:
3437
raise NotImplementedError
3538

36-
def _put_item(self, *, TableName, **kwargs):
37-
# FIXME: DynamoDB resource does not have put_item method; emulate it
38-
self.dynamodb.Table(TableName).put_item(**kwargs)
39-
40-
def _get_item(self, *, TableName, **kwargs):
41-
# FIXME: DynamoDB resource does not have get_item method; emulate it
42-
return self.dynamodb.Table(TableName).get_item(**kwargs)
43-
44-
def _query(self, *, TableName, **kwargs):
45-
# FIXME: DynamoDB resource does not have put_item method; emulate it
46-
return self.dynamodb.Table(TableName).query(**kwargs)
47-
4839
def put(self, item: DynamoDBItem):
4940
TransactionCanceledException = (
5041
self.dynamodb.meta.client.exceptions.TransactionCanceledException
@@ -53,11 +44,9 @@ def put(self, item: DynamoDBItem):
5344
puts, next_counter = self.next(item)
5445
if self.dangerously:
5546
for put in puts:
56-
self._put_item(**put)
47+
self.dynamodb.put_item(**put) # type: ignore[attr-defined]
5748
else:
5849
try:
59-
# FIXME: depends on an unmerged PR for boto3.
60-
# See https://github.com/boto/boto3/pull/4010
6150
self.dynamodb.transact_write_items( # type: ignore[attr-defined]
6251
TransactItems=[{"Put": put} for put in puts]
6352
)
@@ -69,7 +58,7 @@ def put(self, item: DynamoDBItem):
6958
class DynamoDBAutoIncrement(BaseDynamoDBAutoIncrement):
7059
def next(self, item):
7160
counter = (
72-
self._get_item(
61+
self.dynamodb.get_item(
7362
AttributesToGet=[self.attribute_name],
7463
Key=self.counter_table_key,
7564
TableName=self.counter_table_name,
@@ -117,7 +106,7 @@ def next(self, item):
117106

118107
class DynamoDBHistoryAutoIncrement(BaseDynamoDBAutoIncrement):
119108
def list(self) -> list[int]:
120-
result = self._query(
109+
result = self.dynamodb.query( # type: ignore[attr-defined]
121110
TableName=self.table_name,
122111
ExpressionAttributeNames={
123112
**{f"#{i}": key for i, key in enumerate(self.counter_table_key.keys())},
@@ -145,10 +134,10 @@ def get(self, version: Optional[int] = None) -> DynamoDBItem:
145134
"TableName": self.table_name,
146135
"Key": {**self.counter_table_key, self.attribute_name: version},
147136
}
148-
return self._get_item(**kwargs).get("Item")
137+
return self.dynamodb.get_item(**kwargs).get("Item") # type: ignore[attr-defined]
149138

150139
def next(self, item):
151-
existing_item = self._get_item(
140+
existing_item = self.dynamodb.get_item(
152141
TableName=self.counter_table_name,
153142
Key=self.counter_table_key,
154143
).get("Item")

pyproject.toml

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ classifiers = [
1616
"Topic :: Database",
1717
]
1818
dependencies = [
19-
"boto3 @ git+https://github.com/lpsinger/boto3@dynamodb-resource-transact-write-items",
19+
"boto3",
2020
"boto3-stubs[dynamodb]",
2121
]
2222
requires-python = ">=3.9"
@@ -41,9 +41,13 @@ exclude_also = [
4141
"@abstractmethod"
4242
]
4343

44-
[tool.setuptools]
45-
py-modules = [
44+
[tool.setuptools.packages.find]
45+
include = [
4646
"dynamodb_autoincrement",
47+
"boto3_missing",
4748
]
4849

50+
[tool.setuptools.package-data]
51+
boto3_missing = ["**/*.json", "../*.pth"]
52+
4953
[tool.setuptools_scm]

test_dynamodb_autoincrement.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -91,19 +91,19 @@ def test_autoincrement_safely(autoincrement_safely, dynamodb, last_id):
9191
if last_id is None:
9292
next_id = 1
9393
else:
94-
dynamodb.Table("autoincrement").put_item(
94+
dynamodb.put_item(TableName="autoincrement",
9595
Item={"tableName": "widgets", "widgetID": last_id}
9696
)
9797
next_id = last_id + 1
9898

9999
result = autoincrement_safely.put({"widgetName": "runcible spoon"})
100100
assert result == next_id
101101

102-
assert dynamodb.Table("widgets").scan()["Items"] == [
102+
assert dynamodb.scan(TableName="widgets")["Items"] == [
103103
{"widgetID": next_id, "widgetName": "runcible spoon"},
104104
]
105105

106-
assert dynamodb.Table("autoincrement").scan()["Items"] == [
106+
assert dynamodb.scan(TableName="autoincrement")["Items"] == [
107107
{
108108
"tableName": "widgets",
109109
"widgetID": next_id,
@@ -152,7 +152,7 @@ def test_autoincrement_dangerously_fails_on_many_parallel_puts(
152152
@pytest.fixture(params=[None, {"widgetID": 1}, {"widgetID": 1, "version": 1}])
153153
def initial_item(request, create_tables, dynamodb):
154154
if request.param is not None:
155-
dynamodb.Table("widgets").put_item(Item=request.param)
155+
dynamodb.put_item(TableName="widgets", Item=request.param)
156156
return request.param
157157

158158

@@ -174,7 +174,7 @@ def test_autoincrement_version(
174174
)
175175
assert new_version == 1 + has_initial_item
176176

177-
history_items = dynamodb.Table("widgetHistory").query(
177+
history_items = dynamodb.query(
178178
TableName="widgetHistory",
179179
KeyConditionExpression="widgetID = :widgetID",
180180
ExpressionAttributeValues={

0 commit comments

Comments
 (0)