boto3-assist 0.34.0__py3-none-any.whl → 0.36.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
boto3_assist/__init__.py CHANGED
@@ -0,0 +1,10 @@
1
+ """
2
+ Geek Cafe, LLC
3
+ Maintainers: Eric Wilson
4
+ MIT License. See Project Root for the license information.
5
+ """
6
+
7
+ from .connection import Connection
8
+ from .connection_pool import ConnectionPool
9
+
10
+ __all__ = ["Connection", "ConnectionPool"]
@@ -5,6 +5,7 @@ MIT License. See Project Root for the license information.
5
5
  """
6
6
 
7
7
  from typing import Optional, List
8
+ import warnings
8
9
 
9
10
  from aws_lambda_powertools import Logger
10
11
  from botocore.config import Config
@@ -13,6 +14,7 @@ from boto3_assist.environment_services.environment_variables import (
13
14
  EnvironmentVariables,
14
15
  )
15
16
  from boto3_assist.connection_tracker import ConnectionTracker
17
+ from boto3_assist.connection_pool import ConnectionPool
16
18
 
17
19
 
18
20
  logger = Logger()
@@ -35,6 +37,7 @@ class Connection:
35
37
  assume_role_chain: Optional[List[str]] = None,
36
38
  assume_role_duration_seconds: Optional[int] = 3600,
37
39
  config: Optional[Config] = None,
40
+ use_connection_pool: bool = False,
38
41
  ) -> None:
39
42
  self.__aws_profile = aws_profile
40
43
  self.__aws_region = aws_region
@@ -47,6 +50,8 @@ class Connection:
47
50
  self.__assume_role_chain = assume_role_chain
48
51
  self.__assume_role_duration_seconds = assume_role_duration_seconds
49
52
  self.__config = config
53
+ self.__use_connection_pool = use_connection_pool
54
+
50
55
  if self.__service_name is None:
51
56
  raise RuntimeError(
52
57
  "Service Name is not available. The service name is required."
@@ -54,6 +59,18 @@ class Connection:
54
59
 
55
60
  self.raise_on_error: bool = True
56
61
 
62
+ # Issue deprecation warning if not using connection pool
63
+ if not use_connection_pool:
64
+ warnings.warn(
65
+ f"Creating {service_name} Connection without connection pooling. "
66
+ "This creates a new boto3 session on each instantiation, which can impact "
67
+ "Lambda performance. Consider using Connection.from_pool() or "
68
+ "use_connection_pool=True for better performance in Lambda functions. "
69
+ "The default will change to use_connection_pool=True in boto3-assist v2.0.0.",
70
+ DeprecationWarning,
71
+ stacklevel=2,
72
+ )
73
+
57
74
  def setup(self, setup_source: Optional[str] = None) -> None:
58
75
  """
59
76
  Setup the environment. Automatically called via init.
@@ -139,12 +156,74 @@ class Connection:
139
156
  logger.debug("Setting Service Name")
140
157
  self.__service_name = value
141
158
 
159
+ @classmethod
160
+ def from_pool(
161
+ cls,
162
+ service_name: str,
163
+ aws_profile: Optional[str] = None,
164
+ aws_region: Optional[str] = None,
165
+ aws_end_point_url: Optional[str] = None,
166
+ config: Optional[Config] = None,
167
+ **kwargs,
168
+ ) -> "Connection":
169
+ """
170
+ Create connection using connection pool (recommended for Lambda).
171
+
172
+ This is the recommended pattern for Lambda functions as it reuses
173
+ boto3 sessions across invocations in warm containers, reducing
174
+ connection overhead and improving performance.
175
+
176
+ Args:
177
+ service_name: AWS service name (e.g., 's3', 'dynamodb', 'sqs')
178
+ aws_profile: AWS profile name (optional)
179
+ aws_region: AWS region (optional)
180
+ aws_end_point_url: Custom endpoint URL (optional, for moto testing)
181
+ config: Botocore Config object (optional)
182
+ **kwargs: Additional Connection parameters
183
+
184
+ Returns:
185
+ Connection instance configured to use connection pool
186
+
187
+ Example:
188
+ >>> # Recommended pattern for Lambda
189
+ >>> conn = Connection.from_pool(service_name="dynamodb")
190
+ >>> client = conn.session.client
191
+ >>>
192
+ >>> # Subsequent calls reuse the same session
193
+ >>> conn2 = Connection.from_pool(service_name="dynamodb")
194
+ >>> assert conn.session is conn2.session
195
+ """
196
+ return cls(
197
+ service_name=service_name,
198
+ aws_profile=aws_profile,
199
+ aws_region=aws_region,
200
+ aws_end_point_url=aws_end_point_url,
201
+ config=config,
202
+ use_connection_pool=True,
203
+ **kwargs,
204
+ )
205
+
142
206
  @property
143
207
  def session(self) -> Boto3SessionManager:
144
208
  """Session"""
145
- if self.__session is None:
146
- self.setup(setup_source="session init")
147
-
148
- if self.__session is None:
149
- raise RuntimeError("Session is not available")
150
- return self.__session
209
+ if self.__use_connection_pool:
210
+ # Use connection pool for session management
211
+ pool = ConnectionPool.get_instance()
212
+ return pool.get_session(
213
+ service_name=self.service_name,
214
+ aws_profile=self.aws_profile,
215
+ aws_region=self.aws_region,
216
+ aws_endpoint_url=self.end_point_url,
217
+ config=self.__config,
218
+ assume_role_arn=self.__assume_role_arn,
219
+ assume_role_chain=self.__assume_role_chain,
220
+ assume_role_duration_seconds=self.__assume_role_duration_seconds,
221
+ )
222
+ else:
223
+ # Legacy behavior: create session on demand
224
+ if self.__session is None:
225
+ self.setup(setup_source="session init")
226
+
227
+ if self.__session is None:
228
+ raise RuntimeError("Session is not available")
229
+ return self.__session
@@ -0,0 +1,179 @@
1
+ """
2
+ Geek Cafe, LLC
3
+ Maintainers: Eric Wilson
4
+ MIT License. See Project Root for the license information.
5
+
6
+ Connection pooling for boto3 sessions to improve Lambda performance.
7
+ """
8
+
9
+ from typing import Dict, Optional
10
+ from aws_lambda_powertools import Logger
11
+ from .boto3session import Boto3SessionManager
12
+
13
+ logger = Logger()
14
+
15
+
16
+ class ConnectionPool:
17
+ """
18
+ Singleton connection pool for reusing boto3 sessions.
19
+
20
+ Recommended for Lambda functions to minimize connection overhead and
21
+ improve performance in warm containers.
22
+
23
+ Example:
24
+ >>> pool = ConnectionPool.get_instance()
25
+ >>> session = pool.get_session(service_name="dynamodb")
26
+ >>> client = session.client
27
+
28
+ # Subsequent calls reuse the same session
29
+ >>> session2 = pool.get_session(service_name="dynamodb")
30
+ >>> assert session is session2 # Same instance
31
+ """
32
+
33
+ _instance: Optional["ConnectionPool"] = None
34
+
35
+ def __init__(self):
36
+ """Initialize connection pool. Use get_instance() instead."""
37
+ self._connections: Dict[str, Boto3SessionManager] = {}
38
+
39
+ @classmethod
40
+ def get_instance(cls) -> "ConnectionPool":
41
+ """
42
+ Get singleton instance of the connection pool.
43
+
44
+ Returns:
45
+ ConnectionPool: Singleton instance
46
+ """
47
+ if cls._instance is None:
48
+ logger.debug("Creating new ConnectionPool instance")
49
+ cls._instance = ConnectionPool()
50
+ return cls._instance
51
+
52
+ def get_session(
53
+ self,
54
+ service_name: str,
55
+ aws_profile: Optional[str] = None,
56
+ aws_region: Optional[str] = None,
57
+ aws_endpoint_url: Optional[str] = None,
58
+ **kwargs,
59
+ ) -> Boto3SessionManager:
60
+ """
61
+ Get or create cached session for service.
62
+
63
+ Sessions are cached based on service_name, profile, region, and endpoint.
64
+ Subsequent calls with the same parameters return the cached session.
65
+
66
+ Args:
67
+ service_name: AWS service name (e.g., 's3', 'dynamodb', 'sqs')
68
+ aws_profile: AWS profile name (optional)
69
+ aws_region: AWS region (optional, defaults to environment/config)
70
+ aws_endpoint_url: Custom endpoint URL (optional, useful for moto testing)
71
+ **kwargs: Additional Boto3SessionManager parameters
72
+
73
+ Returns:
74
+ Boto3SessionManager: Cached or newly created session manager
75
+
76
+ Example:
77
+ >>> pool = ConnectionPool.get_instance()
78
+ >>> # First call creates new session
79
+ >>> s3_session = pool.get_session(service_name="s3")
80
+ >>> # Second call returns cached session
81
+ >>> s3_session2 = pool.get_session(service_name="s3")
82
+ >>> assert s3_session is s3_session2
83
+ """
84
+ key = self._make_key(service_name, aws_profile, aws_region, aws_endpoint_url)
85
+
86
+ if key not in self._connections:
87
+ logger.debug(
88
+ f"Creating new session for {service_name}",
89
+ extra={
90
+ "service_name": service_name,
91
+ "aws_profile": aws_profile,
92
+ "aws_region": aws_region,
93
+ "cache_key": key,
94
+ },
95
+ )
96
+ self._connections[key] = Boto3SessionManager(
97
+ service_name=service_name,
98
+ aws_profile=aws_profile,
99
+ aws_region=aws_region,
100
+ aws_endpoint_url=aws_endpoint_url,
101
+ **kwargs,
102
+ )
103
+ else:
104
+ logger.debug(
105
+ f"Reusing cached session for {service_name}",
106
+ extra={
107
+ "service_name": service_name,
108
+ "cache_key": key,
109
+ "total_cached_sessions": len(self._connections),
110
+ },
111
+ )
112
+
113
+ return self._connections[key]
114
+
115
+ def reset(self):
116
+ """
117
+ Reset all connections in the pool.
118
+
119
+ This clears all cached sessions. Useful for testing or when you need
120
+ to force recreation of all connections.
121
+
122
+ Warning:
123
+ This should only be used in testing scenarios. In production Lambda
124
+ functions, connections should persist across invocations.
125
+
126
+ Example:
127
+ >>> pool = ConnectionPool.get_instance()
128
+ >>> pool.get_session(service_name="s3")
129
+ >>> pool.reset() # Clear all cached sessions
130
+ >>> # Next call will create new session
131
+ >>> pool.get_session(service_name="s3")
132
+ """
133
+ logger.debug(f"Resetting connection pool ({len(self._connections)} sessions)")
134
+ self._connections.clear()
135
+
136
+ def get_stats(self) -> Dict[str, int]:
137
+ """
138
+ Get statistics about the connection pool.
139
+
140
+ Returns:
141
+ Dict with pool statistics:
142
+ - total_connections: Number of cached sessions
143
+ - services: Number of unique services
144
+
145
+ Example:
146
+ >>> pool = ConnectionPool.get_instance()
147
+ >>> pool.get_session(service_name="s3")
148
+ >>> pool.get_session(service_name="dynamodb")
149
+ >>> stats = pool.get_stats()
150
+ >>> print(stats)
151
+ {'total_connections': 2, 'services': 2}
152
+ """
153
+ services = set()
154
+ for key in self._connections.keys():
155
+ service_name = key.split(":")[0]
156
+ services.add(service_name)
157
+
158
+ return {"total_connections": len(self._connections), "services": len(services)}
159
+
160
+ @staticmethod
161
+ def _make_key(
162
+ service_name: str,
163
+ profile: Optional[str],
164
+ region: Optional[str],
165
+ endpoint: Optional[str],
166
+ ) -> str:
167
+ """
168
+ Create cache key from connection parameters.
169
+
170
+ Args:
171
+ service_name: AWS service name
172
+ profile: AWS profile name (or None)
173
+ region: AWS region (or None)
174
+ endpoint: Custom endpoint URL (or None)
175
+
176
+ Returns:
177
+ str: Cache key for this combination of parameters
178
+ """
179
+ return f"{service_name}:{profile}:{region}:{endpoint}"
@@ -0,0 +1,9 @@
1
+ """
2
+ Geek Cafe, LLC
3
+ Maintainers: Eric Wilson
4
+ MIT License. See Project Root for the license information.
5
+ """
6
+
7
+ from .dynamodb import DynamoDB
8
+
9
+ __all__ = ["DynamoDB"]