boto3-assist 0.1.13__py3-none-any.whl → 0.2.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.
@@ -45,6 +45,7 @@ class Boto3SessionManager:
45
45
  self.aws_access_key_id = aws_access_key_id
46
46
  self.aws_secret_access_key = aws_secret_access_key
47
47
  self.aws_session_token = aws_session_token
48
+
48
49
  self.__session: Any = None
49
50
  self.__client: Any = None
50
51
  self.__resource: Any = None
@@ -90,7 +91,7 @@ class Boto3SessionManager:
90
91
 
91
92
  def __get_aws_session(
92
93
  self, aws_profile: Optional[str] = None, aws_region: Optional[str] = None
93
- ) -> boto3.Session:
94
+ ) -> boto3.Session | None:
94
95
  """Get a boto3 session for AWS."""
95
96
  logger.debug({"profile": aws_profile, "region": aws_region})
96
97
  try:
@@ -153,7 +154,7 @@ class Boto3SessionManager:
153
154
  )
154
155
  return self.__resource
155
156
 
156
- def __create_boto3_session(self) -> boto3.Session:
157
+ def __create_boto3_session(self) -> boto3.Session | None:
157
158
  try:
158
159
  session = boto3.Session(
159
160
  profile_name=self.aws_profile,
@@ -0,0 +1,86 @@
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 Optional
8
+ from typing import TYPE_CHECKING
9
+
10
+ from aws_lambda_powertools import Logger
11
+ from boto3_assist.boto3session import Boto3SessionManager
12
+ from boto3_assist.environment_services.environment_variables import (
13
+ EnvironmentVariables,
14
+ )
15
+ from boto3_assist.cloudwatch.cloudwatch_connection_tracker import (
16
+ CloudWatchConnectionTracker,
17
+ )
18
+ from boto3_assist.connection import Connection
19
+
20
+ if TYPE_CHECKING:
21
+ from mypy_boto3_cloudwatch import CloudWatchClient, CloudWatchServiceResource
22
+ else:
23
+ CloudWatchClient = object
24
+ CloudWatchServiceResource = object
25
+
26
+
27
+ logger = Logger()
28
+ tracker: CloudWatchConnectionTracker = CloudWatchConnectionTracker()
29
+
30
+
31
+ class CloudWatchConnection(Connection):
32
+ """CW Environment"""
33
+
34
+ def __init__(
35
+ self,
36
+ *,
37
+ aws_profile: Optional[str] = None,
38
+ aws_region: Optional[str] = None,
39
+ aws_access_key_id: Optional[str] = None,
40
+ aws_secret_access_key: Optional[str] = None,
41
+ ) -> None:
42
+ super().__init__(
43
+ service_name="cloudwatch",
44
+ aws_profile=aws_profile,
45
+ aws_region=aws_region,
46
+ aws_access_key_id=aws_access_key_id,
47
+ aws_secret_access_key=aws_secret_access_key,
48
+ )
49
+
50
+ self.__client: CloudWatchClient | None = None
51
+ self.__resource: CloudWatchServiceResource | None = None
52
+
53
+ self.raise_on_error: bool = True
54
+
55
+ @property
56
+ def client(self) -> CloudWatchClient:
57
+ """CloudWatch Client Connection"""
58
+ if self.__client is None:
59
+ logger.info("Creating CloudWatch Client")
60
+ self.__client = self.session.client
61
+
62
+ if self.raise_on_error and self.__client is None:
63
+ raise RuntimeError("CloudWatch Client is not available")
64
+ return self.__client
65
+
66
+ @client.setter
67
+ def client(self, value: CloudWatchClient):
68
+ logger.info("Setting CloudWatch Client")
69
+ self.__client = value
70
+
71
+ @property
72
+ def resource(self) -> CloudWatchServiceResource:
73
+ """CloudWatch Resource Connection"""
74
+ if self.__resource is None:
75
+ logger.info("Creating CloudWatch Resource")
76
+ self.__resource = self.session.resource
77
+
78
+ if self.raise_on_error and self.__resource is None:
79
+ raise RuntimeError("CloudWatch Resource is not available")
80
+
81
+ return self.__resource
82
+
83
+ @resource.setter
84
+ def resource(self, value: CloudWatchServiceResource):
85
+ logger.info("Setting CloudWatch Resource")
86
+ self.__resource = value
@@ -0,0 +1,17 @@
1
+ """
2
+ Geek Cafe, LLC
3
+ Maintainers: Eric Wilson
4
+ MIT License. See Project Root for the license information.
5
+ """
6
+
7
+ from boto3_assist.connection_tracker import ConnectionTracker
8
+
9
+
10
+ class CloudWatchConnectionTracker(ConnectionTracker):
11
+ """
12
+ Tracks CloudWatch Connection Requests.
13
+ Useful in for performance tuning and debugging.
14
+ """
15
+
16
+ def __init__(self) -> None:
17
+ super().__init__("CloudWatch")
@@ -0,0 +1,62 @@
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 Optional
8
+ from typing import TYPE_CHECKING
9
+
10
+ from aws_lambda_powertools import Logger
11
+
12
+ from boto3_assist.cloudwatch.cloudwatch_connection_tracker import (
13
+ CloudWatchConnectionTracker,
14
+ )
15
+ from boto3_assist.connection import Connection
16
+
17
+ if TYPE_CHECKING:
18
+ from mypy_boto3_logs import CloudWatchLogsClient
19
+ else:
20
+ CloudWatchLogsClient = object
21
+
22
+
23
+ logger = Logger()
24
+ tracker: CloudWatchConnectionTracker = CloudWatchConnectionTracker()
25
+
26
+
27
+ class CloudWatchConnection(Connection):
28
+ """CW Logs Environment"""
29
+
30
+ def __init__(
31
+ self,
32
+ *,
33
+ aws_profile: Optional[str] = None,
34
+ aws_region: Optional[str] = None,
35
+ aws_access_key_id: Optional[str] = None,
36
+ aws_secret_access_key: Optional[str] = None,
37
+ ) -> None:
38
+ super().__init__(
39
+ service_name="logs",
40
+ aws_profile=aws_profile,
41
+ aws_region=aws_region,
42
+ aws_access_key_id=aws_access_key_id,
43
+ aws_secret_access_key=aws_secret_access_key,
44
+ )
45
+
46
+ self.__client: CloudWatchLogsClient | None = None
47
+
48
+ @property
49
+ def client(self) -> CloudWatchLogsClient:
50
+ """CloudWatch Client Connection"""
51
+ if self.__client is None:
52
+ logger.debug("Creating CloudWatch Client")
53
+ self.__client = self.session.client
54
+
55
+ if self.raise_on_error and self.__client is None:
56
+ raise RuntimeError("CloudWatch Client is not available")
57
+ return self.__client
58
+
59
+ @client.setter
60
+ def client(self, value: CloudWatchLogsClient):
61
+ logger.debug("Setting CloudWatch Client")
62
+ self.__client = value
@@ -0,0 +1,39 @@
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 Optional, List, Dict, Any
8
+ from boto3_assist.cloudwatch.cloudwatch_log_connection import CloudWatchConnection
9
+
10
+
11
+ class CloudWatchLogs(CloudWatchConnection):
12
+ def __init__(
13
+ self,
14
+ *,
15
+ aws_profile: Optional[str] = None,
16
+ aws_region: Optional[str] = None,
17
+ aws_access_key_id: Optional[str] = None,
18
+ aws_secret_access_key: Optional[str] = None,
19
+ ) -> None:
20
+ super().__init__(
21
+ aws_profile=aws_profile,
22
+ aws_region=aws_region,
23
+ aws_access_key_id=aws_access_key_id,
24
+ aws_secret_access_key=aws_secret_access_key,
25
+ )
26
+
27
+ def list_log_groups(self):
28
+ """Retrieve all log groups in the AWS account."""
29
+ log_groups: List[Dict[str, Any]] = []
30
+ paginator = self.client.get_paginator("describe_log_groups")
31
+ for page in paginator.paginate():
32
+ log_groups.extend(page["logGroups"]) # type: ignore[arg-type]
33
+ return log_groups
34
+
35
+
36
+ def main():
37
+ query: CloudWatchLogs = CloudWatchLogs()
38
+ result = query.list_log_groups()
39
+ print(result)
@@ -0,0 +1,191 @@
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 datetime import datetime, timedelta, UTC
9
+ from typing import Optional, Dict, Any, List
10
+ from boto3_assist.cloudwatch.cloudwatch_connection import CloudWatchConnection
11
+ from boto3_assist.cloudwatch.cloudwatch_logs import CloudWatchLogs
12
+
13
+
14
+ class CloudWatchQuery(CloudWatchConnection):
15
+ """Query Cloud Watch"""
16
+
17
+ def __init__(
18
+ self,
19
+ *,
20
+ aws_profile: Optional[str] = None,
21
+ aws_region: Optional[str] = None,
22
+ aws_access_key_id: Optional[str] = None,
23
+ aws_secret_access_key: Optional[str] = None,
24
+ ) -> None:
25
+ super().__init__(
26
+ aws_profile=aws_profile,
27
+ aws_region=aws_region,
28
+ aws_access_key_id=aws_access_key_id,
29
+ aws_secret_access_key=aws_secret_access_key,
30
+ )
31
+
32
+ self.__cw_logs: CloudWatchLogs | None = None
33
+
34
+ @property
35
+ def cw_logs(self) -> CloudWatchLogs:
36
+ """CloudWatch Logs Connection"""
37
+ if self.__cw_logs is None:
38
+ self.__cw_logs = CloudWatchLogs(
39
+ aws_profile=self.aws_profile,
40
+ aws_region=self.aws_region,
41
+ aws_access_key_id=self.aws_access_key_id,
42
+ aws_secret_access_key=self.aws_secret_access_key,
43
+ )
44
+ return self.__cw_logs
45
+
46
+ def get_log_group_size(
47
+ self, log_group_name: str, start_time: datetime, end_time: datetime
48
+ ) -> Dict[str, Any]:
49
+ """
50
+ Get the log group size for a given period of time
51
+ Args:
52
+ log_group_name (str): _description_
53
+ start_time (datetime): _description_
54
+ end_time (datetime): _description_
55
+
56
+ Returns:
57
+ _type_: _description_
58
+ """
59
+ response = self.client.get_metric_data(
60
+ MetricDataQueries=[
61
+ {
62
+ "Id": "storedBytes",
63
+ "MetricStat": {
64
+ "Metric": {
65
+ "Namespace": "AWS/Logs",
66
+ # "MetricName": "StoredBytes",
67
+ "MetricName": "IncomingBytes",
68
+ "Dimensions": [
69
+ {"Name": "LogGroupName", "Value": log_group_name}
70
+ ],
71
+ },
72
+ "Period": 86400, # Daily data
73
+ "Stat": "Sum",
74
+ },
75
+ "ReturnData": True,
76
+ },
77
+ ],
78
+ StartTime=start_time,
79
+ EndTime=end_time,
80
+ )
81
+
82
+ # Extract the total size in bytes for the period
83
+ size: float = 0.0
84
+ if response["MetricDataResults"]:
85
+ # Access the first MetricDataResult
86
+ metric_data_result = response["MetricDataResults"][0]
87
+ # Sum the values if they exist
88
+ size = (
89
+ sum(metric_data_result["Values"]) if metric_data_result["Values"] else 0
90
+ )
91
+ else:
92
+ size = 0
93
+
94
+ size_mb = size / (1024 * 1024)
95
+ size_gb = size_mb / 1024
96
+ resp: Dict[str, Any] = {
97
+ "LogGroupName": log_group_name,
98
+ "Size": {
99
+ "Bytes": size,
100
+ "MB": size_mb,
101
+ "GB": size_gb,
102
+ },
103
+ "StartDate": start_time.isoformat(),
104
+ "EndDate": end_time.isoformat(),
105
+ }
106
+
107
+ return resp
108
+
109
+ def get_log_sizes(
110
+ self,
111
+ start_date_time: datetime | None = None,
112
+ end_date_time: datetime | None = None,
113
+ days: int | None = 7,
114
+ top: int = 0,
115
+ ) -> List[Dict[str, Any]]:
116
+ """
117
+ Gets the log sizes for all log groups
118
+
119
+ Args:
120
+ start_date_time (datetime | None, optional): The Start Date. Defaults to None.
121
+ If None it's set to now in UTC time - the days field
122
+ end_date_time (datetime | None, optional): he Start Date. Defaults to None.
123
+ If None it's set to not in UTC time
124
+ days (int | None, optional): The days offset. Defaults to 7.
125
+ top (int, optional): If greater than zero it will return the top x after sorting
126
+ Defaults to 0.
127
+
128
+ Returns:
129
+ list: _description_
130
+ """
131
+ if not days:
132
+ days = 7
133
+ start_time = start_date_time or (datetime.now(UTC) - timedelta(days=days))
134
+ end_time = end_date_time or datetime.now(UTC)
135
+
136
+ # Step 1: List all log groups
137
+ log_groups = self.cw_logs.list_log_groups()
138
+ log_group_sizes = []
139
+
140
+ # Step 2: Get sizes for each log group
141
+ for log_group in log_groups:
142
+ log_group_name = log_group["logGroupName"]
143
+
144
+ size_info = self.get_log_group_size(log_group_name, start_time, end_time)
145
+ log_group_sizes.append(size_info)
146
+
147
+ # Step 3: Sort by size
148
+ # top_log_groups = sorted(log_group_sizes, key=lambda x: x[1], reverse=True)
149
+ top_log_groups = sorted(
150
+ log_group_sizes,
151
+ key=lambda x: x.get("Size", {}).get("Bytes", 0),
152
+ reverse=True,
153
+ )
154
+ if top and top > 0:
155
+ # find the top x if provided
156
+ top_log_groups = top_log_groups[:top]
157
+
158
+ return top_log_groups
159
+
160
+
161
+ def main():
162
+ log_group = os.environ.get("LOG_GROUP_QUERY_SAMPLE", "<enter-log-group-here>")
163
+ start = datetime.now() - timedelta(days=7) # Last 30 days
164
+ end = datetime.now()
165
+ cw_query: CloudWatchQuery = CloudWatchQuery()
166
+ result = cw_query.get_log_group_size(log_group, start, end)
167
+ print(result)
168
+
169
+ top = 25
170
+ days = 7
171
+ top_log_groups = cw_query.get_log_sizes(top=top, days=days)
172
+ print(f"Top {top} log groups by size for the last week:")
173
+
174
+ for top_log_group in top_log_groups:
175
+ log_group_name = top_log_group["LogGroupName"]
176
+ size_in_bytes = top_log_group.get("Size", {}).get("Bytes", 0)
177
+ size_in_megs = top_log_group.get("Size", {}).get("MB", 0)
178
+ size_in_gigs = top_log_group.get("Size", {}).get("GB", 0)
179
+ size: str = ""
180
+ if size_in_gigs > 1:
181
+ size = f"{size_in_gigs:.2f} GB"
182
+ elif size_in_megs > 1:
183
+ size = f"{size_in_megs:.2f} MB"
184
+ else:
185
+ size = f"{size_in_bytes} bytes"
186
+
187
+ print(f"{size}: {log_group_name}")
188
+
189
+
190
+ if __name__ == "__main__":
191
+ main()
@@ -0,0 +1,101 @@
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 Optional
8
+
9
+ from aws_lambda_powertools import Logger
10
+ from boto3_assist.boto3session import Boto3SessionManager
11
+ from boto3_assist.environment_services.environment_variables import (
12
+ EnvironmentVariables,
13
+ )
14
+ from boto3_assist.cloudwatch.cloudwatch_connection_tracker import (
15
+ CloudWatchConnectionTracker,
16
+ )
17
+
18
+
19
+ logger = Logger()
20
+ tracker: CloudWatchConnectionTracker = CloudWatchConnectionTracker()
21
+
22
+
23
+ class Connection:
24
+ """Boto 3 Connection"""
25
+
26
+ def __init__(
27
+ self,
28
+ *,
29
+ service_name: Optional[str] = None,
30
+ aws_profile: Optional[str] = None,
31
+ aws_region: Optional[str] = None,
32
+ aws_access_key_id: Optional[str] = None,
33
+ aws_secret_access_key: Optional[str] = None,
34
+ ) -> None:
35
+ self.aws_profile = aws_profile or EnvironmentVariables.AWS.profile()
36
+ self.aws_region = aws_region or EnvironmentVariables.AWS.region()
37
+
38
+ self.aws_access_key_id = (
39
+ aws_access_key_id or EnvironmentVariables.AWS.DynamoDB.aws_access_key_id()
40
+ )
41
+ self.aws_secret_access_key = (
42
+ aws_secret_access_key
43
+ or EnvironmentVariables.AWS.DynamoDB.aws_secret_access_key()
44
+ )
45
+ self.__session: Boto3SessionManager | None = None
46
+
47
+ self.__service_name: str | None = service_name
48
+ self.raise_on_error: bool = True
49
+
50
+ def setup(self, setup_source: Optional[str] = None) -> None:
51
+ """
52
+ Setup the environment. Automatically called via init.
53
+ You can run setup at anytime with new parameters.
54
+ Args: setup_source: Optional[str] = None
55
+ Defines the source of the setup. Useful for logging.
56
+ Returns: None
57
+ """
58
+
59
+ logger.debug(
60
+ {
61
+ "metric_filter": f"{self.service_name}_connection_setup",
62
+ "source": f"{self.service_name} Connection",
63
+ "aws_profile": self.aws_profile,
64
+ "aws_region": self.aws_region,
65
+ "setup_source": setup_source,
66
+ }
67
+ )
68
+
69
+ self.__session = Boto3SessionManager(
70
+ service_name=self.service_name,
71
+ aws_profile=self.aws_profile,
72
+ aws_region=self.aws_region,
73
+ aws_access_key_id=self.aws_access_key_id,
74
+ aws_secret_access_key=self.aws_secret_access_key,
75
+ )
76
+
77
+ tracker.increment_connection()
78
+
79
+ self.raise_on_error = EnvironmentVariables.AWS.DynamoDB.raise_on_error_setting()
80
+
81
+ @property
82
+ def service_name(self) -> str:
83
+ """Service Name"""
84
+ if self.__service_name is None:
85
+ raise RuntimeError("Service Name is not available")
86
+ return self.__service_name
87
+
88
+ @service_name.setter
89
+ def service_name(self, value: str):
90
+ logger.debug("Setting Service Name")
91
+ self.__service_name = value
92
+
93
+ @property
94
+ def session(self) -> Boto3SessionManager:
95
+ """Session"""
96
+ if self.__session is None:
97
+ self.setup(setup_source="session init")
98
+
99
+ if self.__session is None:
100
+ raise RuntimeError("Session is not available")
101
+ return self.__session
@@ -16,7 +16,7 @@ class ConnectionTracker:
16
16
 
17
17
  def __init__(self, service_name: str) -> None:
18
18
  self.__stack_trace_env_var: str = "BOTO3_ASSIST_CONNECTION_STACK_TRACE"
19
- self.__connection_couter: int = 0
19
+ self.__connection_counter: int = 0
20
20
  self.__service_name: str = service_name
21
21
  self.__issue_stack_trace: bool | None = None
22
22
 
@@ -31,7 +31,7 @@ class ConnectionTracker:
31
31
 
32
32
  def increment_connection(self) -> None:
33
33
  """Increments the connection counter"""
34
- self.__connection_couter += 1
34
+ self.__connection_counter += 1
35
35
 
36
36
  if self.connection_count > 1:
37
37
  service_message = ""
@@ -53,27 +53,27 @@ class ConnectionTracker:
53
53
 
54
54
  self.__log_warning(
55
55
  f"{service_message}"
56
- f"Your dynamodb connection count is {self.connection_count}. "
56
+ f"Your boto3 connection count is {self.connection_count}. "
57
57
  "Under most circumstances you should be able to use the same connection "
58
58
  "vs. creating a new one. Connections are expensive in terms of time / latency. "
59
59
  "If you are seeing perforance issues, check how and where you are creating your "
60
- "connections. You should be able to pass the .db connection to your other objects "
61
- "and reuse your dynamodb boto connections."
60
+ "connections. You should be able to pass the connection to your other objects "
61
+ "and reuse your boto3 connections."
62
62
  f"{stack_trace_message}"
63
63
  )
64
64
 
65
65
  def decrement_connection(self) -> None:
66
66
  """Decrements the connection counter"""
67
- self.__connection_couter -= 1
67
+ self.__connection_counter -= 1
68
68
 
69
69
  @property
70
70
  def connection_count(self) -> int:
71
71
  """Returns the current connection count"""
72
- return self.__connection_couter
72
+ return self.__connection_counter
73
73
 
74
74
  def reset(self) -> None:
75
75
  """Resets the connection counter"""
76
- self.__connection_couter = 0
76
+ self.__connection_counter = 0
77
77
 
78
78
  def __log_warning(self, message: str) -> None:
79
79
  """Logs a warning message"""
@@ -5,7 +5,7 @@ MIT License. See Project Root for the license information.
5
5
  """
6
6
 
7
7
  import os
8
- from typing import List, Optional, overload
8
+ from typing import List, Optional, overload, Dict, Any
9
9
 
10
10
  from aws_lambda_powertools import Tracer, Logger
11
11
  from boto3.dynamodb.conditions import (
@@ -77,7 +77,7 @@ class DynamoDB(DynamoDBConnection):
77
77
  dict: The Response from DynamoDB's put_item actions.
78
78
  It does not return the saved object, only the response.
79
79
  """
80
- response = None
80
+ response: Dict[str, Any] = {}
81
81
 
82
82
  try:
83
83
  if not isinstance(item, dict):
@@ -93,13 +93,13 @@ class DynamoDB(DynamoDBConnection):
93
93
 
94
94
  if isinstance(item, dict) and isinstance(next(iter(item.values())), dict):
95
95
  # Use boto3.client syntax
96
- response = self.dynamodb_client.put_item(
97
- TableName=table_name, Item=item
96
+ response = dict(
97
+ self.dynamodb_client.put_item(TableName=table_name, Item=item)
98
98
  )
99
99
  else:
100
100
  # Use boto3.resource syntax
101
101
  table = self.dynamodb_resource.Table(table_name)
102
- response = table.put_item(Item=item)
102
+ response = dict(table.put_item(Item=item)) # type: ignore[arg-type]
103
103
 
104
104
  except Exception as e: # pylint: disable=w0718
105
105
  logger.exception(
@@ -117,7 +117,7 @@ class DynamoDB(DynamoDBConnection):
117
117
 
118
118
  if self.log_dynamodb_item_size:
119
119
  size_bytes: int = StringUtility.get_size_in_bytes(item)
120
- size_kb: int = StringUtility.get_size_in_kb(item)
120
+ size_kb: float = StringUtility.get_size_in_kb(item)
121
121
  logger.info({"item_size": {"bytes": size_bytes, "kb": f"{size_kb:.2f}kb"}})
122
122
 
123
123
  if size_kb > 390:
@@ -173,7 +173,7 @@ class DynamoDB(DynamoDBConnection):
173
173
  expression_attribute_names: Optional[dict] = None,
174
174
  source: Optional[str] = None,
175
175
  call_type: str = "resource",
176
- ) -> dict:
176
+ ) -> Dict[str, Any]:
177
177
  """
178
178
  Description:
179
179
  generic get_item dynamoDb call
@@ -186,12 +186,15 @@ class DynamoDB(DynamoDBConnection):
186
186
  if table_name is None:
187
187
  raise ValueError("table_name must be provided when model is used.")
188
188
  if key is not None:
189
- raise ValueError("key cannot be provided when model is used.")
189
+ raise ValueError(
190
+ "key cannot be provided when model is used. "
191
+ "When using the model, we'll automatically use the key defined."
192
+ )
190
193
  key = model.indexes.primary.key()
191
194
  if do_projections:
192
195
  projection_expression = model.projection_expression
193
196
  expression_attribute_names = model.projection_expression_attribute_names
194
- elif key is None or table_name is None:
197
+ elif key is None and model is None:
195
198
  raise ValueError("Either 'key' or 'model' must be provided.")
196
199
 
197
200
  response = None
@@ -205,12 +208,18 @@ class DynamoDB(DynamoDBConnection):
205
208
  # only pass in args that aren't none
206
209
  valid_kwargs = {k: v for k, v in kwargs.items() if v is not None}
207
210
 
211
+ if table_name is None:
212
+ raise ValueError("table_name must be provided.")
208
213
  if call_type == "resource":
209
214
  table = self.dynamodb_resource.Table(table_name)
210
- response = table.get_item(Key=key, **valid_kwargs)
215
+ response = dict(table.get_item(Key=key, **valid_kwargs)) # type: ignore[arg-type]
211
216
  elif call_type == "client":
212
- response = self.dynamodb_client.get_item(
213
- Key=key, TableName=table_name, **valid_kwargs
217
+ response = dict(
218
+ self.dynamodb_client.get_item(
219
+ Key=key,
220
+ TableName=table_name,
221
+ **valid_kwargs, # type: ignore[arg-type]
222
+ )
214
223
  )
215
224
  else:
216
225
  raise ValueError(
@@ -246,10 +255,12 @@ class DynamoDB(DynamoDBConnection):
246
255
  dict: dynamodb response dictionary
247
256
  """
248
257
  table = self.dynamodb_resource.Table(table_name)
249
- response = table.update_item(
250
- Key=key,
251
- UpdateExpression=update_expression,
252
- ExpressionAttributeValues=expression_attribute_values,
258
+ response = dict(
259
+ table.update_item(
260
+ Key=key,
261
+ UpdateExpression=update_expression,
262
+ ExpressionAttributeValues=expression_attribute_values,
263
+ )
253
264
  )
254
265
 
255
266
  return response
@@ -307,7 +318,7 @@ class DynamoDB(DynamoDBConnection):
307
318
  raise ValueError("Query failed: table_name must be provided.")
308
319
 
309
320
  table = self.dynamodb_resource.Table(table_name)
310
- response = table.query(**kwargs)
321
+ response = dict(table.query(**kwargs))
311
322
 
312
323
  return response
313
324
 
@@ -92,6 +92,9 @@ class DynamoDBConnection:
92
92
  """Session"""
93
93
  if self.__session is None:
94
94
  self.setup(setup_source="session init")
95
+
96
+ if self.__session is None:
97
+ raise RuntimeError("Session is not available")
95
98
  return self.__session
96
99
 
97
100
  @property
@@ -34,6 +34,29 @@ class DynamoDBIndexes:
34
34
  """Add a GSI/LSI index"""
35
35
  if index.name is None:
36
36
  raise ValueError("Index name cannot be None")
37
+
38
+ # if the index already exists, raise an exception
39
+ if index.name in self.__indexes:
40
+ raise ValueError(f"Index {index.name} already exists")
41
+ if index.name == DynamoDBIndexes.PRIMARY_INDEX:
42
+ raise ValueError(f"Index {index.name} is reserved for the primary index")
43
+ if index.partition_key is None:
44
+ raise ValueError("Index must have a partition key")
45
+
46
+ # check if the index.partition_key.attribute_name is already in the index
47
+ for _, v in self.__indexes.items():
48
+ if v.partition_key.attribute_name == index.partition_key.attribute_name:
49
+ raise ValueError(
50
+ f"Index {index.name} already exists with partition key {index.partition_key.attribute_name}"
51
+ )
52
+ # check if the gsi1.sort_key.attribute_name exists
53
+ if index.sort_key is not None:
54
+ for _, v in self.__indexes.items():
55
+ if v.sort_key.attribute_name == index.sort_key.attribute_name:
56
+ raise ValueError(
57
+ f"Index {index.name} already exists with sort key {index.sort_key.attribute_name}"
58
+ )
59
+
37
60
  self.__indexes[index.name] = index
38
61
 
39
62
  def get(self, index_name: str) -> DynamoDBIndex:
@@ -7,6 +7,10 @@ from boto3_assist.dynamodb.dynamodb_model_base import DynamoDBModelBase
7
7
  class IDynamoDBService(ABC):
8
8
  """DynamoDB Service Interface"""
9
9
 
10
+ def __init__(self, db: DynamoDB) -> None:
11
+ self.db = db
12
+ super().__init__()
13
+
10
14
  @property
11
15
  @abstractmethod
12
16
  def db(self) -> DynamoDB:
@@ -59,4 +59,6 @@ class DynamoDBKey:
59
59
  break
60
60
  else:
61
61
  parts.append(f"{prefix}{value}")
62
- return "#".join(parts)
62
+ key_str = "#".join(parts)
63
+
64
+ return key_str
@@ -9,7 +9,7 @@ import datetime as dt
9
9
  import decimal
10
10
  import inspect
11
11
  import uuid
12
-
12
+ import base64
13
13
  from typing import TypeVar, List
14
14
  from boto3.dynamodb.types import TypeSerializer
15
15
  from boto3_assist.utilities.serialization_utility import Serialization
@@ -64,16 +64,28 @@ class DynamoDBModelBase:
64
64
  @exclude_from_serialization
65
65
  def projection_expression(self) -> str | None:
66
66
  """Gets the projection expression"""
67
+ prop_list: List[str] = []
67
68
  if self.__projection_expression is None and self.auto_generate_projections:
68
69
  props = self.to_dictionary()
69
70
  # turn props to a list[str]
70
71
  prop_list = list(props.keys())
72
+ else:
73
+ if self.__projection_expression:
74
+ prop_list = self.__projection_expression.split(",")
75
+ prop_list = [p.strip() for p in prop_list]
71
76
 
72
- transformed_list = self.__reserved_words.tranform_projections(prop_list)
73
- self.projection_expression = ",".join(transformed_list)
77
+ if len(prop_list) == 0:
78
+ return None
79
+
80
+ transformed_list = self.__reserved_words.tranform_projections(prop_list)
81
+ self.projection_expression = ",".join(transformed_list)
74
82
 
75
83
  return self.__projection_expression
76
84
 
85
+ @projection_expression.setter
86
+ def projection_expression(self, value: str | None):
87
+ self.__projection_expression = value
88
+
77
89
  @property
78
90
  @exclude_from_serialization
79
91
  def auto_generate_projections(self) -> bool:
@@ -84,10 +96,6 @@ class DynamoDBModelBase:
84
96
  def auto_generate_projections(self, value: bool):
85
97
  self.__auto_generate_projections = value
86
98
 
87
- @projection_expression.setter
88
- def projection_expression(self, value: str | None):
89
- self.__projection_expression = value
90
-
91
99
  @property
92
100
  @exclude_from_serialization
93
101
  def projection_expression_attribute_names(self) -> dict | None:
@@ -105,6 +113,12 @@ class DynamoDBModelBase:
105
113
  self.projection_expression_attribute_names = (
106
114
  self.__reserved_words.transform_attributes(prop_list)
107
115
  )
116
+ else:
117
+ if self.projection_expression:
118
+ expression_list = self.projection_expression.replace("#", "").split(",")
119
+ self.projection_expression_attribute_names = (
120
+ self.__reserved_words.transform_attributes(expression_list)
121
+ )
108
122
 
109
123
  return self.__projection_expression_attribute_names
110
124
 
@@ -325,6 +339,8 @@ class DynamoDBSerializer:
325
339
 
326
340
  # Add instance variables
327
341
  for attr, value in instance.__dict__.items():
342
+ if str(attr) == "T":
343
+ continue
328
344
  # don't get the private properties
329
345
  if not str(attr).startswith("_"):
330
346
  if value is not None or include_none:
@@ -346,6 +362,10 @@ class DynamoDBSerializer:
346
362
  if exclude:
347
363
  continue
348
364
 
365
+ # Skip TypeVar T or instances of DynamoDBModelBase
366
+ if str(name) == "T":
367
+ continue
368
+
349
369
  # don't get the private properties
350
370
  if not str(name).startswith("_"):
351
371
  value = getattr(instance, name)
@@ -3,7 +3,10 @@ from typing import List
3
3
 
4
4
 
5
5
  class DynamoDBReservedWords:
6
- """Reserved Word"""
6
+ """
7
+ Reserved Words used by DynamoDB that can cause issues,
8
+ so they will need to be transformed under certain conditions, such as when doing projections
9
+ """
7
10
 
8
11
  def __init__(self) -> None:
9
12
  self.__list: List[str] = self.__read_list()
@@ -31,13 +34,13 @@ class DynamoDBReservedWords:
31
34
  if isinstance(projections, str):
32
35
  projections = projections.split(",")
33
36
 
34
- # any project that exists add a # infront of it
37
+ # any projection that exists add a # infront of it
35
38
  projections = ["#" + p if self.is_reserved_word(p) else p for p in projections]
36
39
  return projections
37
40
 
38
- def transform_attributes(self, projections: List[str] | str) -> dict:
41
+ def transform_attributes(self, projections: List[str] | str) -> dict | None:
39
42
  """Transforms a dict of attributes to remove reserved words"""
40
- transformed_attributes: dict | None = {}
43
+ transformed_attributes: dict = {}
41
44
  if isinstance(projections, str):
42
45
  projections = projections.split(",")
43
46
  for item in projections:
@@ -1,4 +1,3 @@
1
-
2
1
  ABORT
3
2
  ABSOLUTE
4
3
  ACTION
@@ -84,6 +84,9 @@ class EC2Connection:
84
84
  """Session"""
85
85
  if self.__session is None:
86
86
  self.setup(setup_source="session init")
87
+
88
+ if self.__session is None:
89
+ raise RuntimeError("Session is not available")
87
90
  return self.__session
88
91
 
89
92
  @property
@@ -1,22 +1,42 @@
1
+ """
2
+ Geek Cafe, LLC
3
+ Maintainers: Eric Wilson
4
+ MIT License. See Project Root for the license information.
5
+ """
6
+
1
7
  import os
2
- from typing import IO, Optional, Union
8
+
9
+ from typing import List, Union, Optional, IO
10
+ from pathlib import Path
3
11
  from dotenv import load_dotenv
12
+ from aws_lambda_powertools import Logger
13
+
14
+
15
+ logger = Logger(__name__)
16
+
17
+ DEBUGGING = os.getenv("DEBUGGING", "false").lower() == "true"
4
18
 
5
19
  StrPath = Union[str, "os.PathLike[str]"]
6
20
 
7
21
 
8
22
  class EnvironmentLoader:
23
+ """Environment Loader"""
24
+
9
25
  def __init__(self) -> None:
10
26
  pass
11
27
 
12
28
  def load_environment_file(
13
29
  self,
30
+ *,
31
+ starting_path: Optional[str] = None,
32
+ file_name: Optional[str] = None,
14
33
  path: Optional[StrPath] = None,
15
34
  stream: Optional[IO[str]] = None,
16
35
  verbose: bool = False,
17
36
  override: bool = True,
18
37
  interpolate: bool = True,
19
38
  encoding: Optional[str] = "utf-8",
39
+ raise_error_if_not_found: bool = False,
20
40
  ) -> bool:
21
41
  """
22
42
  Loads an environment file into memory. This simply passes off to load_dotenv in dotenv.
@@ -37,11 +57,55 @@ class EnvironmentLoader:
37
57
  If both `dotenv_path` and `stream` are `None`, `find_dotenv()` is used to find the
38
58
  .env file.
39
59
  """
40
- return load_dotenv(
41
- dotenv_path=path,
60
+
61
+ if not starting_path:
62
+ starting_path = __file__
63
+
64
+ if file_name is None:
65
+ file_name = ".env"
66
+
67
+ new_path: str | StrPath | None = path or self.find_file(
68
+ starting_path=starting_path,
69
+ file_name=file_name,
70
+ raise_error_if_not_found=raise_error_if_not_found,
71
+ )
72
+
73
+ loaded = load_dotenv(
74
+ dotenv_path=new_path,
42
75
  stream=stream,
43
76
  verbose=verbose,
44
77
  override=override,
45
78
  interpolate=interpolate,
46
79
  encoding=encoding,
47
80
  )
81
+
82
+ if DEBUGGING:
83
+ env_vars = os.environ
84
+ logger.debug(f"Loaded environment file: {path}")
85
+ print(env_vars)
86
+
87
+ return loaded
88
+
89
+ def find_file(
90
+ self, starting_path: str, file_name: str, raise_error_if_not_found: bool = True
91
+ ) -> str | None:
92
+ """Searches the project directory structor for a file"""
93
+ parents = 10
94
+ starting_path = starting_path or __file__
95
+
96
+ paths: List[str] = []
97
+ for parent in range(parents):
98
+ path = Path(starting_path).parents[parent].absolute()
99
+ print(f"searching: {path}")
100
+ tmp = os.path.join(path, file_name)
101
+ paths.append(tmp)
102
+ if os.path.exists(tmp):
103
+ return tmp
104
+
105
+ if raise_error_if_not_found:
106
+ searched_paths = "\n".join(paths)
107
+ raise RuntimeError(
108
+ f"Failed to locate environment file: {file_name} in: \n {searched_paths}"
109
+ )
110
+
111
+ return None
@@ -27,6 +27,8 @@ class EnvironmentVariables:
27
27
  gets the aws region from an environment var
28
28
  """
29
29
  value = os.getenv("AWS_REGION")
30
+ if not value:
31
+ value = None
30
32
  return value
31
33
 
32
34
  @staticmethod
@@ -36,6 +38,8 @@ class EnvironmentVariables:
36
38
  This should only be set with temporty creds and only for development purposes
37
39
  """
38
40
  value = os.getenv("AWS_PROFILE")
41
+ if not value:
42
+ value = None
39
43
  return value
40
44
 
41
45
  @staticmethod
boto3_assist/version.py CHANGED
@@ -1 +1 @@
1
- __version__ = '0.1.13'
1
+ __version__ = '0.2.0'
@@ -1,10 +1,8 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: boto3_assist
3
- Version: 0.1.13
3
+ Version: 0.2.0
4
4
  Summary: Additional boto3 wrappers to make your life a little easier
5
5
  Author-email: Eric Wilson <boto3-assist@geekcafe.com>
6
- License-File: LICENSE-EXPLAINED.txt
7
- License-File: LICENSE.txt
8
6
  Classifier: License :: Other/Proprietary License
9
7
  Classifier: Operating System :: OS Independent
10
8
  Classifier: Programming Language :: Python :: 3
@@ -0,0 +1,38 @@
1
+ boto3_assist/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ boto3_assist/boto3session.py,sha256=NWhWtYR3143thEbTpoklkwdz77-fTMs-QsoQdqfRm6E,6430
3
+ boto3_assist/connection.py,sha256=EJlGueLIYqMSKs7aQlThK1S0Zkb8dOYBWch1iRZdgUI,3233
4
+ boto3_assist/connection_tracker.py,sha256=_s1t7h2DOi3CCIHIr_HIKyGjku65WR-HJ_v8vJHDvO0,2977
5
+ boto3_assist/version.py,sha256=FVHPBGkfhbQDi_z3v0PiKJrXXqXOx0vGW_1VaqNJi7U,22
6
+ boto3_assist/cloudwatch/cloudwatch_connection.py,sha256=kiEmn3PVL9hbTrLehb2Xak9pEY8KygKoRO6YxdpYcbk,2634
7
+ boto3_assist/cloudwatch/cloudwatch_connection_tracker.py,sha256=mzRtO1uHrcfJNh1XrGEiXdTqxwEP8d1RqJkraMNkgK0,410
8
+ boto3_assist/cloudwatch/cloudwatch_log_connection.py,sha256=qQMZHjUJ6gA8wU9utjQhOURXNSPH2RjxSoAy83bvoCs,1737
9
+ boto3_assist/cloudwatch/cloudwatch_logs.py,sha256=VtI0OnFjX1l4RYVvA8tvveGkPwAogtrplnflZ4dQSNM,1204
10
+ boto3_assist/cloudwatch/cloudwatch_query.py,sha256=uNhSb1Gfp99v8BaHmCnCKs63j4MMU4WveqBavCJyhGY,6409
11
+ boto3_assist/dynamodb/dynamodb.py,sha256=WZ0tmY-YAfY4aAvc1HNqizcLl_qeuBUj54K3BdtsUpQ,15127
12
+ boto3_assist/dynamodb/dynamodb_connection.py,sha256=Gq5B830hAWAjWrvx0cGm0WpPaIr5usceSsykR-HU0tc,4513
13
+ boto3_assist/dynamodb/dynamodb_connection_tracker.py,sha256=0BWHRfi5_vjkJLuCSX6sYwvA6wc7BSYCQnGrzbhfyKA,404
14
+ boto3_assist/dynamodb/dynamodb_helpers.py,sha256=ajpTJ5bJOm9PDgE2Zx9p2zkTRFV4xswqJRS81SOTn1s,12198
15
+ boto3_assist/dynamodb/dynamodb_importer.py,sha256=nCKsyRQeMqDSf0Q5mQ_X_oVIg4PRnu0hcUzZnBli610,3471
16
+ boto3_assist/dynamodb/dynamodb_index.py,sha256=IihgaUVeSUJOFvgDx-xOQRC0cxQblfie7CM_Yj_K9o4,7498
17
+ boto3_assist/dynamodb/dynamodb_iservice.py,sha256=O9Aj0PFEvcuk2vhARifWTFnUwcQW5EXzwZS478Hm-N0,796
18
+ boto3_assist/dynamodb/dynamodb_key.py,sha256=YD7o1EUlwVBQ55p9YCTKqAUU_np4nqtLIHnmp-BeolM,1803
19
+ boto3_assist/dynamodb/dynamodb_model_base.py,sha256=D74Wjh3YTewkGD0_oZ_9gGsjwgUMhz-oTSRIRdpx4VE,14273
20
+ boto3_assist/dynamodb/dynamodb_model_base_interfaces.py,sha256=yT4zDRI8vP15WVOHnCvY3FsEy_QSIta5-bnUby70Xow,747
21
+ boto3_assist/dynamodb/dynamodb_reindexer.py,sha256=bCj6KIU0fQOgjkkiq9yF51PFZZr4Y9Lu3-hPlmsPG0Y,6164
22
+ boto3_assist/dynamodb/dynamodb_reserved_words.py,sha256=p0irNBSqGe4rd2FwWQqbRJWrNr4svdbWiyIXmz9lj4c,1937
23
+ boto3_assist/dynamodb/dynamodb_reserved_words.txt,sha256=rvctS63Cv3i9SHmPq2Unmj6RZyQ-OMqxUXsNhtbg1is,4136
24
+ boto3_assist/dynamodb/readme.md,sha256=wNMzdRwk0qRV0kE88UUYnJos3pEK0HNjEIVkq2PATf8,1490
25
+ boto3_assist/dynamodb/troubleshooting.md,sha256=uGpBaBUt_MyzjzwFOLOe0udTgcvaOpiTFxfj7ilLNkM,136
26
+ boto3_assist/ec2/ec2_connection.py,sha256=VT_hnu_behs9mf1Prggo5U9c9KAwbnLBTSZkmu4gp30,3277
27
+ boto3_assist/environment_services/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
28
+ boto3_assist/environment_services/environment_loader.py,sha256=jvG1xwMtgkZqu70NbjG1b1IefKiWgaidjZZoqsSfULk,3370
29
+ boto3_assist/environment_services/environment_variables.py,sha256=4ccBKdPt6O7hcRT3zBHd8vqu8yQU8udmoD5RLAT3iMs,6801
30
+ boto3_assist/utilities/datetime_utility.py,sha256=TbqGQkJDTahqvaZAIV550nhYnW1Bsq0Hdu3Go6P4RRs,10282
31
+ boto3_assist/utilities/logging_utility.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
32
+ boto3_assist/utilities/serialization_utility.py,sha256=s_QQRIhtwIE7xN5nU13mNk2wtWyErBX_Sg7n0gbHj-M,4308
33
+ boto3_assist/utilities/string_utility.py,sha256=w8l063UT3GE48tuJopETyZrjG4CgAzWkyDWMAYMg5Og,7432
34
+ boto3_assist-0.2.0.dist-info/METADATA,sha256=BCVBJGfQPqRIO-FvqKhyPOEaJZcJ1U0AlcRs9xFeb68,1742
35
+ boto3_assist-0.2.0.dist-info/WHEEL,sha256=C2FUgwZgiLbznR-k0b_5k3Ai_1aASOXDss3lzCUsUug,87
36
+ boto3_assist-0.2.0.dist-info/licenses/LICENSE-EXPLAINED.txt,sha256=WFREvTpfTjPjDHpOLADxJpCKpIla3Ht87RUUGii4ODU,606
37
+ boto3_assist-0.2.0.dist-info/licenses/LICENSE.txt,sha256=PXDhFWS5L5aOTkVhNvoitHKbAkgxqMI2uUPQyrnXGiI,1105
38
+ boto3_assist-0.2.0.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.25.0
2
+ Generator: hatchling 1.26.3
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
@@ -1,32 +0,0 @@
1
- boto3_assist/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- boto3_assist/boto3session.py,sha256=J20E2lkqJOaey4Ohi-xRe2xABj7QsA8DrggzK29-5es,6415
3
- boto3_assist/connection_tracker.py,sha256=ggCp2gQ7Y-2fHSGFk8QCHJz-Rq1yVNO4aRYgrYTP178,2987
4
- boto3_assist/version.py,sha256=Vx7xxEndWksf5h9xRevQfOixX-CU35SwBXGCh74SioU,23
5
- boto3_assist/dynamodb/dynamodb.py,sha256=vV0HvFCESnVK6BcAfcrWsMsNxIIsctvDX_3xdzOVCT0,14623
6
- boto3_assist/dynamodb/dynamodb_connection.py,sha256=JMCmWOsMzy45rikGl3Z2xqlG2vUTEKSYqi6dpsfJ750,4418
7
- boto3_assist/dynamodb/dynamodb_connection_tracker.py,sha256=0BWHRfi5_vjkJLuCSX6sYwvA6wc7BSYCQnGrzbhfyKA,404
8
- boto3_assist/dynamodb/dynamodb_helpers.py,sha256=ajpTJ5bJOm9PDgE2Zx9p2zkTRFV4xswqJRS81SOTn1s,12198
9
- boto3_assist/dynamodb/dynamodb_importer.py,sha256=nCKsyRQeMqDSf0Q5mQ_X_oVIg4PRnu0hcUzZnBli610,3471
10
- boto3_assist/dynamodb/dynamodb_index.py,sha256=LRQgSci222s-pU-JXgnaAoOa71ABX9h3uJPeCVPl1GE,6315
11
- boto3_assist/dynamodb/dynamodb_iservice.py,sha256=2AuaKxt7DUZbB-GpBBtPtPMpAlgZkumkAldm8vy7-sg,701
12
- boto3_assist/dynamodb/dynamodb_key.py,sha256=X3I3gUPx2T858vjRDi9SN8qn8ez5UJUo0vZiKBeeUWg,1776
13
- boto3_assist/dynamodb/dynamodb_model_base.py,sha256=x4jrH10-uooeQfdKhWQ6htXhA95bvCBET8jKrM7TQD0,13492
14
- boto3_assist/dynamodb/dynamodb_model_base_interfaces.py,sha256=yT4zDRI8vP15WVOHnCvY3FsEy_QSIta5-bnUby70Xow,747
15
- boto3_assist/dynamodb/dynamodb_reindexer.py,sha256=bCj6KIU0fQOgjkkiq9yF51PFZZr4Y9Lu3-hPlmsPG0Y,6164
16
- boto3_assist/dynamodb/dynamodb_reserved_words.py,sha256=iiud7ijpER5MgKHIP_NIgkDmHVy_57VRQzNHRipv0as,1786
17
- boto3_assist/dynamodb/dynamodb_reserved_words.txt,sha256=CanugFPh7rpgJ_jH9k4t8ANycrZJWTjT09sWiW4LC68,4137
18
- boto3_assist/dynamodb/readme.md,sha256=wNMzdRwk0qRV0kE88UUYnJos3pEK0HNjEIVkq2PATf8,1490
19
- boto3_assist/dynamodb/troubleshooting.md,sha256=uGpBaBUt_MyzjzwFOLOe0udTgcvaOpiTFxfj7ilLNkM,136
20
- boto3_assist/ec2/ec2_connection.py,sha256=KN1AivKY6yYpx_AjmaA3aQUZtUwKIvWk4FO8OPq1aTY,3182
21
- boto3_assist/environment_services/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
22
- boto3_assist/environment_services/environment_loader.py,sha256=aW-PvlNi8EuavKPXDAi5txLbJFVRkBpkFIgmnR0OWDw,1581
23
- boto3_assist/environment_services/environment_variables.py,sha256=29ujkcFdgVgDgTK_pcVkpcFofEOYTbMXYyRim5vLj-k,6691
24
- boto3_assist/utilities/datetime_utility.py,sha256=TbqGQkJDTahqvaZAIV550nhYnW1Bsq0Hdu3Go6P4RRs,10282
25
- boto3_assist/utilities/logging_utility.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
26
- boto3_assist/utilities/serialization_utility.py,sha256=s_QQRIhtwIE7xN5nU13mNk2wtWyErBX_Sg7n0gbHj-M,4308
27
- boto3_assist/utilities/string_utility.py,sha256=w8l063UT3GE48tuJopETyZrjG4CgAzWkyDWMAYMg5Og,7432
28
- boto3_assist-0.1.13.dist-info/METADATA,sha256=1Cg2vIjtiWjlTFmAqEhIhKUSrW3fUtW1DztU6FGRZp4,1805
29
- boto3_assist-0.1.13.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
30
- boto3_assist-0.1.13.dist-info/licenses/LICENSE-EXPLAINED.txt,sha256=WFREvTpfTjPjDHpOLADxJpCKpIla3Ht87RUUGii4ODU,606
31
- boto3_assist-0.1.13.dist-info/licenses/LICENSE.txt,sha256=PXDhFWS5L5aOTkVhNvoitHKbAkgxqMI2uUPQyrnXGiI,1105
32
- boto3_assist-0.1.13.dist-info/RECORD,,