boto3-assist 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- boto3_assist/__init__.py +0 -0
- boto3_assist/boto3session.py +173 -0
- boto3_assist/dynamodb/dynamodb.py +445 -0
- boto3_assist/dynamodb/dynamodb_connection.py +128 -0
- boto3_assist/dynamodb/dynamodb_connection_tracker.py +46 -0
- boto3_assist/dynamodb/dynamodb_helpers.py +323 -0
- boto3_assist/dynamodb/dynamodb_importer.py +85 -0
- boto3_assist/dynamodb/dynamodb_index.py +181 -0
- boto3_assist/dynamodb/dynamodb_iservice.py +25 -0
- boto3_assist/dynamodb/dynamodb_key.py +61 -0
- boto3_assist/dynamodb/dynamodb_model_base.py +319 -0
- boto3_assist/dynamodb/dynamodb_model_base_interfaces.py +34 -0
- boto3_assist/dynamodb/dynamodb_reindexer.py +166 -0
- boto3_assist/dynamodb/readme.md +68 -0
- boto3_assist/dynamodb/troubleshooting.md +5 -0
- boto3_assist/environment_services/__init__.py +0 -0
- boto3_assist/environment_services/environment_loader.py +47 -0
- boto3_assist/environment_services/environment_variables.py +199 -0
- boto3_assist/utilities/datetime_utility.py +319 -0
- boto3_assist/utilities/logging_utility.py +0 -0
- boto3_assist/utilities/serialization_utility.py +119 -0
- boto3_assist/utilities/string_utility.py +232 -0
- boto3_assist/version.py +1 -0
- boto3_assist-0.1.0.dist-info/METADATA +54 -0
- boto3_assist-0.1.0.dist-info/RECORD +28 -0
- boto3_assist-0.1.0.dist-info/WHEEL +4 -0
- boto3_assist-0.1.0.dist-info/licenses/LICENSE-EXPLAINED.txt +11 -0
- boto3_assist-0.1.0.dist-info/licenses/LICENSE.txt +21 -0
boto3_assist/__init__.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Geek Cafe, LLC
|
|
3
|
+
Maintainers: Eric Wilson
|
|
4
|
+
MIT License. See Project Root for the license information.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Any, Optional
|
|
8
|
+
|
|
9
|
+
import boto3
|
|
10
|
+
from aws_lambda_powertools import Logger
|
|
11
|
+
from botocore.config import Config
|
|
12
|
+
from botocore.exceptions import ProfileNotFound
|
|
13
|
+
from boto3_assist.environment_services.environment_variables import EnvironmentVariables
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
logger = Logger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Boto3SessionManager:
|
|
20
|
+
"""Manages Boto3 Sessions"""
|
|
21
|
+
|
|
22
|
+
def __init__(
|
|
23
|
+
self,
|
|
24
|
+
service_name: str,
|
|
25
|
+
*,
|
|
26
|
+
aws_profile: Optional[str] = None,
|
|
27
|
+
aws_region: Optional[str] = None,
|
|
28
|
+
assume_role_arn: Optional[str] = None,
|
|
29
|
+
assume_role_session_name: Optional[str] = None,
|
|
30
|
+
cross_account_role_arn: Optional[str] = None,
|
|
31
|
+
config: Optional[Config] = None,
|
|
32
|
+
aws_endpoint_url: Optional[str] = None,
|
|
33
|
+
aws_access_key_id: Optional[str] = None,
|
|
34
|
+
aws_secret_access_key: Optional[str] = None,
|
|
35
|
+
aws_session_token: Optional[str] = None,
|
|
36
|
+
):
|
|
37
|
+
self.service_name = service_name
|
|
38
|
+
self.aws_profile = aws_profile
|
|
39
|
+
self.aws_region = aws_region
|
|
40
|
+
self.assume_role_arn = assume_role_arn
|
|
41
|
+
self.assume_role_session_name = assume_role_session_name
|
|
42
|
+
self.config = config
|
|
43
|
+
self.cross_account_role_arn = cross_account_role_arn
|
|
44
|
+
self.endpoint_url = aws_endpoint_url
|
|
45
|
+
self.aws_access_key_id = aws_access_key_id
|
|
46
|
+
self.aws_secret_access_key = aws_secret_access_key
|
|
47
|
+
self.aws_session_token = aws_session_token
|
|
48
|
+
self.__session: Any = None
|
|
49
|
+
self.__client: Any = None
|
|
50
|
+
self.__resource: Any = None
|
|
51
|
+
|
|
52
|
+
self.__setup()
|
|
53
|
+
|
|
54
|
+
def __setup(self):
|
|
55
|
+
"""Setup AWS session, client, and resource."""
|
|
56
|
+
|
|
57
|
+
profile = self.aws_profile or EnvironmentVariables.AWS.profile()
|
|
58
|
+
region = self.aws_region or EnvironmentVariables.AWS.region()
|
|
59
|
+
if self.assume_role_arn:
|
|
60
|
+
self.__assume_role()
|
|
61
|
+
else:
|
|
62
|
+
logger.debug("Connecting without assuming a role.")
|
|
63
|
+
self.__session = self.__get_aws_session(profile, region)
|
|
64
|
+
|
|
65
|
+
def __assume_role(self):
|
|
66
|
+
"""Assume an AWS IAM role."""
|
|
67
|
+
try:
|
|
68
|
+
logger.debug(f"Assuming role {self.assume_role_arn}")
|
|
69
|
+
sts_client = boto3.client("sts")
|
|
70
|
+
session_name = (
|
|
71
|
+
self.assume_role_session_name
|
|
72
|
+
or f"AssumeRoleSessionFor{self.service_name}"
|
|
73
|
+
)
|
|
74
|
+
if not self.assume_role_arn:
|
|
75
|
+
raise ValueError("assume_role_arn is required")
|
|
76
|
+
assumed_role_response = sts_client.assume_role(
|
|
77
|
+
RoleArn=self.assume_role_arn,
|
|
78
|
+
RoleSessionName=session_name,
|
|
79
|
+
)
|
|
80
|
+
credentials = assumed_role_response["Credentials"]
|
|
81
|
+
self.__session = boto3.Session(
|
|
82
|
+
aws_access_key_id=credentials["AccessKeyId"],
|
|
83
|
+
aws_secret_access_key=credentials["SecretAccessKey"],
|
|
84
|
+
aws_session_token=credentials["SessionToken"],
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
except Exception as e:
|
|
88
|
+
logger.error(f"Error assuming role: {e}")
|
|
89
|
+
raise RuntimeError(f"Failed to assume role {self.assume_role_arn}") from e
|
|
90
|
+
|
|
91
|
+
def __get_aws_session(
|
|
92
|
+
self, aws_profile: Optional[str] = None, aws_region: Optional[str] = None
|
|
93
|
+
) -> boto3.Session:
|
|
94
|
+
"""Get a boto3 session for AWS."""
|
|
95
|
+
logger.debug({"profile": aws_profile, "region": aws_region})
|
|
96
|
+
try:
|
|
97
|
+
self.aws_profile = aws_profile or EnvironmentVariables.AWS.profile()
|
|
98
|
+
self.aws_region = aws_region or EnvironmentVariables.AWS.region()
|
|
99
|
+
tmp_access_key_id = self.aws_access_key_id
|
|
100
|
+
tmp_secret_access_key = self.aws_secret_access_key
|
|
101
|
+
if not EnvironmentVariables.AWS.display_aws_access_key_id():
|
|
102
|
+
tmp_access_key_id = (
|
|
103
|
+
"None" if tmp_access_key_id is None else "***************"
|
|
104
|
+
)
|
|
105
|
+
if not EnvironmentVariables.AWS.display_aws_secret_access_key():
|
|
106
|
+
tmp_secret_access_key = (
|
|
107
|
+
"None" if tmp_secret_access_key is None else "***************"
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
logger.debug(
|
|
111
|
+
{
|
|
112
|
+
"profile": self.aws_profile,
|
|
113
|
+
"region": self.aws_region,
|
|
114
|
+
"aws_access_key_id": tmp_access_key_id,
|
|
115
|
+
"aws_secret_access_key": tmp_secret_access_key,
|
|
116
|
+
"aws_session_token": "*******"
|
|
117
|
+
if self.aws_session_token is not None
|
|
118
|
+
else "",
|
|
119
|
+
}
|
|
120
|
+
)
|
|
121
|
+
logger.debug("Creating boto3 session")
|
|
122
|
+
session = self.__create_boto3_session()
|
|
123
|
+
# if self.aws_profile or self.aws_region
|
|
124
|
+
# else boto3.Session()
|
|
125
|
+
|
|
126
|
+
except Exception as e:
|
|
127
|
+
logger.error(e)
|
|
128
|
+
raise RuntimeError("Failed to create a boto3 session.") from e
|
|
129
|
+
|
|
130
|
+
logger.debug({"session": session})
|
|
131
|
+
return session
|
|
132
|
+
|
|
133
|
+
@property
|
|
134
|
+
def client(self) -> Any:
|
|
135
|
+
"""Return the boto3 client connection."""
|
|
136
|
+
if not self.__client:
|
|
137
|
+
self.__client = self.__session.client(
|
|
138
|
+
self.service_name,
|
|
139
|
+
config=self.config,
|
|
140
|
+
endpoint_url=self.endpoint_url,
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
return self.__client
|
|
144
|
+
|
|
145
|
+
@property
|
|
146
|
+
def resource(self) -> Any:
|
|
147
|
+
"""Return the boto3 resource connection."""
|
|
148
|
+
if not self.__resource:
|
|
149
|
+
self.__resource = self.__session.resource(
|
|
150
|
+
self.service_name,
|
|
151
|
+
config=self.config,
|
|
152
|
+
endpoint_url=self.endpoint_url,
|
|
153
|
+
)
|
|
154
|
+
return self.__resource
|
|
155
|
+
|
|
156
|
+
def __create_boto3_session(self) -> boto3.Session:
|
|
157
|
+
try:
|
|
158
|
+
session = boto3.Session(
|
|
159
|
+
profile_name=self.aws_profile,
|
|
160
|
+
region_name=self.aws_region,
|
|
161
|
+
aws_access_key_id=self.aws_access_key_id,
|
|
162
|
+
aws_secret_access_key=self.aws_secret_access_key,
|
|
163
|
+
aws_session_token=self.aws_session_token,
|
|
164
|
+
)
|
|
165
|
+
return session
|
|
166
|
+
except ProfileNotFound as e:
|
|
167
|
+
print(
|
|
168
|
+
f"An error occurred setting up the boto3 sessions. Profile not found: {e}"
|
|
169
|
+
)
|
|
170
|
+
raise e
|
|
171
|
+
except Exception as e:
|
|
172
|
+
print(f"An error occurred setting up the boto3 sessions: {e}")
|
|
173
|
+
raise e
|
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Geek Cafe, LLC
|
|
3
|
+
Maintainers: Eric Wilson
|
|
4
|
+
MIT License. See Project Root for the license information.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
from typing import List, Optional, overload
|
|
9
|
+
|
|
10
|
+
from aws_lambda_powertools import Tracer, Logger
|
|
11
|
+
from boto3.dynamodb.conditions import (
|
|
12
|
+
Key,
|
|
13
|
+
# And,
|
|
14
|
+
# Equals,
|
|
15
|
+
ComparisonCondition,
|
|
16
|
+
ConditionBase,
|
|
17
|
+
)
|
|
18
|
+
from boto3_assist.dynamodb.dynamodb_connection import DynamoDBConnection
|
|
19
|
+
from boto3_assist.dynamodb.dynamodb_helpers import DynamoDBHelpers
|
|
20
|
+
from boto3_assist.dynamodb.dynamodb_model_base import DynamoDBModelBase
|
|
21
|
+
from boto3_assist.utilities.string_utility import StringUtility
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
logger = Logger()
|
|
25
|
+
tracer = Tracer()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class DynamoDB(DynamoDBConnection):
|
|
29
|
+
"""
|
|
30
|
+
DynamoDB. Wrapper for basic DynamoDB Connection and Actions
|
|
31
|
+
|
|
32
|
+
Inherits:
|
|
33
|
+
DynamoDBConnection
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def __init__(
|
|
37
|
+
self,
|
|
38
|
+
*,
|
|
39
|
+
aws_profile: Optional[str] = None,
|
|
40
|
+
aws_region: Optional[str] = None,
|
|
41
|
+
aws_end_point_url: Optional[str] = None,
|
|
42
|
+
aws_access_key_id: Optional[str] = None,
|
|
43
|
+
aws_secret_access_key: Optional[str] = None,
|
|
44
|
+
) -> None:
|
|
45
|
+
super().__init__(
|
|
46
|
+
aws_profile=aws_profile,
|
|
47
|
+
aws_region=aws_region,
|
|
48
|
+
aws_end_point_url=aws_end_point_url,
|
|
49
|
+
aws_access_key_id=aws_access_key_id,
|
|
50
|
+
aws_secret_access_key=aws_secret_access_key,
|
|
51
|
+
)
|
|
52
|
+
self.helpers: DynamoDBHelpers = DynamoDBHelpers()
|
|
53
|
+
self.log_dynamodb_item_size = (
|
|
54
|
+
str(os.getenv("LOG_DYNAMODB_ITEM_SIZE", "false")).lower() == "true"
|
|
55
|
+
)
|
|
56
|
+
logger.setLevel(os.getenv("LOG_LEVEL", "INFO"))
|
|
57
|
+
|
|
58
|
+
@tracer.capture_method
|
|
59
|
+
def save(
|
|
60
|
+
self,
|
|
61
|
+
item: dict | DynamoDBModelBase,
|
|
62
|
+
table_name: str,
|
|
63
|
+
source: Optional[str] = None,
|
|
64
|
+
) -> dict:
|
|
65
|
+
"""
|
|
66
|
+
Save an item to the database
|
|
67
|
+
Args:
|
|
68
|
+
item (dict): DynamoDB Dictionay Object or DynamoDBModelBase. Supports the "client" or
|
|
69
|
+
"resource" syntax
|
|
70
|
+
table_name (str): The DyamoDb Table Name
|
|
71
|
+
source (str, optional): The source of the call, used for logging. Defaults to None.
|
|
72
|
+
|
|
73
|
+
Raises:
|
|
74
|
+
e: Any Error Raised
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
dict: The Response from DynamoDB's put_item actions.
|
|
78
|
+
It does not return the saved object, only the response.
|
|
79
|
+
"""
|
|
80
|
+
response = None
|
|
81
|
+
|
|
82
|
+
try:
|
|
83
|
+
if not isinstance(item, dict):
|
|
84
|
+
# attemp to convert it
|
|
85
|
+
try:
|
|
86
|
+
item = item.to_resource_dictionary()
|
|
87
|
+
except Exception as e: # pylint: disable=w0718
|
|
88
|
+
logger.exception(e)
|
|
89
|
+
raise ValueError("Unsupported item or module was passed.") from e
|
|
90
|
+
|
|
91
|
+
if isinstance(item, dict):
|
|
92
|
+
self.__log_item_size(item=item)
|
|
93
|
+
|
|
94
|
+
if isinstance(item, dict) and isinstance(next(iter(item.values())), dict):
|
|
95
|
+
# Use boto3.client syntax
|
|
96
|
+
response = self.dynamodb_client.put_item(
|
|
97
|
+
TableName=table_name, Item=item
|
|
98
|
+
)
|
|
99
|
+
else:
|
|
100
|
+
# Use boto3.resource syntax
|
|
101
|
+
table = self.dynamodb_resource.Table(table_name)
|
|
102
|
+
response = table.put_item(Item=item)
|
|
103
|
+
|
|
104
|
+
except Exception as e: # pylint: disable=w0718
|
|
105
|
+
logger.exception(
|
|
106
|
+
{"source": f"{source}", "metric_filter": "put_item", "error": str(e)}
|
|
107
|
+
)
|
|
108
|
+
raise e
|
|
109
|
+
|
|
110
|
+
return response
|
|
111
|
+
|
|
112
|
+
def __log_item_size(self, item: dict):
|
|
113
|
+
if not isinstance(item, dict):
|
|
114
|
+
warning = f"Item is not a dictionary. Type: {type(item).__name__}"
|
|
115
|
+
logger.warning(warning)
|
|
116
|
+
return
|
|
117
|
+
|
|
118
|
+
if self.log_dynamodb_item_size:
|
|
119
|
+
size_bytes: int = StringUtility.get_size_in_bytes(item)
|
|
120
|
+
size_kb: int = StringUtility.get_size_in_kb(item)
|
|
121
|
+
logger.info({"item_size": {"bytes": size_bytes, "kb": f"{size_kb:.2f}kb"}})
|
|
122
|
+
|
|
123
|
+
if size_kb > 390:
|
|
124
|
+
logger.warning(
|
|
125
|
+
{
|
|
126
|
+
"item_size": {
|
|
127
|
+
"bytes": size_bytes,
|
|
128
|
+
"kb": f"{size_kb:.2f}kb",
|
|
129
|
+
},
|
|
130
|
+
"warning": "approaching limit",
|
|
131
|
+
}
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
@overload
|
|
135
|
+
def get(
|
|
136
|
+
self,
|
|
137
|
+
*,
|
|
138
|
+
table_name: str,
|
|
139
|
+
model: DynamoDBModelBase,
|
|
140
|
+
do_projections: bool = False,
|
|
141
|
+
strongly_consistent: bool = False,
|
|
142
|
+
return_consumed_capacity: Optional[str] = None,
|
|
143
|
+
projection_expression: Optional[str] = None,
|
|
144
|
+
expression_attribute_names: Optional[dict] = None,
|
|
145
|
+
source: Optional[str] = None,
|
|
146
|
+
call_type: str = "resource",
|
|
147
|
+
) -> dict: ...
|
|
148
|
+
|
|
149
|
+
@overload
|
|
150
|
+
def get(
|
|
151
|
+
self,
|
|
152
|
+
key: dict,
|
|
153
|
+
table_name: str,
|
|
154
|
+
*,
|
|
155
|
+
strongly_consistent: bool = False,
|
|
156
|
+
return_consumed_capacity: Optional[str] = None,
|
|
157
|
+
projection_expression: Optional[str] = None,
|
|
158
|
+
expression_attribute_names: Optional[dict] = None,
|
|
159
|
+
source: Optional[str] = None,
|
|
160
|
+
call_type: str = "resource",
|
|
161
|
+
) -> dict: ...
|
|
162
|
+
|
|
163
|
+
@tracer.capture_method
|
|
164
|
+
def get(
|
|
165
|
+
self,
|
|
166
|
+
key: Optional[dict] = None,
|
|
167
|
+
table_name: Optional[str] = None,
|
|
168
|
+
model: Optional[DynamoDBModelBase] = None,
|
|
169
|
+
do_projections: bool = False,
|
|
170
|
+
strongly_consistent: bool = False,
|
|
171
|
+
return_consumed_capacity: Optional[str] = None,
|
|
172
|
+
projection_expression: Optional[str] = None,
|
|
173
|
+
expression_attribute_names: Optional[dict] = None,
|
|
174
|
+
source: Optional[str] = None,
|
|
175
|
+
call_type: str = "resource",
|
|
176
|
+
) -> dict:
|
|
177
|
+
"""
|
|
178
|
+
Description:
|
|
179
|
+
generic get_item dynamoDb call
|
|
180
|
+
Parameters:
|
|
181
|
+
key: a dictionary object representing the primary key
|
|
182
|
+
model: a model instance of DynamoDBModelBase
|
|
183
|
+
"""
|
|
184
|
+
|
|
185
|
+
if model is not None:
|
|
186
|
+
if table_name is None:
|
|
187
|
+
raise ValueError("table_name must be provided when model is used.")
|
|
188
|
+
if key is not None:
|
|
189
|
+
raise ValueError("key cannot be provided when model is used.")
|
|
190
|
+
key = model.indexes.primary.key()
|
|
191
|
+
if do_projections:
|
|
192
|
+
projection_expression = model.projection_expression
|
|
193
|
+
expression_attribute_names = model.projection_expression_attribute_names
|
|
194
|
+
elif key is None or table_name is None:
|
|
195
|
+
raise ValueError("Either 'key' or 'model' must be provided.")
|
|
196
|
+
|
|
197
|
+
response = None
|
|
198
|
+
try:
|
|
199
|
+
kwargs = {
|
|
200
|
+
"ConsistentRead": strongly_consistent,
|
|
201
|
+
"ReturnConsumedCapacity": return_consumed_capacity,
|
|
202
|
+
"ProjectionExpression": projection_expression,
|
|
203
|
+
"ExpressionAttributeNames": expression_attribute_names,
|
|
204
|
+
}
|
|
205
|
+
# only pass in args that aren't none
|
|
206
|
+
valid_kwargs = {k: v for k, v in kwargs.items() if v is not None}
|
|
207
|
+
|
|
208
|
+
if call_type == "resource":
|
|
209
|
+
table = self.dynamodb_resource.Table(table_name)
|
|
210
|
+
response = table.get_item(Key=key, **valid_kwargs)
|
|
211
|
+
elif call_type == "client":
|
|
212
|
+
response = self.dynamodb_client.get_item(
|
|
213
|
+
Key=key, TableName=table_name, **valid_kwargs
|
|
214
|
+
)
|
|
215
|
+
else:
|
|
216
|
+
raise ValueError(
|
|
217
|
+
f"Unknown call_type of {call_type}. Supported call_types [resource | client]"
|
|
218
|
+
)
|
|
219
|
+
except Exception as e: # pylint: disable=w0718
|
|
220
|
+
logger.exception(
|
|
221
|
+
{"source": f"{source}", "metric_filter": "get_item", "error": str(e)}
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
response = {"exception": str(e)}
|
|
225
|
+
if self.raise_on_error:
|
|
226
|
+
raise e
|
|
227
|
+
|
|
228
|
+
return response
|
|
229
|
+
|
|
230
|
+
def update_item(
|
|
231
|
+
self,
|
|
232
|
+
table_name: str,
|
|
233
|
+
key: dict,
|
|
234
|
+
update_expression: str,
|
|
235
|
+
expression_attribute_values: dict,
|
|
236
|
+
) -> dict:
|
|
237
|
+
"""_summary_
|
|
238
|
+
|
|
239
|
+
Args:
|
|
240
|
+
table_name (str): table name
|
|
241
|
+
key (dict): pk or pk and sk (composite key)
|
|
242
|
+
update_expression (str): update expression
|
|
243
|
+
expression_attribute_values (dict): expression attribute values
|
|
244
|
+
|
|
245
|
+
Returns:
|
|
246
|
+
dict: dynamodb response dictionary
|
|
247
|
+
"""
|
|
248
|
+
table = self.dynamodb_resource.Table(table_name)
|
|
249
|
+
response = table.update_item(
|
|
250
|
+
Key=key,
|
|
251
|
+
UpdateExpression=update_expression,
|
|
252
|
+
ExpressionAttributeValues=expression_attribute_values,
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
return response
|
|
256
|
+
|
|
257
|
+
def query(
|
|
258
|
+
self,
|
|
259
|
+
key: dict | Key | ConditionBase | ComparisonCondition,
|
|
260
|
+
*,
|
|
261
|
+
index_name: Optional[str] = None,
|
|
262
|
+
ascending: bool = False,
|
|
263
|
+
table_name: Optional[str] = None,
|
|
264
|
+
source: Optional[str] = None,
|
|
265
|
+
strongly_consistent: bool = False,
|
|
266
|
+
projection_expression: Optional[str] = None,
|
|
267
|
+
expression_attribute_names: Optional[dict] = None,
|
|
268
|
+
start_key: Optional[dict] = None,
|
|
269
|
+
limit: Optional[int] = None,
|
|
270
|
+
) -> dict:
|
|
271
|
+
"""
|
|
272
|
+
Run a query and return a list of items
|
|
273
|
+
Args:
|
|
274
|
+
key (Key): _description_
|
|
275
|
+
index_name (str, optional): _description_. Defaults to None.
|
|
276
|
+
ascending (bool, optional): _description_. Defaults to False.
|
|
277
|
+
table_name (str, optional): _description_. Defaults to None.
|
|
278
|
+
source (str, optional): The source of the query. Used for logging. Defaults to None.
|
|
279
|
+
|
|
280
|
+
Returns:
|
|
281
|
+
dict: dynamodb response dictionary
|
|
282
|
+
"""
|
|
283
|
+
|
|
284
|
+
logger.debug({"action": "query", "source": source})
|
|
285
|
+
|
|
286
|
+
kwargs: dict = {}
|
|
287
|
+
if index_name:
|
|
288
|
+
kwargs["IndexName"] = f"{index_name}"
|
|
289
|
+
kwargs["TableName"] = f"{table_name}"
|
|
290
|
+
kwargs["KeyConditionExpression"] = key
|
|
291
|
+
kwargs["ScanIndexForward"] = ascending
|
|
292
|
+
kwargs["ConsistentRead"] = strongly_consistent
|
|
293
|
+
|
|
294
|
+
if projection_expression:
|
|
295
|
+
kwargs["ProjectionExpression"] = projection_expression
|
|
296
|
+
|
|
297
|
+
if expression_attribute_names:
|
|
298
|
+
kwargs["ExpressionAttributeNames"] = expression_attribute_names
|
|
299
|
+
|
|
300
|
+
if start_key:
|
|
301
|
+
kwargs["ExclusiveStartKey"] = start_key
|
|
302
|
+
|
|
303
|
+
if limit:
|
|
304
|
+
kwargs["Limit"] = limit
|
|
305
|
+
|
|
306
|
+
if table_name is None:
|
|
307
|
+
raise ValueError("Query failed: table_name must be provided.")
|
|
308
|
+
|
|
309
|
+
table = self.dynamodb_resource.Table(table_name)
|
|
310
|
+
response = table.query(**kwargs)
|
|
311
|
+
|
|
312
|
+
return response
|
|
313
|
+
|
|
314
|
+
@overload
|
|
315
|
+
def delete(self, *, table_name: str, model: DynamoDBModelBase) -> dict:
|
|
316
|
+
pass
|
|
317
|
+
|
|
318
|
+
@overload
|
|
319
|
+
def delete(
|
|
320
|
+
self,
|
|
321
|
+
*,
|
|
322
|
+
table_name: str,
|
|
323
|
+
primary_key: dict,
|
|
324
|
+
) -> dict:
|
|
325
|
+
pass
|
|
326
|
+
|
|
327
|
+
@tracer.capture_method
|
|
328
|
+
def delete(
|
|
329
|
+
self,
|
|
330
|
+
*,
|
|
331
|
+
primary_key: Optional[dict] = None,
|
|
332
|
+
table_name: Optional[str] = None,
|
|
333
|
+
model: Optional[DynamoDBModelBase] = None,
|
|
334
|
+
):
|
|
335
|
+
"""deletes an item from the database"""
|
|
336
|
+
|
|
337
|
+
if model is not None:
|
|
338
|
+
if table_name is None:
|
|
339
|
+
raise ValueError("table_name must be provided when model is used.")
|
|
340
|
+
if primary_key is not None:
|
|
341
|
+
raise ValueError("primary_key cannot be provided when model is used.")
|
|
342
|
+
primary_key = model.indexes.primary.key()
|
|
343
|
+
|
|
344
|
+
response = None
|
|
345
|
+
|
|
346
|
+
if table_name is None or primary_key is None:
|
|
347
|
+
raise ValueError("table_name and primary_key must be provided.")
|
|
348
|
+
|
|
349
|
+
table = self.dynamodb_resource.Table(table_name)
|
|
350
|
+
response = table.delete_item(Key=primary_key)
|
|
351
|
+
|
|
352
|
+
return response
|
|
353
|
+
|
|
354
|
+
def list_tables(self) -> List[str]:
|
|
355
|
+
"""Get a list of tables from the current connection"""
|
|
356
|
+
tables = list(self.dynamodb_resource.tables.all())
|
|
357
|
+
table_list: List[str] = []
|
|
358
|
+
if len(tables) > 0:
|
|
359
|
+
for table in tables:
|
|
360
|
+
table_list.append(table.name)
|
|
361
|
+
|
|
362
|
+
return table_list
|
|
363
|
+
|
|
364
|
+
def query_by_criteria(
|
|
365
|
+
self,
|
|
366
|
+
*,
|
|
367
|
+
model: DynamoDBModelBase,
|
|
368
|
+
table_name: str,
|
|
369
|
+
index_name: str,
|
|
370
|
+
key: dict | Key | ConditionBase | ComparisonCondition,
|
|
371
|
+
start_key: Optional[dict] = None,
|
|
372
|
+
do_projections: bool = False,
|
|
373
|
+
ascending: bool = False,
|
|
374
|
+
strongly_consistent: bool = False,
|
|
375
|
+
) -> dict:
|
|
376
|
+
"""Helper function to list by criteria"""
|
|
377
|
+
|
|
378
|
+
projection_expression: str | None = None
|
|
379
|
+
expression_attribute_names: dict | None = None
|
|
380
|
+
|
|
381
|
+
if do_projections:
|
|
382
|
+
projection_expression = model.projection_expression
|
|
383
|
+
expression_attribute_names = model.projection_expression_attribute_names
|
|
384
|
+
|
|
385
|
+
response = self.query(
|
|
386
|
+
key=key,
|
|
387
|
+
index_name=index_name,
|
|
388
|
+
table_name=table_name,
|
|
389
|
+
start_key=start_key,
|
|
390
|
+
projection_expression=projection_expression,
|
|
391
|
+
expression_attribute_names=expression_attribute_names,
|
|
392
|
+
ascending=ascending,
|
|
393
|
+
strongly_consistent=strongly_consistent,
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
return response
|
|
397
|
+
|
|
398
|
+
def has_more_records(self, response: dict) -> bool:
|
|
399
|
+
"""
|
|
400
|
+
Check if there are more records to process.
|
|
401
|
+
This based on the existance of the LastEvaluatedKey in the response.
|
|
402
|
+
Parameters:
|
|
403
|
+
response (dict): dynamodb response dictionary
|
|
404
|
+
|
|
405
|
+
Returns:
|
|
406
|
+
bool: True if there are more records, False otherwise
|
|
407
|
+
"""
|
|
408
|
+
|
|
409
|
+
return "LastEvaluatedKey" in response
|
|
410
|
+
|
|
411
|
+
def last_key(self, response: dict) -> dict | None:
|
|
412
|
+
"""
|
|
413
|
+
Get the LastEvaluatedKey, which can be used to continue processing the results
|
|
414
|
+
Parameters:
|
|
415
|
+
response (dict): dynamodb response dictionary
|
|
416
|
+
|
|
417
|
+
Returns:
|
|
418
|
+
dict | None: The last key or None if not found
|
|
419
|
+
"""
|
|
420
|
+
|
|
421
|
+
return response.get("LastEvaluatedKey")
|
|
422
|
+
|
|
423
|
+
def items(self, response: dict) -> list:
|
|
424
|
+
"""
|
|
425
|
+
Get the Items from the dynamodb response
|
|
426
|
+
Parameters:
|
|
427
|
+
response (dict): dynamodb response dictionary
|
|
428
|
+
|
|
429
|
+
Returns:
|
|
430
|
+
list: A list or empty array/list if no items found
|
|
431
|
+
"""
|
|
432
|
+
|
|
433
|
+
return response.get("Items", [])
|
|
434
|
+
|
|
435
|
+
def item(self, response: dict) -> dict:
|
|
436
|
+
"""
|
|
437
|
+
Get the Item from the dynamodb response
|
|
438
|
+
Parameters:
|
|
439
|
+
response (dict): dynamodb response dictionary
|
|
440
|
+
|
|
441
|
+
Returns:
|
|
442
|
+
dict: A dictionary or empty dictionary if no item found
|
|
443
|
+
"""
|
|
444
|
+
|
|
445
|
+
return response.get("Item", {})
|