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.
- boto3_assist/boto3session.py +3 -2
- boto3_assist/cloudwatch/cloudwatch_connection.py +86 -0
- boto3_assist/cloudwatch/cloudwatch_connection_tracker.py +17 -0
- boto3_assist/cloudwatch/cloudwatch_log_connection.py +62 -0
- boto3_assist/cloudwatch/cloudwatch_logs.py +39 -0
- boto3_assist/cloudwatch/cloudwatch_query.py +191 -0
- boto3_assist/connection.py +101 -0
- boto3_assist/connection_tracker.py +8 -8
- boto3_assist/dynamodb/dynamodb.py +28 -17
- boto3_assist/dynamodb/dynamodb_connection.py +3 -0
- boto3_assist/dynamodb/dynamodb_index.py +23 -0
- boto3_assist/dynamodb/dynamodb_iservice.py +4 -0
- boto3_assist/dynamodb/dynamodb_key.py +3 -1
- boto3_assist/dynamodb/dynamodb_model_base.py +27 -7
- boto3_assist/dynamodb/dynamodb_reserved_words.py +7 -4
- boto3_assist/dynamodb/dynamodb_reserved_words.txt +0 -1
- boto3_assist/ec2/ec2_connection.py +3 -0
- boto3_assist/environment_services/environment_loader.py +67 -3
- boto3_assist/environment_services/environment_variables.py +4 -0
- boto3_assist/version.py +1 -1
- {boto3_assist-0.1.13.dist-info → boto3_assist-0.2.0.dist-info}/METADATA +1 -3
- boto3_assist-0.2.0.dist-info/RECORD +38 -0
- {boto3_assist-0.1.13.dist-info → boto3_assist-0.2.0.dist-info}/WHEEL +1 -1
- boto3_assist-0.1.13.dist-info/RECORD +0 -32
- {boto3_assist-0.1.13.dist-info → boto3_assist-0.2.0.dist-info}/licenses/LICENSE-EXPLAINED.txt +0 -0
- {boto3_assist-0.1.13.dist-info → boto3_assist-0.2.0.dist-info}/licenses/LICENSE.txt +0 -0
boto3_assist/boto3session.py
CHANGED
|
@@ -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.
|
|
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.
|
|
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
|
|
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
|
|
61
|
-
"and reuse your
|
|
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.
|
|
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.
|
|
72
|
+
return self.__connection_counter
|
|
73
73
|
|
|
74
74
|
def reset(self) -> None:
|
|
75
75
|
"""Resets the connection counter"""
|
|
76
|
-
self.
|
|
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 =
|
|
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 =
|
|
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:
|
|
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
|
-
) ->
|
|
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(
|
|
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
|
|
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 =
|
|
213
|
-
|
|
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 =
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
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
|
|
|
@@ -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:
|
|
@@ -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
|
-
|
|
73
|
-
|
|
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
|
-
"""
|
|
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
|
|
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
|
|
43
|
+
transformed_attributes: dict = {}
|
|
41
44
|
if isinstance(projections, str):
|
|
42
45
|
projections = projections.split(",")
|
|
43
46
|
for item in projections:
|
|
@@ -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
|
-
|
|
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
|
-
|
|
41
|
-
|
|
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
|
+
__version__ = '0.2.0'
|
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: boto3_assist
|
|
3
|
-
Version: 0.
|
|
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,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,,
|
{boto3_assist-0.1.13.dist-info → boto3_assist-0.2.0.dist-info}/licenses/LICENSE-EXPLAINED.txt
RENAMED
|
File without changes
|
|
File without changes
|