Skip to content

Commit 3f25d67

Browse files
committed
Implement aiobotocore patch
1 parent f6f6ecd commit 3f25d67

File tree

7 files changed

+124
-2
lines changed

7 files changed

+124
-2
lines changed

fastapi_users_db_dynamodb/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
from fastapi_users.db.base import BaseUserDatabase
2727
from fastapi_users.models import ID, OAP, UP
2828

29+
from fastapi_users_db_dynamodb._aioboto3_patch import * # noqa: F403
2930
from fastapi_users_db_dynamodb.generics import GUID
3031

3132
__version__ = "1.0.0"
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import inspect
2+
from binascii import crc32
3+
4+
import aiobotocore.endpoint
5+
import aiobotocore.retryhandler
6+
from aiobotocore.endpoint import HttpxStreamingBody, StreamingBody
7+
from aiobotocore.retryhandler import ChecksumError, logger
8+
9+
try:
10+
import httpx
11+
except ImportError:
12+
httpx = None
13+
14+
15+
async def _fixed_check_response(self, attempt_number, response):
16+
http_response = response[0]
17+
expected_crc = http_response.headers.get(self._header_name)
18+
if expected_crc is None:
19+
logger.debug(
20+
"crc32 check skipped, the %s header is not in the http response.",
21+
self._header_name,
22+
)
23+
else:
24+
if inspect.isawaitable(http_response.content):
25+
data_buf = await http_response.content
26+
else:
27+
data_buf = http_response.content
28+
29+
actual_crc32 = crc32(data_buf) & 0xFFFFFFFF
30+
if not actual_crc32 == int(expected_crc):
31+
logger.debug(
32+
"retry needed: crc32 check failed, expected != actual: %s != %s",
33+
int(expected_crc),
34+
actual_crc32,
35+
)
36+
raise ChecksumError(
37+
checksum_type="crc32",
38+
expected_checksum=int(expected_crc),
39+
actual_checksum=actual_crc32,
40+
)
41+
42+
43+
async def convert_to_response_dict(http_response, operation_model):
44+
"""Convert an HTTP response object to a request dict.
45+
46+
This converts the HTTP response object to a dictionary.
47+
48+
:type http_response: botocore.awsrequest.AWSResponse
49+
:param http_response: The HTTP response from an AWS service request.
50+
51+
:rtype: dict
52+
:return: A response dictionary which will contain the following keys:
53+
* headers (dict)
54+
* status_code (int)
55+
* body (string or file-like object)
56+
57+
"""
58+
response_dict = {
59+
"headers": http_response.headers,
60+
"status_code": http_response.status_code,
61+
"context": {
62+
"operation_name": operation_model.name,
63+
},
64+
}
65+
if response_dict["status_code"] >= 300:
66+
if inspect.isawaitable(http_response.content):
67+
response_dict["body"] = await http_response.content
68+
else:
69+
response_dict["body"] = http_response.content
70+
elif operation_model.has_event_stream_output:
71+
response_dict["body"] = http_response.raw
72+
elif operation_model.has_streaming_output:
73+
if httpx and isinstance(http_response.raw, httpx.Response):
74+
response_dict["body"] = HttpxStreamingBody(http_response.raw)
75+
else:
76+
length = response_dict["headers"].get("content-length")
77+
response_dict["body"] = StreamingBody(http_response.raw, length)
78+
else:
79+
if inspect.isawaitable(http_response.content):
80+
response_dict["body"] = await http_response.content
81+
else:
82+
response_dict["body"] = http_response.content
83+
return response_dict
84+
85+
86+
aiobotocore.retryhandler.AioCRC32Checker._check_response = _fixed_check_response
87+
aiobotocore.endpoint.convert_to_response_dict = convert_to_response_dict

fastapi_users_db_dynamodb/access_token.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
from fastapi_users.authentication.strategy.db import AP, AccessTokenDatabase
1616
from fastapi_users.models import ID
1717

18+
from fastapi_users_db_dynamodb._aioboto3_patch import * # noqa: F403
19+
1820

1921
class DynamoDBBaseAccessTokenTable(Generic[ID]):
2022
"""Base access token table schema for DynamoDB."""

pyproject.toml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ dependencies = [
3737
"asgi_lifespan",
3838
"ruff",
3939
"moto[all]",
40+
"types-aioboto3",
41+
"types-aiobotocore",
4042
]
4143

4244
[tool.hatch.envs.default.scripts]
@@ -84,8 +86,8 @@ classifiers = [
8486
]
8587
requires-python = ">=3.9"
8688
dependencies = [
87-
"fastapi-users >= 10.0.0",
88-
"aioboto3 >= 11.0.0",
89+
"fastapi-users >= 14.0.0",
90+
"aioboto3 >= 15.0.0",
8991
]
9092

9193
[project.urls]

tests/tables.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import aioboto3
2+
3+
4+
async def ensure_table_exists(session: aioboto3.Session, table_name: str, region: str):
5+
async with session.resource("dynamodb", region_name=region) as client:
6+
try:
7+
await client.describe_table(TableName=table_name)
8+
except client.exceptions.ResourceNotFoundException:
9+
await client.create_table(
10+
TableName=table_name,
11+
KeySchema=[
12+
{"AttributeName": "id", "KeyType": "HASH"},
13+
],
14+
AttributeDefinitions=[
15+
{"AttributeName": "id", "AttributeType": "S"},
16+
],
17+
ProvisionedThroughput={"ReadCapacityUnits": 5, "WriteCapacityUnits": 5},
18+
)
19+
20+
waiter = client.get_waiter("table_exists")
21+
await waiter.wait(TableName=table_name)

tests/test_access_token.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,13 @@
99
from pydantic import UUID4
1010

1111
from fastapi_users_db_dynamodb import DynamoDBBaseUserTableUUID, DynamoDBUserDatabase
12+
from fastapi_users_db_dynamodb._aioboto3_patch import * # noqa: F403
1213
from fastapi_users_db_dynamodb.access_token import (
1314
DynamoDBAccessTokenDatabase,
1415
DynamoDBBaseAccessTokenTableUUID,
1516
)
1617
from tests.conftest import DATABASE_REGION
18+
from tests.tables import ensure_table_exists
1719

1820

1921
class Base:
@@ -41,6 +43,8 @@ async def dynamodb_access_token_db(
4143
session = aioboto3.Session()
4244
user_table_name = "users_test"
4345
token_table_name = "access_tokens_test"
46+
await ensure_table_exists(session, user_table_name, DATABASE_REGION)
47+
await ensure_table_exists(session, token_table_name, DATABASE_REGION)
4448

4549
user_db = DynamoDBUserDatabase(
4650
session,

tests/test_users.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@
1111
DynamoDBBaseUserTableUUID,
1212
DynamoDBUserDatabase,
1313
)
14+
from fastapi_users_db_dynamodb._aioboto3_patch import * # noqa: F403
1415
from tests.conftest import DATABASE_REGION
16+
from tests.tables import ensure_table_exists
1517

1618

1719
class Base:
@@ -40,6 +42,7 @@ async def dynamodb_user_db() -> AsyncGenerator[DynamoDBUserDatabase, None]:
4042
with mock_aws():
4143
session = aioboto3.Session()
4244
table_name = "users_test"
45+
await ensure_table_exists(session, table_name, DATABASE_REGION)
4346

4447
db = DynamoDBUserDatabase(
4548
session,
@@ -56,6 +59,8 @@ async def dynamodb_user_db_oauth() -> AsyncGenerator[DynamoDBUserDatabase, None]
5659
session = aioboto3.Session()
5760
user_table_name = "users_test_oauth"
5861
oauth_table_name = "oauth_accounts_test"
62+
await ensure_table_exists(session, user_table_name, DATABASE_REGION)
63+
await ensure_table_exists(session, oauth_table_name, DATABASE_REGION)
5964

6065
db = DynamoDBUserDatabase(
6166
session,

0 commit comments

Comments
 (0)