boto3-assist 0.4.0__py3-none-any.whl → 0.5.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.
@@ -1,80 +1,120 @@
1
1
  """
2
2
  Geek Cafe, LLC
3
3
  Maintainers: Eric Wilson
4
- MIT License. See Project Root for the license information.
4
+ MIT License. See Project Root for the license information.
5
5
  """
6
6
 
7
7
  import traceback
8
8
  import os
9
+ from typing import Dict
9
10
 
10
11
 
11
12
  class ConnectionTracker:
12
13
  """
13
- Tracks Connection Requests.
14
- Useful in for performance tuning and debugging.
14
+ Tracks Boto3 connection requests for performance tuning and debugging.
15
+
16
+ Attributes:
17
+ __stack_trace_env_var (str): Environment variable name to enable stack trace logging.
18
+ __issue_stack_trace (bool | None): Caches the result of whether stack trace logging is enabled.
19
+ __connection_counter (Dict[str, int]): Tracks the number of connections per service.
15
20
  """
16
21
 
17
- def __init__(self, service_name: str) -> None:
22
+ def __init__(self) -> None:
18
23
  self.__stack_trace_env_var: str = "BOTO3_ASSIST_CONNECTION_STACK_TRACE"
19
- self.__connection_counter: int = 0
20
- self.__service_name: str = service_name
21
24
  self.__issue_stack_trace: bool | None = None
25
+ self.__connection_counter: Dict[str, int] = {}
26
+
27
+ def add(self, service_name: str) -> None:
28
+ """
29
+ Increments the connection count for a given service and
30
+ performs a check on the number of connections.
31
+
32
+ Args:
33
+ service_name (str): Name of the AWS service.
34
+ """
35
+ self.__connection_counter[service_name] = (
36
+ self.__connection_counter.get(service_name, 0) + 1
37
+ )
38
+
39
+ self.check(service_name=service_name)
22
40
 
23
41
  @property
24
42
  def issue_stack_trace(self) -> bool:
25
- """Returns True if the stack trace should be issued"""
43
+ """
44
+ Checks if stack trace logging is enabled by the environment variable.
45
+
46
+ Returns:
47
+ bool: True if stack trace logging is enabled, False otherwise.
48
+ """
26
49
  if self.__issue_stack_trace is None:
27
50
  self.__issue_stack_trace = (
28
51
  os.getenv(self.__stack_trace_env_var, "").lower() == "true"
29
52
  )
30
53
  return self.__issue_stack_trace
31
54
 
32
- def increment_connection(self) -> None:
33
- """Increments the connection counter"""
34
- self.__connection_counter += 1
55
+ def check(self, service_name: str) -> None:
56
+ """
57
+ Checks the connection count for a service and logs warnings if needed.
35
58
 
36
- if self.connection_count > 1:
37
- service_message = ""
38
- stack_trace_message = ""
39
- if self.__service_name:
40
- service_message = f"Your {self.__service_name} service has more than one connection.\n"
59
+ Args:
60
+ service_name (str): Name of the AWS service.
61
+ """
62
+ connection_count = self.__connection_counter.get(service_name, 0)
63
+ if connection_count > 1:
64
+ service_message = (
65
+ f"Your {service_name} service has more than one connection.\n"
66
+ )
41
67
 
42
68
  if not self.issue_stack_trace:
43
69
  stack_trace_message = (
44
- f"\nTo add addtional information to the log and determine where additional connections are being created"
45
- f", set the environment variable {self.__stack_trace_env_var} to true.\n"
70
+ f"\nTo add additional information to the log and determine where additional connections are being created, "
71
+ f"set the environment variable {self.__stack_trace_env_var} to true.\n"
46
72
  )
47
73
  else:
48
74
  stack = "\n".join(traceback.format_stack())
49
75
  stack_trace_message = (
50
- f"\nStack Trace Enabeld with {self.__stack_trace_env_var}"
51
- f"\n{stack}"
76
+ f"\nStack Trace Enabled with {self.__stack_trace_env_var}\n{stack}"
52
77
  )
53
78
 
54
79
  self.__log_warning(
55
80
  f"{service_message}"
56
- f"Your boto3 connection count is {self.connection_count}. "
57
- "Under most circumstances you should be able to use the same connection "
58
- "vs. creating a new one. Connections are expensive in terms of time / latency. "
59
- "If you are seeing perforance issues, check how and where you are creating your "
60
- "connections. You should be able to pass the connection to your other objects "
81
+ f"Your boto3 connection count is {connection_count}. "
82
+ "Under most circumstances, you should be able to use the same connection "
83
+ "instead of creating a new one. Connections are expensive in terms of time and latency. "
84
+ "If you are seeing performance issues, check how and where you are creating your "
85
+ "connections. You should be able to pass the connection to your other objects "
61
86
  "and reuse your boto3 connections."
87
+ "\n\nMOCK Testing may show this message as well, in which case you can dismiss this warning.\n\n"
62
88
  f"{stack_trace_message}"
63
89
  )
64
90
 
65
- def decrement_connection(self) -> None:
66
- """Decrements the connection counter"""
67
- self.__connection_counter -= 1
91
+ def decrement_connection(self, service_name: str) -> None:
92
+ """
93
+ Decrements the connection count for a service.
68
94
 
69
- @property
70
- def connection_count(self) -> int:
71
- """Returns the current connection count"""
72
- return self.__connection_counter
95
+ Args:
96
+ service_name (str): Name of the AWS service.
97
+ """
98
+ if (
99
+ service_name in self.__connection_counter
100
+ and self.__connection_counter[service_name] > 0
101
+ ):
102
+ self.__connection_counter[service_name] -= 1
73
103
 
74
- def reset(self) -> None:
75
- """Resets the connection counter"""
76
- self.__connection_counter = 0
104
+ def reset(self, service_name: str) -> None:
105
+ """
106
+ Resets the connection count for a service to zero.
107
+
108
+ Args:
109
+ service_name (str): Name of the AWS service.
110
+ """
111
+ self.__connection_counter[service_name] = 0
77
112
 
78
113
  def __log_warning(self, message: str) -> None:
79
- """Logs a warning message"""
114
+ """
115
+ Logs a warning message.
116
+
117
+ Args:
118
+ message (str): The warning message to log.
119
+ """
80
120
  print(f"Warning: {message}")
@@ -8,11 +8,8 @@ from typing import Optional
8
8
  from typing import TYPE_CHECKING
9
9
 
10
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.dynamodb.dynamodb_connection_tracker import DynamoDBConnectionTracker
11
+ from boto3_assist.connection import Connection
12
+
16
13
 
17
14
  if TYPE_CHECKING:
18
15
  from mypy_boto3_dynamodb import DynamoDBClient, DynamoDBServiceResource
@@ -22,10 +19,9 @@ else:
22
19
 
23
20
 
24
21
  logger = Logger()
25
- tracker: DynamoDBConnectionTracker = DynamoDBConnectionTracker()
26
22
 
27
23
 
28
- class DynamoDBConnection:
24
+ class DynamoDBConnection(Connection):
29
25
  """DB Environment"""
30
26
 
31
27
  def __init__(
@@ -37,66 +33,20 @@ class DynamoDBConnection:
37
33
  aws_access_key_id: Optional[str] = None,
38
34
  aws_secret_access_key: Optional[str] = None,
39
35
  ) -> None:
40
- self.aws_profile = aws_profile or EnvironmentVariables.AWS.profile()
41
- self.aws_region = aws_region or EnvironmentVariables.AWS.region()
42
- self.end_point_url = (
43
- aws_end_point_url or EnvironmentVariables.AWS.DynamoDB.endpoint_url()
44
- )
45
- self.aws_access_key_id = (
46
- aws_access_key_id or EnvironmentVariables.AWS.DynamoDB.aws_access_key_id()
47
- )
48
- self.aws_secret_access_key = (
49
- aws_secret_access_key
50
- or EnvironmentVariables.AWS.DynamoDB.aws_secret_access_key()
36
+ super().__init__(
37
+ service_name="dynamodb",
38
+ aws_profile=aws_profile,
39
+ aws_region=aws_region,
40
+ aws_access_key_id=aws_access_key_id,
41
+ aws_secret_access_key=aws_secret_access_key,
42
+ aws_end_point_url=aws_end_point_url,
51
43
  )
52
- self.__session: Boto3SessionManager | None = None
44
+
53
45
  self.__dynamodb_client: DynamoDBClient | None = None
54
46
  self.__dynamodb_resource: DynamoDBServiceResource | None = None
55
47
 
56
48
  self.raise_on_error: bool = True
57
49
 
58
- def setup(self, setup_source: Optional[str] = None) -> None:
59
- """
60
- Setup the environment. Automatically called via init.
61
- You can run setup at anytime with new parameters.
62
- Args: setup_source: Optional[str] = None
63
- Defines the source of the setup. Useful for logging.
64
- Returns: None
65
- """
66
-
67
- logger.info(
68
- {
69
- "metric_filter": "db_connection_setup",
70
- "source": "DynamoDBConnection",
71
- "aws_profile": self.aws_profile,
72
- "aws_region": self.aws_region,
73
- "setup_source": setup_source,
74
- }
75
- )
76
-
77
- self.__session = Boto3SessionManager(
78
- service_name="dynamodb",
79
- aws_profile=self.aws_profile,
80
- aws_region=self.aws_region,
81
- aws_access_key_id=self.aws_access_key_id,
82
- aws_secret_access_key=self.aws_secret_access_key,
83
- aws_endpoint_url=self.end_point_url,
84
- )
85
-
86
- tracker.increment_connection()
87
-
88
- self.raise_on_error = EnvironmentVariables.AWS.DynamoDB.raise_on_error_setting()
89
-
90
- @property
91
- def session(self) -> Boto3SessionManager:
92
- """Session"""
93
- if self.__session is None:
94
- self.setup(setup_source="session init")
95
-
96
- if self.__session is None:
97
- raise RuntimeError("Session is not available")
98
- return self.__session
99
-
100
50
  @property
101
51
  def client(self) -> DynamoDBClient:
102
52
  """DynamoDB Client Connection"""
@@ -8,24 +8,20 @@ from typing import Optional
8
8
  from typing import TYPE_CHECKING
9
9
 
10
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.connection_tracker import ConnectionTracker
11
+
12
+ from boto3_assist.connection import Connection
16
13
 
17
14
  if TYPE_CHECKING:
18
15
  from mypy_boto3_ec2 import Client
19
16
  else:
20
17
  Client = object
21
18
 
22
- SERVICE_NAME = "ec2"
19
+
23
20
  logger = Logger()
24
- tracker: ConnectionTracker = ConnectionTracker(service_name=SERVICE_NAME)
25
21
 
26
22
 
27
- class EC2Connection:
28
- """DB Environment"""
23
+ class EC2Connection(Connection):
24
+ """EC2 Connection"""
29
25
 
30
26
  def __init__(
31
27
  self,
@@ -35,69 +31,24 @@ class EC2Connection:
35
31
  aws_access_key_id: Optional[str] = None,
36
32
  aws_secret_access_key: Optional[str] = None,
37
33
  ) -> None:
38
- self.aws_profile = aws_profile
39
- self.aws_region = aws_region
34
+ super().__init__(
35
+ service_name="ec2",
36
+ aws_profile=aws_profile,
37
+ aws_region=aws_region,
38
+ aws_access_key_id=aws_access_key_id,
39
+ aws_secret_access_key=aws_secret_access_key,
40
+ )
40
41
 
41
- self.aws_access_key_id = aws_access_key_id
42
- self.aws_secret_access_key = aws_secret_access_key
43
- self.__session: Boto3SessionManager | None = None
44
42
  self.__client: Client | None = None
45
43
 
46
44
  self.raise_on_error: bool = True
47
45
 
48
- def setup(self, setup_source: Optional[str] = None) -> None:
49
- """
50
- Setup the environment. Automatically called via init.
51
- You can run setup at anytime with new parameters.
52
- Args: setup_source: Optional[str] = None
53
- Defines the source of the setup. Useful for logging.
54
- Returns: None
55
- """
56
-
57
- logger.info(
58
- {
59
- "metric_filter": "connection_setup",
60
- "source": "setup",
61
- "aws_profile": self.aws_profile,
62
- "aws_region": self.aws_region,
63
- "setup_source": setup_source,
64
- }
65
- )
66
-
67
- # lazy load the session
68
- self.__session = Boto3SessionManager(
69
- service_name=SERVICE_NAME,
70
- aws_profile=self.aws_profile,
71
- aws_region=self.aws_region or EnvironmentVariables.AWS.region(),
72
- aws_access_key_id=self.aws_access_key_id
73
- or EnvironmentVariables.AWS.aws_access_key_id(),
74
- aws_secret_access_key=self.aws_secret_access_key
75
- or EnvironmentVariables.AWS.aws_secret_access_key(),
76
- )
77
-
78
- tracker.increment_connection()
79
-
80
- self.raise_on_error = EnvironmentVariables.AWS.DynamoDB.raise_on_error_setting()
81
-
82
- @property
83
- def session(self) -> Boto3SessionManager:
84
- """Session"""
85
- if self.__session is None:
86
- self.setup(setup_source="session init")
87
-
88
- if self.__session is None:
89
- raise RuntimeError("Session is not available")
90
- return self.__session
91
-
92
46
  @property
93
47
  def client(self) -> Client:
94
48
  """Client Connection"""
95
49
  if self.__client is None:
96
- logger.info("Creating Client")
97
50
  self.__client = self.session.client
98
51
 
99
- if self.raise_on_error and self.__client is None:
100
- raise RuntimeError("Client is not available")
101
52
  return self.__client
102
53
 
103
54
  @client.setter
@@ -96,7 +96,7 @@ class EnvironmentLoader:
96
96
  paths: List[str] = []
97
97
  for parent in range(parents):
98
98
  path = Path(starting_path).parents[parent].absolute()
99
- print(f"searching: {path}")
99
+ logger.debug(f"searching for {file_name} in: {path}")
100
100
  tmp = os.path.join(path, file_name)
101
101
  paths.append(tmp)
102
102
  if os.path.exists(tmp):
@@ -0,0 +1,30 @@
1
+ """
2
+ Geek Cafe, LLC
3
+ Maintainers: Eric Wilson
4
+ MIT License. See Project Root for the license information.
5
+ """
6
+
7
+ from __future__ import annotations
8
+ from typing import TypeVar
9
+ from boto3_assist.utilities.serialization_utility import Serialization
10
+
11
+
12
+ class SerializableModel:
13
+ """Library to Serialize object to a DynamoDB Format"""
14
+
15
+ T = TypeVar("T", bound="SerializableModel")
16
+
17
+ @staticmethod
18
+ def map(source: dict | object, target: T, coerce: bool = True) -> T:
19
+ """
20
+ Map the source dictionary to the target object.
21
+
22
+ Args:
23
+ - source: The dictionary to map from.
24
+ - target: The object to map to.
25
+ """
26
+ mapped = Serialization.map(source, target, coerce=coerce)
27
+ if mapped is None:
28
+ raise ValueError("Unable to map source to target")
29
+
30
+ return mapped
@@ -12,7 +12,7 @@ from boto3_assist.boto3session import Boto3SessionManager
12
12
  from boto3_assist.environment_services.environment_variables import (
13
13
  EnvironmentVariables,
14
14
  )
15
- from boto3_assist.connection_tracker import ConnectionTracker
15
+ from boto3_assist.connection import Connection
16
16
 
17
17
  if TYPE_CHECKING:
18
18
  from mypy_boto3_s3 import S3Client, S3ServiceResource
@@ -22,10 +22,9 @@ else:
22
22
 
23
23
 
24
24
  logger = Logger()
25
- tracker: ConnectionTracker = ConnectionTracker(service_name="s3")
26
25
 
27
26
 
28
- class S3Connection:
27
+ class S3Connection(Connection):
29
28
  """Connection"""
30
29
 
31
30
  def __init__(
@@ -37,64 +36,24 @@ class S3Connection:
37
36
  aws_access_key_id: Optional[str] = None,
38
37
  aws_secret_access_key: Optional[str] = None,
39
38
  ) -> None:
40
- self.aws_profile = aws_profile or EnvironmentVariables.AWS.profile()
41
- self.aws_region = aws_region or EnvironmentVariables.AWS.region()
42
- self.end_point_url = (
43
- aws_end_point_url or EnvironmentVariables.AWS.endpoint_url()
44
- )
45
- self.aws_access_key_id = (
46
- aws_access_key_id or EnvironmentVariables.AWS.aws_access_key_id()
47
- )
48
- self.aws_secret_access_key = (
49
- aws_secret_access_key or EnvironmentVariables.AWS.aws_secret_access_key()
50
- )
51
- self.__session: Boto3SessionManager | None = None
52
- self.__client: S3Client | None = None
53
- self.__resource: S3ServiceResource | None = None
54
-
55
- self.raise_on_error: bool = True
56
-
57
- def setup(self, setup_source: Optional[str] = None) -> None:
58
- """
59
- Setup the environment. Automatically called via init.
60
- You can run setup at anytime with new parameters.
61
- Args: setup_source: Optional[str] = None
62
- Defines the source of the setup. Useful for logging.
63
- Returns: None
64
- """
65
-
66
- self.__session = Boto3SessionManager(
39
+ super().__init__(
67
40
  service_name="s3",
68
- aws_profile=self.aws_profile,
69
- aws_region=self.aws_region,
70
- aws_access_key_id=self.aws_access_key_id,
71
- aws_secret_access_key=self.aws_secret_access_key,
72
- aws_endpoint_url=self.end_point_url,
41
+ aws_profile=aws_profile,
42
+ aws_region=aws_region,
43
+ aws_access_key_id=aws_access_key_id,
44
+ aws_secret_access_key=aws_secret_access_key,
45
+ aws_end_point_url=aws_end_point_url,
73
46
  )
74
47
 
75
- tracker.increment_connection()
76
-
77
- self.raise_on_error = False
78
-
79
- @property
80
- def session(self) -> Boto3SessionManager:
81
- """Session"""
82
- if self.__session is None:
83
- self.setup(setup_source="session init")
84
-
85
- if self.__session is None:
86
- raise RuntimeError("Session is not available")
87
- return self.__session
48
+ self.__client: S3Client | None = None
49
+ self.__resource: S3ServiceResource | None = None
88
50
 
89
51
  @property
90
52
  def client(self) -> S3Client:
91
53
  """Client Connection"""
92
54
  if self.__client is None:
93
- logger.info("Creating Client")
94
55
  self.__client = self.session.client
95
56
 
96
- if self.raise_on_error and self.__client is None:
97
- raise RuntimeError("Client is not available")
98
57
  return self.__client
99
58
 
100
59
  @client.setter
@@ -317,3 +317,29 @@ class DatetimeUtility:
317
317
  tz = pytz.timezone(timezone_name)
318
318
  result = utc_datetime.astimezone(tz)
319
319
  return result
320
+
321
+ @staticmethod
322
+ def get_timestamp(value: datetime | None | str) -> float:
323
+ """Get a timestampe from a date or 0.0"""
324
+ if value is None:
325
+ return 0.0
326
+ if not isinstance(value, datetime):
327
+ value = DatetimeUtility.to_datetime_utc(value=value)
328
+
329
+ if not isinstance(value, datetime):
330
+ return 0.0
331
+ ts = value.timestamp()
332
+ return ts
333
+
334
+ @staticmethod
335
+ def get_timestamp_or_none(value: datetime | None | str) -> float | None:
336
+ """Get a timestampe from a date or None"""
337
+ if value is None:
338
+ return None
339
+ if not isinstance(value, datetime):
340
+ value = DatetimeUtility.to_datetime_utc(value=value)
341
+
342
+ if not isinstance(value, datetime):
343
+ return None
344
+ ts = value.timestamp()
345
+ return ts
@@ -0,0 +1,26 @@
1
+ from typing import List
2
+
3
+
4
+ class DictionaryUtilitiy:
5
+ """
6
+ A class to provide utility methods for working with dictionaries.
7
+ """
8
+
9
+ @staticmethod
10
+ def find_dict_by_name(
11
+ dict_list: List[dict], key_field: str, name: str
12
+ ) -> List[dict] | dict | str:
13
+ """
14
+ Searches for dictionaries in a list where the key 'name' matches the specified value.
15
+
16
+ Args:
17
+ dict_list (list): A list of dictionaries to search through.
18
+ key_field (str): The key to search for in each dictionary.
19
+ name (str): The value to search for in the 'key_field' key.
20
+
21
+ Returns:
22
+ list: A list of dictionaries where the 'key_field' key matches the specified value.
23
+ """
24
+ # List comprehension to filter dictionaries that have the 'name' key equal to the specified name
25
+
26
+ return [d for d in dict_list if d.get(key_field) == name]