boto3-assist 0.1.14__py3-none-any.whl → 0.2.1__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.
Files changed (30) hide show
  1. boto3_assist/boto3session.py +3 -2
  2. boto3_assist/cloudwatch/cloudwatch_connection.py +84 -0
  3. boto3_assist/cloudwatch/cloudwatch_connection_tracker.py +17 -0
  4. boto3_assist/cloudwatch/cloudwatch_log_connection.py +62 -0
  5. boto3_assist/cloudwatch/cloudwatch_logs.py +39 -0
  6. boto3_assist/cloudwatch/cloudwatch_query.py +191 -0
  7. boto3_assist/connection.py +101 -0
  8. boto3_assist/connection_tracker.py +8 -8
  9. boto3_assist/dynamodb/dynamodb.py +30 -19
  10. boto3_assist/dynamodb/dynamodb_connection.py +31 -2
  11. boto3_assist/dynamodb/dynamodb_index.py +27 -0
  12. boto3_assist/dynamodb/dynamodb_iservice.py +4 -0
  13. boto3_assist/dynamodb/dynamodb_model_base.py +6 -7
  14. boto3_assist/dynamodb/dynamodb_reserved_words.py +6 -3
  15. boto3_assist/dynamodb/dynamodb_reserved_words.txt +0 -1
  16. boto3_assist/ec2/ec2_connection.py +3 -0
  17. boto3_assist/environment_services/environment_loader.py +67 -3
  18. boto3_assist/environment_services/environment_variables.py +4 -0
  19. boto3_assist/errors/custom_exceptions.py +34 -0
  20. boto3_assist/s3/s3.py +476 -0
  21. boto3_assist/s3/s3_connection.py +120 -0
  22. boto3_assist/utilities/file_operations.py +105 -0
  23. boto3_assist/utilities/http_utility.py +42 -0
  24. boto3_assist/version.py +1 -1
  25. {boto3_assist-0.1.14.dist-info → boto3_assist-0.2.1.dist-info}/METADATA +1 -3
  26. boto3_assist-0.2.1.dist-info/RECORD +43 -0
  27. {boto3_assist-0.1.14.dist-info → boto3_assist-0.2.1.dist-info}/WHEEL +1 -1
  28. boto3_assist-0.1.14.dist-info/RECORD +0 -32
  29. {boto3_assist-0.1.14.dist-info → boto3_assist-0.2.1.dist-info}/licenses/LICENSE-EXPLAINED.txt +0 -0
  30. {boto3_assist-0.1.14.dist-info → boto3_assist-0.2.1.dist-info}/licenses/LICENSE.txt +0 -0
@@ -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:
@@ -144,7 +144,7 @@ class DynamoDB(DynamoDBConnection):
144
144
  expression_attribute_names: Optional[dict] = None,
145
145
  source: Optional[str] = None,
146
146
  call_type: str = "resource",
147
- ) -> dict: ...
147
+ ) -> Dict[str, Any]: ...
148
148
 
149
149
  @overload
150
150
  def get(
@@ -158,7 +158,7 @@ class DynamoDB(DynamoDBConnection):
158
158
  expression_attribute_names: Optional[dict] = None,
159
159
  source: Optional[str] = None,
160
160
  call_type: str = "resource",
161
- ) -> dict: ...
161
+ ) -> Dict[str, Any]: ...
162
162
 
163
163
  @tracer.capture_method
164
164
  def get(
@@ -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,10 +92,13 @@ 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
98
- def dynamodb_client(self) -> DynamoDBClient:
101
+ def client(self) -> DynamoDBClient:
99
102
  """DynamoDB Client Connection"""
100
103
  if self.__dynamodb_client is None:
101
104
  logger.info("Creating DynamoDB Client")
@@ -105,13 +108,26 @@ class DynamoDBConnection:
105
108
  raise RuntimeError("DynamoDB Client is not available")
106
109
  return self.__dynamodb_client
107
110
 
111
+ @client.setter
112
+ def client(self, value: DynamoDBClient):
113
+ logger.info("Setting DynamoDB Client")
114
+ self.__dynamodb_client = value
115
+
116
+ @property
117
+ def dynamodb_client(self) -> DynamoDBClient:
118
+ """
119
+ DynamoDB Client Connection
120
+ - Backward Compatible. You should use client instead
121
+ """
122
+ return self.client
123
+
108
124
  @dynamodb_client.setter
109
125
  def dynamodb_client(self, value: DynamoDBClient):
110
126
  logger.info("Setting DynamoDB Client")
111
127
  self.__dynamodb_client = value
112
128
 
113
129
  @property
114
- def dynamodb_resource(self) -> DynamoDBServiceResource:
130
+ def resource(self) -> DynamoDBServiceResource:
115
131
  """DynamoDB Resource Connection"""
116
132
  if self.__dynamodb_resource is None:
117
133
  logger.info("Creating DynamoDB Resource")
@@ -122,6 +138,19 @@ class DynamoDBConnection:
122
138
 
123
139
  return self.__dynamodb_resource
124
140
 
141
+ @resource.setter
142
+ def resource(self, value: DynamoDBServiceResource):
143
+ logger.info("Setting DynamoDB Resource")
144
+ self.__dynamodb_resource = value
145
+
146
+ @property
147
+ def dynamodb_resource(self) -> DynamoDBServiceResource:
148
+ """
149
+ DynamoDB Resource Connection
150
+ - Backward Compatible. You should use resource instead
151
+ """
152
+ return self.resource
153
+
125
154
  @dynamodb_resource.setter
126
155
  def dynamodb_resource(self, value: DynamoDBServiceResource):
127
156
  logger.info("Setting DynamoDB Resource")
@@ -34,6 +34,33 @@ 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"The attrubute {index.partition_key.attribute_name} is already being used by index "
51
+ f"{v.name}. "
52
+ f"Reusing this attribute would over write the value on index {v.name}"
53
+ )
54
+ # check if the gsi1.sort_key.attribute_name exists
55
+ if index.sort_key is not None:
56
+ for _, v in self.__indexes.items():
57
+ if v.sort_key.attribute_name == index.sort_key.attribute_name:
58
+ raise ValueError(
59
+ f"The attrubute {index.sort_key.attribute_name} is already being used by index "
60
+ f"{v.name}. "
61
+ f"Reusing this attribute would over write the value on index {v.name}"
62
+ )
63
+
37
64
  self.__indexes[index.name] = index
38
65
 
39
66
  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,8 +9,7 @@ import datetime as dt
9
9
  import decimal
10
10
  import inspect
11
11
  import uuid
12
-
13
- from typing import TypeVar, List
12
+ from typing import TypeVar, List, Dict, Any
14
13
  from boto3.dynamodb.types import TypeSerializer
15
14
  from boto3_assist.utilities.serialization_utility import Serialization
16
15
  from boto3_assist.dynamodb.dynamodb_helpers import DynamoDBHelpers
@@ -82,6 +81,10 @@ class DynamoDBModelBase:
82
81
 
83
82
  return self.__projection_expression
84
83
 
84
+ @projection_expression.setter
85
+ def projection_expression(self, value: str | None):
86
+ self.__projection_expression = value
87
+
85
88
  @property
86
89
  @exclude_from_serialization
87
90
  def auto_generate_projections(self) -> bool:
@@ -92,10 +95,6 @@ class DynamoDBModelBase:
92
95
  def auto_generate_projections(self, value: bool):
93
96
  self.__auto_generate_projections = value
94
97
 
95
- @projection_expression.setter
96
- def projection_expression(self, value: str | None):
97
- self.__projection_expression = value
98
-
99
98
  @property
100
99
  @exclude_from_serialization
101
100
  def projection_expression_attribute_names(self) -> dict | None:
@@ -126,7 +125,7 @@ class DynamoDBModelBase:
126
125
  def projection_expression_attribute_names(self, value: dict | None):
127
126
  self.__projection_expression_attribute_names = value
128
127
 
129
- def map(self: T, item: dict | DynamoDBModelBase | None) -> T | None:
128
+ def map(self: T, item: Dict[str, Any] | DynamoDBModelBase | None) -> T | None:
130
129
  """
131
130
  Map the item to the instance. If the item is a DynamoDBModelBase,
132
131
  it will be converted to a dictionary first and then mapped.
@@ -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()
@@ -35,9 +38,9 @@ class DynamoDBReservedWords:
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
@@ -0,0 +1,34 @@
1
+ class Error(Exception):
2
+ """Base class for exceptions in this module."""
3
+
4
+
5
+ class DbFailures(Error):
6
+ """DB Failure Error"""
7
+
8
+
9
+ class InvalidHttpMethod(Exception):
10
+ """Invalid Http Method"""
11
+
12
+ def __init__(
13
+ self,
14
+ code=422,
15
+ message="Invalid Http Method",
16
+ ):
17
+ """The user account is not valid"""
18
+ self.message = {
19
+ "status_code": code,
20
+ "message": message,
21
+ }
22
+ super().__init__(self.message)
23
+
24
+
25
+ class InvalidRoutePath(Exception):
26
+ """Invalid Http Route"""
27
+
28
+ def __init__(self, message="Invalid Route"):
29
+ """Invalid Route"""
30
+ self.message = {
31
+ "status_code": 404,
32
+ "message": message,
33
+ }
34
+ super().__init__(self.message)