Skip to content

Commit 53b31f8

Browse files
authored
feature: add DAL interface (#587)
1 parent 1603800 commit 53b31f8

File tree

10 files changed

+211
-251
lines changed

10 files changed

+211
-251
lines changed

.github/workflows/codeql-analysis.yml

-70
This file was deleted.

.vscode/settings.json

+2
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@
22
"cSpell.words": [
33
"appconfig",
44
"Codecov",
5+
"getlogin",
56
"getuid",
67
"mkdocs",
78
"mypy",
89
"ranisenberg",
910
"runtimes",
11+
"Stubber",
1012
"yapf"
1113
]
1214
}

mypy.ini

+9
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,12 @@ show_column_numbers = True
88
show_error_codes = True
99
show_error_context = True
1010

11+
12+
# Disable specific error codes in the 'tests' package
13+
[mypy-tests.*]
14+
disable_error_code = annotation-unchecked
15+
allow_untyped_defs = True
16+
1117
[mypy-jmespath]
1218
ignore_missing_imports=True
1319

@@ -23,6 +29,9 @@ ignore_missing_imports = True
2329
[mypy-botocore]
2430
ignore_missing_imports = True
2531

32+
[mypy-botocore.stub]
33+
ignore_missing_imports = True
34+
2635
[mypy-botocore.response]
2736
ignore_missing_imports = True
2837

poetry.lock

+125-125
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

service/dal/db_handler.py

+5-32
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,10 @@
1-
import uuid
2-
3-
import boto3
4-
from botocore.exceptions import ClientError
5-
from cachetools import TTLCache, cached
6-
from mypy_boto3_dynamodb import DynamoDBServiceResource
7-
from mypy_boto3_dynamodb.service_resource import Table
8-
from pydantic import ValidationError
1+
from abc import ABC, abstractmethod
92

103
from service.dal.schemas.db import OrderEntry
11-
from service.handlers.utils.observability import logger, tracer
12-
from service.schemas.exceptions import InternalServerException
13-
14-
15-
# cache dynamodb connection data for no longer than 5 minutes
16-
@cached(cache=TTLCache(maxsize=1, ttl=300))
17-
def _get_db_handler(table_name: str) -> Table:
18-
dynamodb: DynamoDBServiceResource = boto3.resource('dynamodb')
19-
return dynamodb.Table(table_name)
204

215

22-
@tracer.capture_method(capture_response=False)
23-
def create_order_in_db(table_name: str, customer_name: str, order_item_count: int) -> OrderEntry:
24-
order_id = str(uuid.uuid4())
25-
logger.info('trying to save order', extra={'order_id': order_id})
26-
try:
27-
entry = OrderEntry(order_id=order_id, customer_name=customer_name, order_item_count=order_item_count)
28-
logger.info('opening connection to dynamodb table', extra={'table_name': table_name})
29-
table: Table = _get_db_handler(table_name)
30-
table.put_item(Item=entry.dict())
31-
except (ClientError, ValidationError) as exc:
32-
error_msg = 'failed to create order'
33-
logger.exception(error_msg, extra={'exception': str(exc), 'customer_name': customer_name})
34-
raise InternalServerException(error_msg) from exc
6+
class DalHandler(ABC):
357

36-
logger.info('finished create order', extra={'order_id': order_id, 'order_item_count': order_item_count, 'customer_name': customer_name})
37-
return entry
8+
@abstractmethod
9+
def create_order_in_db(self, customer_name: str, order_item_count: int) -> OrderEntry:
10+
... # pragma: no cover

service/dal/dynamo_dal_handler.py

+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import uuid
2+
3+
import boto3
4+
from botocore.exceptions import ClientError
5+
from cachetools import TTLCache, cached
6+
from mypy_boto3_dynamodb import DynamoDBServiceResource
7+
from mypy_boto3_dynamodb.service_resource import Table
8+
from pydantic import ValidationError
9+
10+
from service.dal.db_handler import DalHandler
11+
from service.dal.schemas.db import OrderEntry
12+
from service.handlers.utils.observability import logger, tracer
13+
from service.schemas.exceptions import InternalServerException
14+
15+
16+
class DynamoDalHandler(DalHandler):
17+
18+
def __init__(self, table_name: str):
19+
self.table_name = table_name
20+
21+
# cache dynamodb connection data for no longer than 5 minutes
22+
@cached(cache=TTLCache(maxsize=1, ttl=300))
23+
def _get_db_handler(self) -> Table:
24+
dynamodb: DynamoDBServiceResource = boto3.resource('dynamodb')
25+
return dynamodb.Table(self.table_name)
26+
27+
@tracer.capture_method(capture_response=False)
28+
def create_order_in_db(self, customer_name: str, order_item_count: int) -> OrderEntry:
29+
order_id = str(uuid.uuid4())
30+
logger.info('trying to save order', extra={'order_id': order_id})
31+
try:
32+
entry = OrderEntry(order_id=order_id, customer_name=customer_name, order_item_count=order_item_count)
33+
logger.info('opening connection to dynamodb table', extra={'table_name': self.table_name})
34+
table: Table = self._get_db_handler()
35+
table.put_item(Item=entry.dict())
36+
except (ClientError, ValidationError) as exc:
37+
error_msg = 'failed to create order'
38+
logger.exception(error_msg, extra={'exception': str(exc), 'customer_name': customer_name})
39+
raise InternalServerException(error_msg) from exc
40+
41+
logger.info('finished create order', extra={'order_id': order_id, 'order_item_count': order_item_count, 'customer_name': customer_name})
42+
return entry

service/handlers/create_order.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ def create_order(event: Dict[str, Any], context: LambdaContext) -> Dict[str, Any
4343
logger.error('event failed input validation', extra={'error': str(exc)})
4444
return build_response(http_status=HTTPStatus.BAD_REQUEST, body={})
4545

46-
metrics.add_metric(name='ValidEvents', unit=MetricUnit.Count, value=1)
46+
metrics.add_metric(name='ValidCreateOrderEvents', unit=MetricUnit.Count, value=1)
4747
try:
4848
response: CreateOrderOutput = handle_create_request(
4949
customer_name=input.customer_name,

service/logic/handle_create_request.py

+10-3
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,18 @@
1-
from service.dal.db_handler import create_order_in_db
1+
from functools import lru_cache
2+
3+
from service.dal.dynamo_dal_handler import DynamoDalHandler
24
from service.dal.schemas.db import OrderEntry
35
from service.handlers.schemas.dynamic_configuration import FeatureFlagsNames
46
from service.handlers.utils.dynamic_configuration import get_dynamic_configuration_store
57
from service.handlers.utils.observability import logger, tracer
68
from service.schemas.output import CreateOrderOutput
79

810

11+
@lru_cache
12+
def get_dynamodb_dal_handler(table_name: str) -> DynamoDalHandler:
13+
return DynamoDalHandler(table_name)
14+
15+
916
@tracer.capture_method(capture_response=False)
1017
def handle_create_request(customer_name: str, order_item_count: int, table_name: str) -> CreateOrderOutput:
1118
logger.info('starting to handle create request', extra={'order_item_count': order_item_count, 'customer_name': customer_name})
@@ -24,7 +31,7 @@ def handle_create_request(customer_name: str, order_item_count: int, table_name:
2431
default=False,
2532
)
2633
logger.debug('premium feature flag value', extra={'premium': premium})
27-
28-
order: OrderEntry = create_order_in_db(table_name, customer_name, order_item_count)
34+
dal_handler: DynamoDalHandler = get_dynamodb_dal_handler(table_name)
35+
order: OrderEntry = dal_handler.create_order_in_db(customer_name, order_item_count)
2936
# convert from db entry to output, they won't always be the same
3037
return CreateOrderOutput(customer_name=order.customer_name, order_item_count=order.order_item_count, order_id=order.order_id)

tests/integration/test_create_order.py

+8-9
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@
44

55
import boto3
66
from aws_lambda_powertools.utilities.feature_flags.exceptions import SchemaValidationError
7-
from botocore.exceptions import ClientError
7+
from botocore.stub import Stubber
88

9+
from service.dal.dynamo_dal_handler import DynamoDalHandler
910
from service.handlers.create_order import create_order
1011
from service.handlers.schemas.input import Input
1112
from tests.utils import generate_api_gw_event, generate_context
@@ -64,17 +65,15 @@ def test_handler_200_ok(mocker, table_name: str):
6465
assert response['Item']['order_item_count'] == order_item_count
6566

6667

67-
def test_internal_server_error(mocker):
68-
69-
def db_mock_function(table_name: str):
70-
raise ClientError(error_response={}, operation_name='put_item')
71-
72-
db_mock = mocker.patch('service.dal.db_handler._get_db_handler')
73-
db_mock.side_effect = db_mock_function
68+
def test_internal_server_error():
69+
db_handler: DynamoDalHandler = DynamoDalHandler('table')
70+
table = db_handler._get_db_handler()
71+
stubber = Stubber(table.meta.client)
72+
stubber.add_client_error(method='put_item', service_error_code='ValidationException')
73+
stubber.activate()
7474
body = Input(customer_name='RanTheBuilder', order_item_count=5)
7575
response = create_order(generate_api_gw_event(body.dict()), generate_context())
7676
assert response['statusCode'] == HTTPStatus.INTERNAL_SERVER_ERROR
77-
db_mock.assert_called
7877

7978

8079
def test_handler_bad_request(mocker):

tests/unit/test_db_handler.py

+9-11
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,15 @@
11
import pytest
2-
from botocore.exceptions import ClientError
2+
from botocore.stub import Stubber
33

4-
from service.dal.db_handler import create_order_in_db
4+
from service.dal.dynamo_dal_handler import DynamoDalHandler
55
from service.schemas.exceptions import InternalServerException
66

77

8-
def test_raise_exception(mocker):
9-
10-
def db_mock_function(table_name: str):
11-
raise ClientError(error_response={}, operation_name='put_item')
12-
13-
db_mock = mocker.patch('service.dal.db_handler._get_db_handler')
14-
db_mock.side_effect = db_mock_function
8+
def test_raise_exception():
9+
db_handler: DynamoDalHandler = DynamoDalHandler('table')
10+
table = db_handler._get_db_handler()
11+
stubber = Stubber(table.meta.client)
12+
stubber.add_client_error(method='put_item', service_error_code='ValidationException')
13+
stubber.activate()
1514
with pytest.raises(InternalServerException):
16-
create_order_in_db(table_name='table', customer_name='customer', order_item_count=5)
17-
db_mock.assert_called
15+
db_handler.create_order_in_db(customer_name='customer', order_item_count=5)

0 commit comments

Comments
 (0)