Skip to content
This repository was archived by the owner on Nov 19, 2024. It is now read-only.

Commit d2fbef0

Browse files
committed
First commit
0 parents  commit d2fbef0

File tree

14 files changed

+1465
-0
lines changed

14 files changed

+1465
-0
lines changed
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
name: Install DynamoDB
2+
description: >
3+
Install a local version of DynamoDB.
4+
See https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/DynamoDBLocal.DownloadingAndRunning.html
5+
outputs:
6+
dynamodb-dir:
7+
description: Path to the directory containing the JAR file
8+
value: ${{ steps.step.outputs.dynamodb-dir }}
9+
runs:
10+
using: composite
11+
steps:
12+
- id: step
13+
run: curl https://d1ni2b6xgvw0s0.cloudfront.net/v2.x/dynamodb_local_latest.tar.gz | tar -C ${{ github.action_path }} -xz && echo dynamodb-dir=${{ github.action_path }} >> $GITHUB_OUTPUT
14+
shell: bash

.github/dependabot.yml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
version: 2
2+
3+
updates:
4+
- package-ecosystem: github-actions
5+
directory: /
6+
schedule:
7+
interval: weekly
8+
9+
- package-ecosystem: pip
10+
directory: /
11+
schedule:
12+
interval: weekly

.github/workflows/publish.yml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
on:
2+
push:
3+
branches:
4+
- main
5+
tags:
6+
- v*
7+
8+
jobs:
9+
publish:
10+
uses: OpenAstronomy/github-actions-workflows/.github/workflows/publish_pure_python.yml@v1
11+
secrets:
12+
pypi_token: ${{ secrets.pypi_token }}

.github/workflows/pull_request.yml

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
name: Pull Request
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
pull_request:
8+
branches:
9+
- main
10+
11+
jobs:
12+
python:
13+
runs-on: ubuntu-latest
14+
steps:
15+
- uses: actions/checkout@v4
16+
17+
- name: Check for Lint
18+
uses: chartboost/ruff-action@v1
19+
20+
- name: Check Formatting
21+
uses: chartboost/ruff-action@v1
22+
with:
23+
args: format --check
24+
25+
- uses: actions/setup-python@v5
26+
with:
27+
python-version: '3.11'
28+
29+
- id: install-dynamodb
30+
name: Install DynamoDB
31+
uses: ./.github/actions/install-dynamodb
32+
33+
- name: Install Poetry
34+
run: pipx install poetry
35+
36+
- name: Install Dependencies
37+
run: poetry install --all-extras
38+
39+
- name: Pytest
40+
run: poetry run pytest --dynamodb-dir=${{ steps.install-dynamodb.outputs.dynamodb-dir }} --cov --cov-report=xml
41+
42+
- name: MyPy
43+
run: poetry run mypy .
44+
45+
- name: Upload to Codecov.io
46+
uses: codecov/codecov-action@v4

.gitignore

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
*.egg-info
2+
__pycache__
3+
.coverage
4+
.tox
5+
.vscode
6+
build
7+
dist
8+
_version.py
9+
dynamodb-local-metadata.json

.vscode/extensions.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"recommendations": [
3+
"charliermarsh.ruff"
4+
]
5+
}

README.md

Whitespace-only changes.

dynamodb_autoincrement/__init__.py

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
from abc import ABC, abstractmethod
2+
from dataclasses import dataclass
3+
from typing import Any, Iterable, Optional
4+
5+
from mypy_boto3_dynamodb.service_resource import DynamoDBServiceResource
6+
7+
from .types import DynamoDBItem
8+
9+
10+
@dataclass(frozen=True)
11+
class BaseDynamoDBAutoIncrement(ABC):
12+
dynamodb: DynamoDBServiceResource
13+
counter_table_name: str
14+
counter_table_key: DynamoDBItem
15+
attribute_name: str
16+
table_name: str
17+
initial_value: int
18+
dangerously: bool = False
19+
20+
@abstractmethod
21+
def next(self, item: DynamoDBItem) -> tuple[Iterable[dict[str, Any]], str]:
22+
raise NotImplementedError
23+
24+
def _put_item(self, *, TableName, **kwargs):
25+
# FIXME: DynamoDB resource does not have put_item method; emulate it
26+
self.dynamodb.Table(TableName).put_item(**kwargs)
27+
28+
def _get_item(self, *, TableName, **kwargs):
29+
# FIXME: DynamoDB resource does not have get_item method; emulate it
30+
return self.dynamodb.Table(TableName).get_item(**kwargs)
31+
32+
def _query(self, *, TableName, **kwargs):
33+
return self.dynamodb.Table(TableName).query(**kwargs)
34+
35+
def put(self, item: DynamoDBItem):
36+
TransactionCanceledException = (
37+
self.dynamodb.meta.client.exceptions.TransactionCanceledException
38+
)
39+
while True:
40+
puts, next_counter = self.next(item)
41+
if self.dangerously:
42+
for put in puts:
43+
self._put_item(**put)
44+
else:
45+
try:
46+
# FIXME: depends on unreleased code in boto3.
47+
# See https://github.com/boto/boto3/pull/4010
48+
self.dynamodb.transact_write_items( # type: ignore[attr-defined]
49+
TransactItems=[{"Put": put} for put in puts]
50+
)
51+
except TransactionCanceledException:
52+
continue
53+
return next_counter
54+
55+
56+
class DynamoDBAutoIncrement(BaseDynamoDBAutoIncrement):
57+
def next(self, item):
58+
counter = (
59+
self._get_item(
60+
AttributesToGet=[self.attribute_name],
61+
Key=self.counter_table_key,
62+
TableName=self.counter_table_name,
63+
)
64+
.get("Item", {})
65+
.get(self.attribute_name)
66+
)
67+
68+
if counter is None:
69+
next_counter = self.initial_value
70+
put_kwargs = {"ConditionExpression": "attribute_not_exists(#counter)"}
71+
else:
72+
next_counter = counter + 1
73+
put_kwargs = {
74+
"ConditionExpression": "#counter = :counter",
75+
"ExpressionAttributeValues": {
76+
":counter": counter,
77+
},
78+
}
79+
80+
puts = [
81+
{
82+
**put_kwargs,
83+
"ExpressionAttributeNames": {
84+
"#counter": self.attribute_name,
85+
},
86+
"Item": {
87+
**self.counter_table_key,
88+
self.attribute_name: next_counter,
89+
},
90+
"TableName": self.counter_table_name,
91+
},
92+
{
93+
"ConditionExpression": "attribute_not_exists(#counter)",
94+
"ExpressionAttributeNames": {
95+
"#counter": self.attribute_name,
96+
},
97+
"Item": {self.attribute_name: next_counter, **item},
98+
"TableName": self.table_name,
99+
},
100+
]
101+
102+
return puts, next_counter
103+
104+
105+
class DynamoDBHistoryAutoIncrement(BaseDynamoDBAutoIncrement):
106+
def list(self) -> list[int]:
107+
result = self._query(
108+
TableName=self.table_name,
109+
ExpressionAttributeNames={
110+
**{f"#{i}": key for i, key in enumerate(self.counter_table_key.keys())},
111+
"#counter": self.attribute_name,
112+
},
113+
ExpressionAttributeValues={
114+
f":{i}": value
115+
for i, value in enumerate(self.counter_table_key.values())
116+
},
117+
KeyConditionExpression=" AND ".join(
118+
f"#{i} = :{i}" for i in range(len(self.counter_table_key.keys()))
119+
),
120+
ProjectionExpression="#counter",
121+
)
122+
return sorted(item[self.attribute_name] for item in result["Items"])
123+
124+
def get(self, version: Optional[int] = None) -> DynamoDBItem:
125+
if version is None:
126+
kwargs = {
127+
"TableName": self.counter_table_name,
128+
"Key": self.counter_table_key,
129+
}
130+
else:
131+
kwargs = {
132+
"TableName": self.table_name,
133+
"Key": {**self.counter_table_key, self.attribute_name: version},
134+
}
135+
return self._get_item(**kwargs).get("Item")
136+
137+
def next(self, item):
138+
existing_item = self._get_item(
139+
TableName=self.counter_table_name,
140+
Key=self.counter_table_key,
141+
).get("Item")
142+
143+
counter = (
144+
None if existing_item is None else existing_item.get(self.attribute_name)
145+
)
146+
147+
if counter is None:
148+
next_counter = self.initial_value
149+
put_kwargs = {"ConditionExpression": "attribute_not_exists(#counter)"}
150+
else:
151+
next_counter = counter + 1
152+
put_kwargs = {
153+
"ConditionExpression": "#counter = :counter",
154+
"ExpressionAttributeValues": {
155+
":counter": counter,
156+
},
157+
}
158+
159+
if existing_item is not None and counter is None:
160+
existing_item[self.attribute_name] = next_counter
161+
next_counter += 1
162+
163+
puts = [
164+
{
165+
**put_kwargs,
166+
"ExpressionAttributeNames": {
167+
"#counter": self.attribute_name,
168+
},
169+
"Item": {
170+
**item,
171+
**self.counter_table_key,
172+
self.attribute_name: next_counter,
173+
},
174+
"TableName": self.counter_table_name,
175+
},
176+
]
177+
178+
if existing_item is not None:
179+
puts.append(
180+
{
181+
"ConditionExpression": "attribute_not_exists(#counter)",
182+
"ExpressionAttributeNames": {
183+
"#counter": self.attribute_name,
184+
},
185+
"Item": existing_item,
186+
"TableName": self.table_name,
187+
}
188+
)
189+
190+
return puts, next_counter

dynamodb_autoincrement/tests/__init__.py

Whitespace-only changes.
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import pytest
2+
3+
4+
@pytest.fixture
5+
def create_tables(dynamodb):
6+
for kwargs in [
7+
{
8+
"AttributeDefinitions": [
9+
{"AttributeName": "tableName", "AttributeType": "S"}
10+
],
11+
"BillingMode": "PAY_PER_REQUEST",
12+
"KeySchema": [{"AttributeName": "tableName", "KeyType": "HASH"}],
13+
"TableName": "autoincrement",
14+
},
15+
{
16+
"BillingMode": "PAY_PER_REQUEST",
17+
"AttributeDefinitions": [
18+
{"AttributeName": "widgetID", "AttributeType": "N"}
19+
],
20+
"KeySchema": [{"AttributeName": "widgetID", "KeyType": "HASH"}],
21+
"TableName": "widgets",
22+
},
23+
{
24+
"BillingMode": "PAY_PER_REQUEST",
25+
"AttributeDefinitions": [
26+
{"AttributeName": "widgetID", "AttributeType": "N"},
27+
{"AttributeName": "version", "AttributeType": "N"},
28+
],
29+
"KeySchema": [
30+
{"AttributeName": "widgetID", "KeyType": "HASH"},
31+
{"AttributeName": "version", "KeyType": "RANGE"},
32+
],
33+
"TableName": "widgetHistory",
34+
},
35+
]:
36+
dynamodb.create_table(**kwargs)

0 commit comments

Comments
 (0)