-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit e7da2f9
Showing
14 changed files
with
1,447 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
name: Install DynamoDB | ||
description: > | ||
Install a local version of DynamoDB. | ||
See https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/DynamoDBLocal.DownloadingAndRunning.html | ||
outputs: | ||
dynamodb-dir: | ||
description: Path to the directory containing the JAR file | ||
value: ${{ steps.step.outputs.dynamodb-dir }} | ||
runs: | ||
using: composite | ||
steps: | ||
- id: step | ||
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 | ||
shell: bash |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
version: 2 | ||
|
||
updates: | ||
- package-ecosystem: github-actions | ||
directory: / | ||
schedule: | ||
interval: weekly | ||
|
||
- package-ecosystem: pip | ||
directory: / | ||
schedule: | ||
interval: weekly |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
on: | ||
push: | ||
branches: | ||
- main | ||
tags: | ||
- v* | ||
|
||
jobs: | ||
publish: | ||
uses: OpenAstronomy/github-actions-workflows/.github/workflows/publish_pure_python.yml@v1 | ||
secrets: | ||
pypi_token: ${{ secrets.pypi_token }} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
name: Pull Request | ||
|
||
on: | ||
push: | ||
branches: | ||
- main | ||
pull_request: | ||
branches: | ||
- main | ||
|
||
jobs: | ||
python: | ||
runs-on: ubuntu-latest | ||
steps: | ||
- uses: actions/checkout@v4 | ||
|
||
- name: Check for Lint | ||
uses: chartboost/ruff-action@v1 | ||
|
||
- name: Check Formatting | ||
uses: chartboost/ruff-action@v1 | ||
with: | ||
args: format --check | ||
|
||
- uses: actions/setup-python@v5 | ||
with: | ||
python-version: '3.11' | ||
|
||
- id: install-dynamodb | ||
name: Install DynamoDB | ||
uses: ./.github/actions/install-dynamodb | ||
|
||
- name: Install Poetry | ||
run: pipx install poetry | ||
|
||
- name: Install Dependencies | ||
run: poetry install --all-extras | ||
|
||
- name: Pytest | ||
run: poetry run pytest --dynamodb-dir=${{ steps.install-dynamodb.outputs.dynamodb-dir }} --cov --cov-report=xml | ||
|
||
- name: MyPy | ||
run: poetry run mypy . | ||
|
||
- name: Upload to Codecov.io | ||
uses: codecov/codecov-action@v4 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
*.egg-info | ||
__pycache__ | ||
.coverage | ||
.tox | ||
.vscode | ||
build | ||
dist | ||
_version.py | ||
dynamodb-local-metadata.json |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
{ | ||
"recommendations": [ | ||
"charliermarsh.ruff" | ||
] | ||
} |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,188 @@ | ||
from abc import ABC, abstractmethod | ||
from dataclasses import dataclass | ||
from typing import Any, Iterable, Optional | ||
|
||
from mypy_boto3_dynamodb.service_resource import DynamoDBServiceResource | ||
|
||
from .types import DynamoDBItem | ||
|
||
|
||
@dataclass(frozen=True) | ||
class BaseDynamoDBAutoIncrement(ABC): | ||
dynamodb: DynamoDBServiceResource | ||
counter_table_name: str | ||
counter_table_key: DynamoDBItem | ||
attribute_name: str | ||
table_name: str | ||
initial_value: int | ||
dangerously: bool = False | ||
|
||
@abstractmethod | ||
def next(self, item: DynamoDBItem) -> tuple[Iterable[dict[str, Any]], str]: | ||
raise NotImplementedError | ||
|
||
def _put_item(self, *, TableName, **kwargs): | ||
# FIXME: DynamoDB resource does not have put_item method; emulate it | ||
self.dynamodb.Table(TableName).put_item(**kwargs) | ||
|
||
def _get_item(self, *, TableName, **kwargs): | ||
# FIXME: DynamoDB resource does not have get_item method; emulate it | ||
return self.dynamodb.Table(TableName).get_item(**kwargs) | ||
|
||
def _query(self, *, TableName, **kwargs): | ||
return self.dynamodb.Table(TableName).query(**kwargs) | ||
|
||
def put(self, item: DynamoDBItem): | ||
TransactionCanceledException = ( | ||
self.dynamodb.meta.client.exceptions.TransactionCanceledException | ||
) | ||
while True: | ||
puts, next_counter = self.next(item) | ||
if self.dangerously: | ||
for put in puts: | ||
self._put_item(**put) | ||
else: | ||
try: | ||
self.dynamodb.transact_write_items( | ||
TransactItems=[{"Put": put} for put in puts] | ||
) | ||
except TransactionCanceledException: | ||
continue | ||
return next_counter | ||
|
||
|
||
class DynamoDBAutoIncrement(BaseDynamoDBAutoIncrement): | ||
def next(self, item): | ||
counter = ( | ||
self._get_item( | ||
AttributesToGet=[self.attribute_name], | ||
Key=self.counter_table_key, | ||
TableName=self.counter_table_name, | ||
) | ||
.get("Item", {}) | ||
.get(self.attribute_name) | ||
) | ||
|
||
if counter is None: | ||
next_counter = self.initial_value | ||
put_kwargs = {"ConditionExpression": "attribute_not_exists(#counter)"} | ||
else: | ||
next_counter = counter + 1 | ||
put_kwargs = { | ||
"ConditionExpression": "#counter = :counter", | ||
"ExpressionAttributeValues": { | ||
":counter": counter, | ||
}, | ||
} | ||
|
||
puts = [ | ||
{ | ||
**put_kwargs, | ||
"ExpressionAttributeNames": { | ||
"#counter": self.attribute_name, | ||
}, | ||
"Item": { | ||
**self.counter_table_key, | ||
self.attribute_name: next_counter, | ||
}, | ||
"TableName": self.counter_table_name, | ||
}, | ||
{ | ||
"ConditionExpression": "attribute_not_exists(#counter)", | ||
"ExpressionAttributeNames": { | ||
"#counter": self.attribute_name, | ||
}, | ||
"Item": {self.attribute_name: next_counter, **item}, | ||
"TableName": self.table_name, | ||
}, | ||
] | ||
|
||
return puts, next_counter | ||
|
||
|
||
class DynamoDBHistoryAutoIncrement(BaseDynamoDBAutoIncrement): | ||
def list(self) -> list[int]: | ||
result = self._query( | ||
TableName=self.table_name, | ||
ExpressionAttributeNames={ | ||
**{f"#{i}": key for i, key in enumerate(self.counter_table_key.keys())}, | ||
"#counter": self.attribute_name, | ||
}, | ||
ExpressionAttributeValues={ | ||
f":{i}": value | ||
for i, value in enumerate(self.counter_table_key.values()) | ||
}, | ||
KeyConditionExpression=" AND ".join( | ||
f"#{i} = :{i}" for i in range(len(self.counter_table_key.keys())) | ||
), | ||
ProjectionExpression="#counter", | ||
) | ||
return sorted(item[self.attribute_name] for item in result["Items"]) | ||
|
||
def get(self, version: Optional[int] = None) -> DynamoDBItem: | ||
if version is None: | ||
kwargs = { | ||
"TableName": self.counter_table_name, | ||
"Key": self.counter_table_key, | ||
} | ||
else: | ||
kwargs = { | ||
"TableName": self.table_name, | ||
"Key": {**self.counter_table_key, self.attribute_name: version}, | ||
} | ||
return self._get_item(**kwargs).get("Item") | ||
|
||
def next(self, item): | ||
existing_item = self._get_item( | ||
TableName=self.counter_table_name, | ||
Key=self.counter_table_key, | ||
).get("Item") | ||
|
||
counter = ( | ||
None if existing_item is None else existing_item.get(self.attribute_name) | ||
) | ||
|
||
if counter is None: | ||
next_counter = self.initial_value | ||
put_kwargs = {"ConditionExpression": "attribute_not_exists(#counter)"} | ||
else: | ||
next_counter = counter + 1 | ||
put_kwargs = { | ||
"ConditionExpression": "#counter = :counter", | ||
"ExpressionAttributeValues": { | ||
":counter": counter, | ||
}, | ||
} | ||
|
||
if existing_item is not None and counter is not None: | ||
existing_item[self.attribute_name] = next_counter | ||
next_counter += 1 | ||
|
||
puts = [ | ||
{ | ||
**put_kwargs, | ||
"ExpressionAttributeNames": { | ||
"#counter": self.attribute_name, | ||
}, | ||
"Item": { | ||
**item, | ||
**self.counter_table_key, | ||
self.attribute_name: next_counter, | ||
}, | ||
"TableName": self.counter_table_name, | ||
}, | ||
] | ||
|
||
if existing_item is not None: | ||
puts.append( | ||
{ | ||
"ConditionExpression": "attribute_not_exists(#counter)", | ||
"ExpressionAttributeNames": { | ||
"#counter": self.attribute_name, | ||
}, | ||
"Item": existing_item, | ||
"TableName": self.table_name, | ||
} | ||
) | ||
|
||
return puts, next_counter |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
import pytest | ||
|
||
|
||
@pytest.fixture | ||
def create_tables(dynamodb): | ||
for kwargs in [ | ||
{ | ||
"AttributeDefinitions": [ | ||
{"AttributeName": "tableName", "AttributeType": "S"} | ||
], | ||
"BillingMode": "PAY_PER_REQUEST", | ||
"KeySchema": [{"AttributeName": "tableName", "KeyType": "HASH"}], | ||
"TableName": "autoincrement", | ||
}, | ||
{ | ||
"BillingMode": "PAY_PER_REQUEST", | ||
"AttributeDefinitions": [ | ||
{"AttributeName": "widgetID", "AttributeType": "N"} | ||
], | ||
"KeySchema": [{"AttributeName": "widgetID", "KeyType": "HASH"}], | ||
"TableName": "widgets", | ||
}, | ||
{ | ||
"BillingMode": "PAY_PER_REQUEST", | ||
"AttributeDefinitions": [ | ||
{"AttributeName": "widgetID", "AttributeType": "N"}, | ||
{"AttributeName": "version", "AttributeType": "N"}, | ||
], | ||
"KeySchema": [ | ||
{"AttributeName": "widgetID", "KeyType": "HASH"}, | ||
{"AttributeName": "version", "KeyType": "RANGE"}, | ||
], | ||
"TableName": "widgetHistory", | ||
}, | ||
]: | ||
dynamodb.create_table(**kwargs) |
Oops, something went wrong.