testcontainers-aws 0.1.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.
@@ -0,0 +1,12 @@
1
+ """
2
+ testcontainers-aws: Simplified AWS service testing with Testcontainers
3
+
4
+ Provides easy-to-use abstractions for testing AWS services using Testcontainers
5
+ and LocalStack. Automatically handles container lifecycle, service initialization,
6
+ and boto3 client configuration.
7
+ """
8
+
9
+ from testcontainers_aws.core import AWSTestEnvironment
10
+
11
+ __version__ = "0.1.0"
12
+ __all__ = ["AWSTestEnvironment"]
@@ -0,0 +1,261 @@
1
+ """Core AWS test environment orchestration."""
2
+
3
+ from typing import Dict, List, Optional, Any
4
+ import boto3
5
+ from testcontainers.localstack import LocalStackContainer
6
+
7
+ from testcontainers_aws.services.s3 import S3ServiceManager
8
+ from testcontainers_aws.services.dynamodb import DynamoDBServiceManager
9
+ from testcontainers_aws.services.sqs import SQSServiceManager
10
+ from testcontainers_aws.services.kinesis import KinesisServiceManager
11
+ from testcontainers_aws.services.lambda_service import LambdaServiceManager
12
+ from testcontainers_aws.services.sns import SNSServiceManager
13
+
14
+
15
+ class AWSTestEnvironment:
16
+ """
17
+ Orchestrates AWS services for testing using LocalStack.
18
+
19
+ Provides a unified interface to spin up and configure multiple AWS services
20
+ in a single container, with automatic boto3 client configuration and
21
+ helper methods for common testing operations.
22
+
23
+ Example:
24
+ ```python
25
+ with AWSTestEnvironment(services=['s3', 'dynamodb']) as aws:
26
+ s3 = aws.get_s3_client()
27
+ dynamodb = aws.get_dynamodb_client()
28
+
29
+ # Use clients for testing
30
+ s3.create_bucket(Bucket='test-bucket')
31
+ ```
32
+ """
33
+
34
+ # Map service names to LocalStack service strings
35
+ SERVICE_MAP = {
36
+ 's3': 's3',
37
+ 'dynamodb': 'dynamodb',
38
+ 'sqs': 'sqs',
39
+ 'sns': 'sns',
40
+ 'kinesis': 'kinesis',
41
+ 'lambda': 'lambda',
42
+ 'logs': 'logs',
43
+ 'cloudformation': 'cloudformation',
44
+ 'iam': 'iam',
45
+ 'sts': 'sts',
46
+ 'ssm': 'ssm',
47
+ 'secretsmanager': 'secretsmanager',
48
+ }
49
+
50
+ def __init__(
51
+ self,
52
+ services: Optional[List[str]] = None,
53
+ init_config: Optional[Dict[str, Any]] = None,
54
+ image_tag: str = "latest",
55
+ ):
56
+ """
57
+ Initialize AWS test environment.
58
+
59
+ Args:
60
+ services: List of AWS services to enable (e.g., ['s3', 'dynamodb']).
61
+ If None, all common services are enabled.
62
+ init_config: Configuration for initializing resources (buckets, tables, etc.)
63
+ image_tag: LocalStack Docker image tag (default: 'latest')
64
+ """
65
+ self.services = services or ['s3', 'dynamodb', 'sqs', 'kinesis']
66
+ self.init_config = init_config or {}
67
+ self.image_tag = image_tag
68
+
69
+ self._container: Optional[LocalStackContainer] = None
70
+ self._clients: Dict[str, Any] = {}
71
+ self._service_managers: Dict[str, Any] = {}
72
+
73
+ def __enter__(self) -> 'AWSTestEnvironment':
74
+ """Start the container and initialize services."""
75
+ self.start()
76
+ return self
77
+
78
+ def __exit__(self, exc_type, exc_val, exc_tb):
79
+ """Stop the container and cleanup."""
80
+ self.stop()
81
+
82
+ def start(self) -> None:
83
+ """Start LocalStack container and initialize configured services."""
84
+ # Convert service names to LocalStack service strings
85
+ localstack_services = [self.SERVICE_MAP.get(s, s) for s in self.services]
86
+
87
+ # Create and start LocalStack container
88
+ self._container = LocalStackContainer(
89
+ image=f"localstack/localstack:{self.image_tag}"
90
+ )
91
+
92
+ # Set services to enable
93
+ for service in localstack_services:
94
+ self._container = self._container.with_services(service)
95
+
96
+ self._container.start()
97
+
98
+ # Initialize service managers
99
+ self._init_service_managers()
100
+
101
+ # Run initialization tasks
102
+ self._initialize_resources()
103
+
104
+ def stop(self) -> None:
105
+ """Stop the LocalStack container."""
106
+ if self._container:
107
+ self._container.stop()
108
+ self._container = None
109
+ self._clients.clear()
110
+ self._service_managers.clear()
111
+
112
+ def _init_service_managers(self) -> None:
113
+ """Initialize service manager instances."""
114
+ if 's3' in self.services:
115
+ self._service_managers['s3'] = S3ServiceManager(self)
116
+
117
+ if 'dynamodb' in self.services:
118
+ self._service_managers['dynamodb'] = DynamoDBServiceManager(self)
119
+
120
+ if 'sqs' in self.services:
121
+ self._service_managers['sqs'] = SQSServiceManager(self)
122
+
123
+ if 'kinesis' in self.services:
124
+ self._service_managers['kinesis'] = KinesisServiceManager(self)
125
+
126
+ if 'lambda' in self.services:
127
+ self._service_managers['lambda'] = LambdaServiceManager(self)
128
+
129
+ if 'sns' in self.services:
130
+ self._service_managers['sns'] = SNSServiceManager(self)
131
+
132
+ def _initialize_resources(self) -> None:
133
+ """Initialize AWS resources based on init_config."""
134
+ # Initialize S3 buckets
135
+ if 's3_buckets' in self.init_config and 's3' in self._service_managers:
136
+ for bucket in self.init_config['s3_buckets']:
137
+ if isinstance(bucket, str):
138
+ self._service_managers['s3'].create_bucket(bucket)
139
+ elif isinstance(bucket, dict):
140
+ self._service_managers['s3'].create_bucket(**bucket)
141
+
142
+ # Initialize DynamoDB tables
143
+ if 'dynamodb_tables' in self.init_config and 'dynamodb' in self._service_managers:
144
+ for table in self.init_config['dynamodb_tables']:
145
+ self._service_managers['dynamodb'].create_table(**table)
146
+
147
+ # Initialize SQS queues
148
+ if 'sqs_queues' in self.init_config and 'sqs' in self._service_managers:
149
+ for queue in self.init_config['sqs_queues']:
150
+ if isinstance(queue, str):
151
+ self._service_managers['sqs'].create_queue(queue)
152
+ elif isinstance(queue, dict):
153
+ self._service_managers['sqs'].create_queue(**queue)
154
+
155
+ # Initialize Kinesis streams
156
+ if 'kinesis_streams' in self.init_config and 'kinesis' in self._service_managers:
157
+ for stream in self.init_config['kinesis_streams']:
158
+ if isinstance(stream, str):
159
+ self._service_managers['kinesis'].create_stream(stream)
160
+ elif isinstance(stream, dict):
161
+ self._service_managers['kinesis'].create_stream(**stream)
162
+
163
+ # Initialize SNS topics
164
+ if 'sns_topics' in self.init_config and 'sns' in self._service_managers:
165
+ for topic in self.init_config['sns_topics']:
166
+ if isinstance(topic, str):
167
+ self._service_managers['sns'].create_topic(topic)
168
+ elif isinstance(topic, dict):
169
+ self._service_managers['sns'].create_topic(**topic)
170
+
171
+ # Initialize Lambda functions
172
+ if 'lambda_functions' in self.init_config and 'lambda' in self._service_managers:
173
+ for function in self.init_config['lambda_functions']:
174
+ self._service_managers['lambda'].create_function(**function)
175
+
176
+ def get_client(self, service_name: str) -> Any:
177
+ """
178
+ Get a boto3 client for the specified AWS service.
179
+
180
+ Args:
181
+ service_name: AWS service name (e.g., 's3', 'dynamodb')
182
+
183
+ Returns:
184
+ Configured boto3 client for the service
185
+ """
186
+ if service_name not in self._clients:
187
+ if not self._container:
188
+ raise RuntimeError("Container not started. Use 'with' statement or call start().")
189
+
190
+ endpoint_url = self._container.get_url()
191
+
192
+ self._clients[service_name] = boto3.client(
193
+ service_name,
194
+ endpoint_url=endpoint_url,
195
+ aws_access_key_id='test',
196
+ aws_secret_access_key='test',
197
+ region_name='us-east-1',
198
+ )
199
+
200
+ return self._clients[service_name]
201
+
202
+ def get_s3_client(self):
203
+ """Get pre-configured S3 client."""
204
+ return self.get_client('s3')
205
+
206
+ def get_dynamodb_client(self):
207
+ """Get pre-configured DynamoDB client."""
208
+ return self.get_client('dynamodb')
209
+
210
+ def get_sqs_client(self):
211
+ """Get pre-configured SQS client."""
212
+ return self.get_client('sqs')
213
+
214
+ def get_kinesis_client(self):
215
+ """Get pre-configured Kinesis client."""
216
+ return self.get_client('kinesis')
217
+
218
+ def get_sns_client(self):
219
+ """Get pre-configured SNS client."""
220
+ return self.get_client('sns')
221
+
222
+ def get_lambda_client(self):
223
+ """Get pre-configured Lambda client."""
224
+ return self.get_client('lambda')
225
+
226
+ @property
227
+ def s3(self):
228
+ """Access S3 service manager with helper methods."""
229
+ return self._service_managers.get('s3')
230
+
231
+ @property
232
+ def dynamodb(self):
233
+ """Access DynamoDB service manager with helper methods."""
234
+ return self._service_managers.get('dynamodb')
235
+
236
+ @property
237
+ def sqs(self):
238
+ """Access SQS service manager with helper methods."""
239
+ return self._service_managers.get('sqs')
240
+
241
+ @property
242
+ def kinesis(self):
243
+ """Access Kinesis service manager with helper methods."""
244
+ return self._service_managers.get('kinesis')
245
+
246
+ @property
247
+ def lambda_(self):
248
+ """Access Lambda service manager with helper methods."""
249
+ return self._service_managers.get('lambda')
250
+
251
+ @property
252
+ def sns(self):
253
+ """Access SNS service manager with helper methods."""
254
+ return self._service_managers.get('sns')
255
+
256
+ @property
257
+ def endpoint_url(self) -> str:
258
+ """Get the LocalStack endpoint URL."""
259
+ if not self._container:
260
+ raise RuntimeError("Container not started")
261
+ return self._container.get_url()
@@ -0,0 +1,291 @@
1
+ """Pytest fixtures for AWS testing with testcontainers."""
2
+
3
+ import pytest
4
+ from testcontainers_aws.core import AWSTestEnvironment
5
+
6
+
7
+ @pytest.fixture(scope='session')
8
+ def aws_environment():
9
+ """
10
+ Session-scoped AWS test environment with common services.
11
+
12
+ Provides S3, DynamoDB, SQS, Kinesis, Lambda, and SNS services. The container is
13
+ shared across all tests in the session for performance.
14
+
15
+ Yields:
16
+ AWSTestEnvironment: Configured AWS environment
17
+ """
18
+ with AWSTestEnvironment(services=['s3', 'dynamodb', 'sqs', 'kinesis', 'lambda', 'sns']) as aws:
19
+ yield aws
20
+
21
+
22
+ @pytest.fixture(scope='function')
23
+ def aws_environment_function():
24
+ """
25
+ Function-scoped AWS test environment with common services.
26
+
27
+ Provides S3, DynamoDB, SQS, Kinesis, Lambda, and SNS services. A fresh container
28
+ is created for each test function for complete isolation.
29
+
30
+ Yields:
31
+ AWSTestEnvironment: Configured AWS environment
32
+ """
33
+ with AWSTestEnvironment(services=['s3', 'dynamodb', 'sqs', 'kinesis', 'lambda', 'sns']) as aws:
34
+ yield aws
35
+
36
+
37
+ @pytest.fixture(scope='session')
38
+ def s3_client(aws_environment):
39
+ """
40
+ Session-scoped S3 client.
41
+
42
+ Args:
43
+ aws_environment: AWS environment fixture
44
+
45
+ Returns:
46
+ boto3 S3 client configured for LocalStack
47
+ """
48
+ return aws_environment.get_s3_client()
49
+
50
+
51
+ @pytest.fixture(scope='session')
52
+ def dynamodb_client(aws_environment):
53
+ """
54
+ Session-scoped DynamoDB client.
55
+
56
+ Args:
57
+ aws_environment: AWS environment fixture
58
+
59
+ Returns:
60
+ boto3 DynamoDB client configured for LocalStack
61
+ """
62
+ return aws_environment.get_dynamodb_client()
63
+
64
+
65
+ @pytest.fixture(scope='session')
66
+ def sqs_client(aws_environment):
67
+ """
68
+ Session-scoped SQS client.
69
+
70
+ Args:
71
+ aws_environment: AWS environment fixture
72
+
73
+ Returns:
74
+ boto3 SQS client configured for LocalStack
75
+ """
76
+ return aws_environment.get_sqs_client()
77
+
78
+
79
+ @pytest.fixture(scope='session')
80
+ def kinesis_client(aws_environment):
81
+ """
82
+ Session-scoped Kinesis client.
83
+
84
+ Args:
85
+ aws_environment: AWS environment fixture
86
+
87
+ Returns:
88
+ boto3 Kinesis client configured for LocalStack
89
+ """
90
+ return aws_environment.get_kinesis_client()
91
+
92
+
93
+ @pytest.fixture(scope='session')
94
+ def lambda_client(aws_environment):
95
+ """
96
+ Session-scoped Lambda client.
97
+
98
+ Args:
99
+ aws_environment: AWS environment fixture
100
+
101
+ Returns:
102
+ boto3 Lambda client configured for LocalStack
103
+ """
104
+ return aws_environment.get_lambda_client()
105
+
106
+
107
+ @pytest.fixture(scope='session')
108
+ def sns_client(aws_environment):
109
+ """
110
+ Session-scoped SNS client.
111
+
112
+ Args:
113
+ aws_environment: AWS environment fixture
114
+
115
+ Returns:
116
+ boto3 SNS client configured for LocalStack
117
+ """
118
+ return aws_environment.get_sns_client()
119
+
120
+
121
+ @pytest.fixture
122
+ def s3_bucket(aws_environment):
123
+ """
124
+ Function-scoped S3 bucket that's automatically cleaned up.
125
+
126
+ Creates a test bucket and removes it after the test completes.
127
+
128
+ Args:
129
+ aws_environment: AWS environment fixture
130
+
131
+ Yields:
132
+ str: Bucket name
133
+ """
134
+ bucket_name = 'test-bucket'
135
+ aws_environment.s3.create_bucket(bucket_name)
136
+
137
+ yield bucket_name
138
+
139
+ # Cleanup
140
+ try:
141
+ aws_environment.s3.delete_bucket(bucket_name, force=True)
142
+ except Exception:
143
+ pass # Bucket might already be deleted
144
+
145
+
146
+ @pytest.fixture
147
+ def dynamodb_table(aws_environment):
148
+ """
149
+ Function-scoped DynamoDB table that's automatically cleaned up.
150
+
151
+ Creates a test table with a simple schema and removes it after the test.
152
+
153
+ Args:
154
+ aws_environment: AWS environment fixture
155
+
156
+ Yields:
157
+ str: Table name
158
+ """
159
+ table_name = 'test-table'
160
+ aws_environment.dynamodb.create_table(
161
+ name=table_name,
162
+ key_schema=[
163
+ {'AttributeName': 'id', 'KeyType': 'HASH'}
164
+ ],
165
+ attribute_definitions=[
166
+ {'AttributeName': 'id', 'AttributeType': 'S'}
167
+ ]
168
+ )
169
+
170
+ yield table_name
171
+
172
+ # Cleanup
173
+ try:
174
+ aws_environment.dynamodb.delete_table(table_name)
175
+ except Exception:
176
+ pass
177
+
178
+
179
+ @pytest.fixture
180
+ def sqs_queue(aws_environment):
181
+ """
182
+ Function-scoped SQS queue that's automatically cleaned up.
183
+
184
+ Creates a test queue and removes it after the test completes.
185
+
186
+ Args:
187
+ aws_environment: AWS environment fixture
188
+
189
+ Yields:
190
+ str: Queue URL
191
+ """
192
+ queue_response = aws_environment.sqs.create_queue('test-queue')
193
+ queue_url = queue_response['QueueUrl']
194
+
195
+ yield queue_url
196
+
197
+ # Cleanup
198
+ try:
199
+ aws_environment.sqs.delete_queue(queue_url)
200
+ except Exception:
201
+ pass
202
+
203
+
204
+ @pytest.fixture
205
+ def kinesis_stream(aws_environment):
206
+ """
207
+ Function-scoped Kinesis stream that's automatically cleaned up.
208
+
209
+ Creates a test stream and removes it after the test completes.
210
+
211
+ Args:
212
+ aws_environment: AWS environment fixture
213
+
214
+ Yields:
215
+ str: Stream name
216
+ """
217
+ stream_name = 'test-stream'
218
+ aws_environment.kinesis.create_stream(stream_name, shard_count=1)
219
+
220
+ yield stream_name
221
+
222
+ # Cleanup
223
+ try:
224
+ aws_environment.kinesis.delete_stream(stream_name)
225
+ except Exception:
226
+ pass
227
+
228
+
229
+ @pytest.fixture
230
+ def sns_topic(aws_environment):
231
+ """
232
+ Function-scoped SNS topic that's automatically cleaned up.
233
+
234
+ Creates a test topic and removes it after the test completes.
235
+
236
+ Args:
237
+ aws_environment: AWS environment fixture
238
+
239
+ Yields:
240
+ str: Topic ARN
241
+ """
242
+ response = aws_environment.sns.create_topic('test-topic')
243
+ topic_arn = response['TopicArn']
244
+
245
+ yield topic_arn
246
+
247
+ # Cleanup
248
+ try:
249
+ aws_environment.sns.delete_topic(topic_arn)
250
+ except Exception:
251
+ pass
252
+
253
+
254
+ @pytest.fixture
255
+ def lambda_function(aws_environment):
256
+ """
257
+ Function-scoped Lambda function that's automatically cleaned up.
258
+
259
+ Creates a simple test Lambda function and removes it after the test.
260
+
261
+ Args:
262
+ aws_environment: AWS environment fixture
263
+
264
+ Yields:
265
+ str: Function name
266
+ """
267
+ from testcontainers_aws.services.lambda_service import create_lambda_zip
268
+
269
+ function_name = 'test-function'
270
+ code = create_lambda_zip("""
271
+ def handler(event, context):
272
+ return {
273
+ 'statusCode': 200,
274
+ 'body': 'Hello from Lambda!'
275
+ }
276
+ """)
277
+
278
+ aws_environment.lambda_.create_function(
279
+ function_name=function_name,
280
+ runtime='python3.11',
281
+ handler='index.handler',
282
+ code=code
283
+ )
284
+
285
+ yield function_name
286
+
287
+ # Cleanup
288
+ try:
289
+ aws_environment.lambda_.delete_function(function_name)
290
+ except Exception:
291
+ pass
@@ -0,0 +1,17 @@
1
+ """AWS service managers for simplified resource management."""
2
+
3
+ from testcontainers_aws.services.s3 import S3ServiceManager
4
+ from testcontainers_aws.services.dynamodb import DynamoDBServiceManager
5
+ from testcontainers_aws.services.sqs import SQSServiceManager
6
+ from testcontainers_aws.services.kinesis import KinesisServiceManager
7
+ from testcontainers_aws.services.lambda_service import LambdaServiceManager
8
+ from testcontainers_aws.services.sns import SNSServiceManager
9
+
10
+ __all__ = [
11
+ "S3ServiceManager",
12
+ "DynamoDBServiceManager",
13
+ "SQSServiceManager",
14
+ "KinesisServiceManager",
15
+ "LambdaServiceManager",
16
+ "SNSServiceManager",
17
+ ]
@@ -0,0 +1,25 @@
1
+ """Base service manager for AWS services."""
2
+
3
+ from typing import TYPE_CHECKING, Any
4
+
5
+ if TYPE_CHECKING:
6
+ from testcontainers_aws.core import AWSTestEnvironment
7
+
8
+
9
+ class BaseServiceManager:
10
+ """Base class for AWS service managers."""
11
+
12
+ def __init__(self, environment: 'AWSTestEnvironment'):
13
+ """
14
+ Initialize service manager.
15
+
16
+ Args:
17
+ environment: Parent AWSTestEnvironment instance
18
+ """
19
+ self.environment = environment
20
+ self._client = None
21
+
22
+ @property
23
+ def client(self) -> Any:
24
+ """Get the boto3 client for this service (implemented by subclasses)."""
25
+ raise NotImplementedError("Subclasses must implement the client property")