samstack 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,376 @@
1
+ """
2
+ LocalStack resource fixtures for testing AWS services.
3
+
4
+ Provides session-scoped boto3 clients, session-scoped factory fixtures,
5
+ and function-scoped convenience fixtures for S3, DynamoDB, SQS, and SNS.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import warnings
11
+ from collections.abc import Callable, Iterator
12
+ from contextlib import contextmanager
13
+ from typing import TYPE_CHECKING
14
+ from uuid import uuid4
15
+
16
+ import boto3
17
+ import pytest
18
+
19
+ from samstack._constants import LOCALSTACK_ACCESS_KEY, LOCALSTACK_SECRET_KEY
20
+ from samstack.resources.dynamodb import DynamoTable
21
+ from samstack.resources.s3 import S3Bucket
22
+ from samstack.resources.sns import SnsTopic
23
+ from samstack.resources.sqs import SqsQueue
24
+ from samstack.settings import SamStackSettings
25
+
26
+
27
+ @contextmanager
28
+ def _safe_cleanup(description: str) -> Iterator[None]:
29
+ try:
30
+ yield
31
+ except Exception as exc:
32
+ warnings.warn(
33
+ f"samstack: failed to clean up {description}: {exc}",
34
+ stacklevel=1,
35
+ )
36
+
37
+
38
+ if TYPE_CHECKING:
39
+ from mypy_boto3_dynamodb import DynamoDBClient
40
+ from mypy_boto3_dynamodb.service_resource import DynamoDBServiceResource, Table
41
+ from mypy_boto3_s3 import S3Client
42
+ from mypy_boto3_sns import SNSClient
43
+ from mypy_boto3_sqs import SQSClient
44
+
45
+
46
+ # ---------------------------------------------------------------------------
47
+ # S3
48
+ # ---------------------------------------------------------------------------
49
+
50
+
51
+ @pytest.fixture(scope="session")
52
+ def s3_client(
53
+ localstack_endpoint: str,
54
+ samstack_settings: SamStackSettings,
55
+ ) -> S3Client:
56
+ """Session-scoped boto3 S3 client pointed at LocalStack."""
57
+ return boto3.client(
58
+ "s3",
59
+ endpoint_url=localstack_endpoint,
60
+ region_name=samstack_settings.region,
61
+ aws_access_key_id=LOCALSTACK_ACCESS_KEY,
62
+ aws_secret_access_key=LOCALSTACK_SECRET_KEY,
63
+ )
64
+
65
+
66
+ @pytest.fixture(scope="session")
67
+ def s3_bucket_factory(
68
+ s3_client: S3Client,
69
+ ) -> Iterator[Callable[[str], S3Bucket]]:
70
+ """
71
+ Session-scoped factory that creates S3Bucket instances.
72
+
73
+ Each call creates a uniquely named bucket. All buckets are deleted
74
+ at end of session.
75
+
76
+ Usage::
77
+
78
+ def test_something(s3_bucket_factory):
79
+ bucket = s3_bucket_factory("my-data")
80
+ bucket.put("key.json", {"value": 1})
81
+ """
82
+ created: list[S3Bucket] = []
83
+
84
+ def _create(name: str) -> S3Bucket:
85
+ actual = f"{name}-{uuid4().hex[:8]}"
86
+ s3_client.create_bucket(Bucket=actual)
87
+ bucket = S3Bucket(name=actual, client=s3_client)
88
+ created.append(bucket)
89
+ return bucket
90
+
91
+ yield _create
92
+
93
+ for bucket in created:
94
+ with _safe_cleanup(f"S3 bucket '{bucket.name}'"):
95
+ for key in bucket.list_keys():
96
+ bucket.delete(key)
97
+ s3_client.delete_bucket(Bucket=bucket.name)
98
+
99
+
100
+ @pytest.fixture
101
+ def s3_bucket(s3_client: S3Client) -> Iterator[S3Bucket]:
102
+ """
103
+ Function-scoped S3Bucket fixture. Fresh bucket per test, deleted after.
104
+
105
+ Usage::
106
+
107
+ def test_upload(s3_bucket):
108
+ s3_bucket.put("file.txt", b"hello")
109
+ assert s3_bucket.get("file.txt") == b"hello"
110
+ """
111
+ name = f"test-{uuid4().hex[:8]}"
112
+ s3_client.create_bucket(Bucket=name)
113
+ bucket = S3Bucket(name=name, client=s3_client)
114
+ yield bucket
115
+ with _safe_cleanup(f"S3 bucket '{name}'"):
116
+ for key in bucket.list_keys():
117
+ bucket.delete(key)
118
+ s3_client.delete_bucket(Bucket=name)
119
+
120
+
121
+ # ---------------------------------------------------------------------------
122
+ # DynamoDB
123
+ # ---------------------------------------------------------------------------
124
+
125
+
126
+ @pytest.fixture(scope="session")
127
+ def dynamodb_client(
128
+ localstack_endpoint: str,
129
+ samstack_settings: SamStackSettings,
130
+ ) -> DynamoDBClient:
131
+ """Session-scoped boto3 DynamoDB low-level client pointed at LocalStack."""
132
+ return boto3.client(
133
+ "dynamodb",
134
+ endpoint_url=localstack_endpoint,
135
+ region_name=samstack_settings.region,
136
+ aws_access_key_id=LOCALSTACK_ACCESS_KEY,
137
+ aws_secret_access_key=LOCALSTACK_SECRET_KEY,
138
+ )
139
+
140
+
141
+ @pytest.fixture(scope="session")
142
+ def _dynamodb_resource(
143
+ localstack_endpoint: str,
144
+ samstack_settings: SamStackSettings,
145
+ ) -> DynamoDBServiceResource:
146
+ """Session-scoped boto3 DynamoDB resource (high-level) for table wrappers."""
147
+ return boto3.resource(
148
+ "dynamodb",
149
+ endpoint_url=localstack_endpoint,
150
+ region_name=samstack_settings.region,
151
+ aws_access_key_id=LOCALSTACK_ACCESS_KEY,
152
+ aws_secret_access_key=LOCALSTACK_SECRET_KEY,
153
+ )
154
+
155
+
156
+ def _create_dynamo_table(
157
+ dynamodb_client: DynamoDBClient,
158
+ resource: DynamoDBServiceResource,
159
+ name: str,
160
+ keys: dict[str, str],
161
+ ) -> DynamoTable:
162
+ key_pairs = list(keys.items())
163
+ attr_defs = [{"AttributeName": k, "AttributeType": v} for k, v in key_pairs]
164
+ key_schema = [{"AttributeName": key_pairs[0][0], "KeyType": "HASH"}]
165
+ if len(key_pairs) > 1:
166
+ key_schema.append({"AttributeName": key_pairs[1][0], "KeyType": "RANGE"})
167
+ dynamodb_client.create_table(
168
+ TableName=name,
169
+ AttributeDefinitions=attr_defs,
170
+ KeySchema=key_schema,
171
+ BillingMode="PAY_PER_REQUEST",
172
+ )
173
+ table: Table = resource.Table(name)
174
+ return DynamoTable(name=name, table=table)
175
+
176
+
177
+ @pytest.fixture(scope="session")
178
+ def dynamodb_table_factory(
179
+ dynamodb_client: DynamoDBClient,
180
+ _dynamodb_resource: DynamoDBServiceResource,
181
+ ) -> Iterator[Callable[[str, dict[str, str]], DynamoTable]]:
182
+ """
183
+ Session-scoped factory that creates DynamoTable instances.
184
+
185
+ Each call creates a uniquely named table. All tables are deleted at end of session.
186
+ Uses the high-level resource API so item values are plain Python types.
187
+
188
+ Args:
189
+ name: Base name for the table (UUID suffix appended).
190
+ keys: Mapping of attribute name to type (``"S"``, ``"N"``, or ``"B"``).
191
+ First entry is the HASH key; second (if present) is the RANGE key.
192
+
193
+ Usage::
194
+
195
+ def test_something(dynamodb_table_factory):
196
+ table = dynamodb_table_factory("orders", {"order_id": "S"})
197
+ table.put_item({"order_id": "1", "total": 99})
198
+ """
199
+ created: list[str] = []
200
+
201
+ def _create(name: str, keys: dict[str, str]) -> DynamoTable:
202
+ actual = f"{name}-{uuid4().hex[:8]}"
203
+ table = _create_dynamo_table(dynamodb_client, _dynamodb_resource, actual, keys)
204
+ created.append(actual)
205
+ return table
206
+
207
+ yield _create
208
+
209
+ for table_name in created:
210
+ with _safe_cleanup(f"DynamoDB table '{table_name}'"):
211
+ dynamodb_client.delete_table(TableName=table_name)
212
+
213
+
214
+ @pytest.fixture
215
+ def dynamodb_table(
216
+ dynamodb_client: DynamoDBClient,
217
+ _dynamodb_resource: DynamoDBServiceResource,
218
+ ) -> Iterator[DynamoTable]:
219
+ """
220
+ Function-scoped DynamoTable fixture. Default key schema: ``{"id": "S"}``.
221
+ Fresh table per test, deleted after.
222
+
223
+ Usage::
224
+
225
+ def test_store(dynamodb_table):
226
+ dynamodb_table.put_item({"id": "1", "data": "x"})
227
+ assert dynamodb_table.get_item({"id": "1"})["data"] == "x"
228
+ """
229
+ name = f"test-{uuid4().hex[:8]}"
230
+ table = _create_dynamo_table(dynamodb_client, _dynamodb_resource, name, {"id": "S"})
231
+ yield table
232
+ with _safe_cleanup(f"DynamoDB table '{name}'"):
233
+ dynamodb_client.delete_table(TableName=name)
234
+
235
+
236
+ # ---------------------------------------------------------------------------
237
+ # SQS
238
+ # ---------------------------------------------------------------------------
239
+
240
+
241
+ @pytest.fixture(scope="session")
242
+ def sqs_client(
243
+ localstack_endpoint: str,
244
+ samstack_settings: SamStackSettings,
245
+ ) -> SQSClient:
246
+ """Session-scoped boto3 SQS client pointed at LocalStack."""
247
+ return boto3.client(
248
+ "sqs",
249
+ endpoint_url=localstack_endpoint,
250
+ region_name=samstack_settings.region,
251
+ aws_access_key_id=LOCALSTACK_ACCESS_KEY,
252
+ aws_secret_access_key=LOCALSTACK_SECRET_KEY,
253
+ )
254
+
255
+
256
+ @pytest.fixture(scope="session")
257
+ def sqs_queue_factory(
258
+ sqs_client: SQSClient,
259
+ ) -> Iterator[Callable[[str], SqsQueue]]:
260
+ """
261
+ Session-scoped factory that creates SqsQueue instances.
262
+
263
+ Each call creates a uniquely named queue. All queues are deleted at end of session.
264
+
265
+ Usage::
266
+
267
+ def test_something(sqs_queue_factory):
268
+ queue = sqs_queue_factory("jobs")
269
+ queue.send({"task": "process", "id": 1})
270
+ """
271
+ created: list[SqsQueue] = []
272
+
273
+ def _create(name: str) -> SqsQueue:
274
+ actual = f"{name}-{uuid4().hex[:8]}"
275
+ resp = sqs_client.create_queue(QueueName=actual)
276
+ queue = SqsQueue(url=resp["QueueUrl"], client=sqs_client)
277
+ created.append(queue)
278
+ return queue
279
+
280
+ yield _create
281
+
282
+ for queue in created:
283
+ with _safe_cleanup(f"SQS queue '{queue.url}'"):
284
+ sqs_client.delete_queue(QueueUrl=queue.url)
285
+
286
+
287
+ @pytest.fixture
288
+ def sqs_queue(sqs_client: SQSClient) -> Iterator[SqsQueue]:
289
+ """
290
+ Function-scoped SqsQueue fixture. Fresh queue per test, deleted after.
291
+
292
+ Usage::
293
+
294
+ def test_process(sqs_queue):
295
+ sqs_queue.send({"job": "run"})
296
+ messages = sqs_queue.receive()
297
+ assert len(messages) == 1
298
+ """
299
+ name = f"test-{uuid4().hex[:8]}"
300
+ resp = sqs_client.create_queue(QueueName=name)
301
+ queue = SqsQueue(url=resp["QueueUrl"], client=sqs_client)
302
+ yield queue
303
+ with _safe_cleanup(f"SQS queue '{name}'"):
304
+ sqs_client.delete_queue(QueueUrl=queue.url)
305
+
306
+
307
+ # ---------------------------------------------------------------------------
308
+ # SNS
309
+ # ---------------------------------------------------------------------------
310
+
311
+
312
+ @pytest.fixture(scope="session")
313
+ def sns_client(
314
+ localstack_endpoint: str,
315
+ samstack_settings: SamStackSettings,
316
+ ) -> SNSClient:
317
+ """Session-scoped boto3 SNS client pointed at LocalStack."""
318
+ return boto3.client(
319
+ "sns",
320
+ endpoint_url=localstack_endpoint,
321
+ region_name=samstack_settings.region,
322
+ aws_access_key_id=LOCALSTACK_ACCESS_KEY,
323
+ aws_secret_access_key=LOCALSTACK_SECRET_KEY,
324
+ )
325
+
326
+
327
+ @pytest.fixture(scope="session")
328
+ def sns_topic_factory(
329
+ sns_client: SNSClient,
330
+ ) -> Iterator[Callable[[str], SnsTopic]]:
331
+ """
332
+ Session-scoped factory that creates SnsTopic instances.
333
+
334
+ Each call creates a uniquely named topic. All topics are deleted at end of session.
335
+
336
+ Usage::
337
+
338
+ def test_something(sns_topic_factory, sqs_queue_factory):
339
+ topic = sns_topic_factory("notifications")
340
+ queue = sqs_queue_factory("inbox")
341
+ topic.subscribe_sqs(queue_arn)
342
+ topic.publish({"event": "created"})
343
+ """
344
+ created: list[SnsTopic] = []
345
+
346
+ def _create(name: str) -> SnsTopic:
347
+ actual = f"{name}-{uuid4().hex[:8]}"
348
+ resp = sns_client.create_topic(Name=actual)
349
+ topic = SnsTopic(arn=resp["TopicArn"], client=sns_client)
350
+ created.append(topic)
351
+ return topic
352
+
353
+ yield _create
354
+
355
+ for topic in created:
356
+ with _safe_cleanup(f"SNS topic '{topic.arn}'"):
357
+ sns_client.delete_topic(TopicArn=topic.arn)
358
+
359
+
360
+ @pytest.fixture
361
+ def sns_topic(sns_client: SNSClient) -> Iterator[SnsTopic]:
362
+ """
363
+ Function-scoped SnsTopic fixture. Fresh topic per test, deleted after.
364
+
365
+ Usage::
366
+
367
+ def test_notify(sns_topic):
368
+ msg_id = sns_topic.publish("hello")
369
+ assert isinstance(msg_id, str)
370
+ """
371
+ name = f"test-{uuid4().hex[:8]}"
372
+ resp = sns_client.create_topic(Name=name)
373
+ topic = SnsTopic(arn=resp["TopicArn"], client=sns_client)
374
+ yield topic
375
+ with _safe_cleanup(f"SNS topic '{name}'"):
376
+ sns_client.delete_topic(TopicArn=topic.arn)
@@ -0,0 +1,47 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Iterator
4
+
5
+ import pytest
6
+
7
+ from samstack.fixtures._sam_container import _run_sam_service
8
+ from samstack.settings import SamStackSettings
9
+
10
+
11
+ @pytest.fixture(scope="session")
12
+ def sam_api_extra_args() -> list[str]:
13
+ """
14
+ Extra CLI args appended to `sam local start-api` after the defaults.
15
+
16
+ Override in your conftest.py:
17
+
18
+ @pytest.fixture(scope="session")
19
+ def sam_api_extra_args() -> list[str]:
20
+ return ["--debug"]
21
+ """
22
+ return []
23
+
24
+
25
+ @pytest.fixture(scope="session")
26
+ def sam_api(
27
+ samstack_settings: SamStackSettings,
28
+ sam_build: None,
29
+ docker_network: str,
30
+ sam_api_extra_args: list[str],
31
+ ) -> Iterator[str]:
32
+ """
33
+ Start `sam local start-api` in Docker. Yields base URL http://127.0.0.1:{api_port}.
34
+ Logs written to {log_dir}/start-api.log.
35
+ """
36
+ with _run_sam_service(
37
+ settings=samstack_settings,
38
+ docker_network=docker_network,
39
+ subcommand="start-api",
40
+ port=samstack_settings.api_port,
41
+ warm_containers="LAZY",
42
+ settings_extra_args=samstack_settings.start_api_args,
43
+ fixture_extra_args=sam_api_extra_args,
44
+ log_filename="start-api.log",
45
+ wait_mode="http",
46
+ ) as endpoint:
47
+ yield endpoint
@@ -0,0 +1,95 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from pathlib import Path
5
+
6
+ import pytest
7
+
8
+ from samstack._constants import LOCALSTACK_ACCESS_KEY, LOCALSTACK_SECRET_KEY
9
+ from samstack._errors import SamBuildError
10
+ from samstack._process import run_one_shot_container
11
+ from samstack.fixtures._sam_container import DOCKER_SOCKET
12
+ from samstack.settings import SamStackSettings
13
+
14
+
15
+ def _add_gitignore_entry(project_root: Path, log_dir: str) -> None:
16
+ gitignore = project_root / ".gitignore"
17
+ entry = f"{log_dir}/"
18
+ if gitignore.exists():
19
+ content = gitignore.read_text()
20
+ if entry in content.splitlines():
21
+ return
22
+ gitignore.write_text(content.rstrip("\n") + f"\n{entry}\n")
23
+ else:
24
+ gitignore.write_text(f"{entry}\n")
25
+
26
+
27
+ @pytest.fixture(scope="session")
28
+ def sam_env_vars(samstack_settings: SamStackSettings) -> dict[str, dict[str, str]]:
29
+ """
30
+ Default environment variables injected into all Lambda functions at runtime.
31
+
32
+ Override in your conftest.py to add function-specific vars:
33
+
34
+ @pytest.fixture(scope="session")
35
+ def sam_env_vars(sam_env_vars):
36
+ sam_env_vars["MyFunction"] = {"MY_VAR": "value"}
37
+ return sam_env_vars
38
+ """
39
+ return {
40
+ "Parameters": {
41
+ "AWS_ENDPOINT_URL": "http://localstack:4566",
42
+ "AWS_DEFAULT_REGION": samstack_settings.region,
43
+ "AWS_ACCESS_KEY_ID": LOCALSTACK_ACCESS_KEY,
44
+ "AWS_SECRET_ACCESS_KEY": LOCALSTACK_SECRET_KEY,
45
+ }
46
+ }
47
+
48
+
49
+ @pytest.fixture(scope="session")
50
+ def sam_build(
51
+ samstack_settings: SamStackSettings,
52
+ sam_env_vars: dict[str, dict[str, str]],
53
+ ) -> None:
54
+ """
55
+ Run `sam build` in a one-shot Docker container. Runs once per test session.
56
+
57
+ The build output lands in {project_root}/.aws-sam/ and is reused by
58
+ sam_api and sam_lambda_endpoint fixtures.
59
+ """
60
+ log_dir = samstack_settings.project_root / samstack_settings.log_dir
61
+ log_dir.mkdir(parents=True, exist_ok=True)
62
+
63
+ # Write env vars JSON to a path accessible inside the SAM container
64
+ env_vars_path = log_dir / "env_vars.json"
65
+ env_vars_path.write_text(json.dumps(sam_env_vars, indent=2))
66
+
67
+ # Mount the project at its real host path so that Lambda containers
68
+ # created by SAM via the Docker socket can also mount it — Docker Desktop
69
+ # only shares /Users (and similar host paths), not paths inside containers.
70
+ host_path = str(samstack_settings.project_root)
71
+ build_cmd = [
72
+ "sam",
73
+ "build",
74
+ "--skip-pull-image",
75
+ "--template",
76
+ samstack_settings.template,
77
+ *samstack_settings.build_args,
78
+ ]
79
+ volumes = {
80
+ host_path: {"bind": host_path, "mode": "rw"},
81
+ DOCKER_SOCKET: {"bind": DOCKER_SOCKET, "mode": "rw"},
82
+ }
83
+
84
+ logs, exit_code = run_one_shot_container(
85
+ image=samstack_settings.sam_image,
86
+ command=build_cmd,
87
+ volumes=volumes,
88
+ working_dir=host_path,
89
+ environment={"DOCKER_DEFAULT_PLATFORM": samstack_settings.docker_platform},
90
+ )
91
+ if exit_code != 0:
92
+ raise SamBuildError(logs=logs)
93
+
94
+ if samstack_settings.add_gitignore:
95
+ _add_gitignore_entry(samstack_settings.project_root, samstack_settings.log_dir)
@@ -0,0 +1,74 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Iterator
4
+ from typing import TYPE_CHECKING
5
+
6
+ import boto3
7
+ import pytest
8
+
9
+ if TYPE_CHECKING:
10
+ from mypy_boto3_lambda import LambdaClient
11
+
12
+ from samstack._constants import LOCALSTACK_ACCESS_KEY, LOCALSTACK_SECRET_KEY
13
+ from samstack.fixtures._sam_container import _run_sam_service
14
+ from samstack.settings import SamStackSettings
15
+
16
+
17
+ @pytest.fixture(scope="session")
18
+ def sam_lambda_extra_args() -> list[str]:
19
+ """
20
+ Extra CLI args appended to `sam local start-lambda` after the defaults.
21
+
22
+ Override in your conftest.py:
23
+
24
+ @pytest.fixture(scope="session")
25
+ def sam_lambda_extra_args() -> list[str]:
26
+ return ["--debug"]
27
+ """
28
+ return []
29
+
30
+
31
+ @pytest.fixture(scope="session")
32
+ def sam_lambda_endpoint(
33
+ samstack_settings: SamStackSettings,
34
+ sam_build: None,
35
+ docker_network: str,
36
+ sam_lambda_extra_args: list[str],
37
+ ) -> Iterator[str]:
38
+ """
39
+ Start `sam local start-lambda` in Docker. Yields the endpoint URL
40
+ http://127.0.0.1:{lambda_port} for use with boto3 Lambda client.
41
+ Logs written to {log_dir}/start-lambda.log.
42
+ """
43
+ with _run_sam_service(
44
+ settings=samstack_settings,
45
+ docker_network=docker_network,
46
+ subcommand="start-lambda",
47
+ port=samstack_settings.lambda_port,
48
+ warm_containers="EAGER",
49
+ settings_extra_args=samstack_settings.start_lambda_args,
50
+ fixture_extra_args=sam_lambda_extra_args,
51
+ log_filename="start-lambda.log",
52
+ wait_mode="port",
53
+ ) as endpoint:
54
+ yield endpoint
55
+
56
+
57
+ @pytest.fixture(scope="session")
58
+ def lambda_client(
59
+ samstack_settings: SamStackSettings,
60
+ sam_lambda_endpoint: str,
61
+ ) -> LambdaClient:
62
+ """
63
+ Boto3 Lambda client pointed at the local SAM Lambda endpoint.
64
+
65
+ Use this to invoke functions directly without HTTP:
66
+ result = lambda_client.invoke(FunctionName="MyFunction", Payload=b"{}")
67
+ """
68
+ return boto3.client(
69
+ "lambda",
70
+ endpoint_url=sam_lambda_endpoint,
71
+ region_name=samstack_settings.region,
72
+ aws_access_key_id=LOCALSTACK_ACCESS_KEY,
73
+ aws_secret_access_key=LOCALSTACK_SECRET_KEY,
74
+ )
samstack/plugin.py ADDED
@@ -0,0 +1,94 @@
1
+ """
2
+ Pytest plugin entry point for samstack.
3
+
4
+ Registered via [project.entry-points."pytest11"] in pyproject.toml:
5
+ samstack = "samstack.plugin"
6
+
7
+ This module registers all fixtures and provides the samstack_settings fixture
8
+ by reading [tool.samstack] from the child project's pyproject.toml.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from pathlib import Path
14
+
15
+ import pytest
16
+
17
+ from samstack.fixtures.localstack import (
18
+ docker_network,
19
+ docker_network_name,
20
+ localstack_container,
21
+ localstack_endpoint,
22
+ )
23
+ from samstack.fixtures.resources import (
24
+ _dynamodb_resource, # noqa: F401 — internal fixture, must be registered for pytest discovery
25
+ dynamodb_client,
26
+ dynamodb_table,
27
+ dynamodb_table_factory,
28
+ s3_bucket,
29
+ s3_bucket_factory,
30
+ s3_client,
31
+ sns_client,
32
+ sns_topic,
33
+ sns_topic_factory,
34
+ sqs_client,
35
+ sqs_queue,
36
+ sqs_queue_factory,
37
+ )
38
+ from samstack.fixtures.sam_api import sam_api, sam_api_extra_args
39
+ from samstack.fixtures.sam_build import sam_build, sam_env_vars
40
+ from samstack.fixtures.sam_lambda import (
41
+ lambda_client,
42
+ sam_lambda_endpoint,
43
+ sam_lambda_extra_args,
44
+ )
45
+ from samstack.settings import SamStackSettings, load_settings
46
+
47
+ __all__ = [
48
+ "docker_network",
49
+ "docker_network_name",
50
+ "dynamodb_client",
51
+ "dynamodb_table",
52
+ "dynamodb_table_factory",
53
+ "lambda_client",
54
+ "localstack_container",
55
+ "localstack_endpoint",
56
+ "s3_bucket",
57
+ "s3_bucket_factory",
58
+ "s3_client",
59
+ "sam_api",
60
+ "sns_client",
61
+ "sns_topic",
62
+ "sns_topic_factory",
63
+ "sqs_client",
64
+ "sqs_queue",
65
+ "sqs_queue_factory",
66
+ "sam_api_extra_args",
67
+ "sam_build",
68
+ "sam_env_vars",
69
+ "sam_lambda_endpoint",
70
+ "sam_lambda_extra_args",
71
+ "samstack_settings",
72
+ ]
73
+
74
+
75
+ @pytest.fixture(scope="session")
76
+ def samstack_settings() -> SamStackSettings:
77
+ """
78
+ Load [tool.samstack] from the child project's pyproject.toml.
79
+
80
+ samstack searches upward from the current working directory for pyproject.toml.
81
+ Override this fixture to supply settings programmatically:
82
+
83
+ @pytest.fixture(scope="session")
84
+ def samstack_settings() -> SamStackSettings:
85
+ return SamStackSettings(sam_image="public.ecr.aws/sam/build-python3.13")
86
+ """
87
+ cwd = Path.cwd()
88
+ for parent in [cwd, *cwd.parents]:
89
+ candidate = parent / "pyproject.toml"
90
+ if candidate.exists():
91
+ return load_settings(parent)
92
+ raise FileNotFoundError(
93
+ "pyproject.toml not found. samstack requires [tool.samstack] in pyproject.toml."
94
+ )
samstack/py.typed ADDED
File without changes
@@ -0,0 +1,6 @@
1
+ from samstack.resources.dynamodb import DynamoTable
2
+ from samstack.resources.s3 import S3Bucket
3
+ from samstack.resources.sns import SnsTopic
4
+ from samstack.resources.sqs import SqsQueue
5
+
6
+ __all__ = ["DynamoTable", "S3Bucket", "SnsTopic", "SqsQueue"]