samstack 0.1.0__tar.gz

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 (60) hide show
  1. samstack-0.1.0/.github/CODEOWNERS +1 -0
  2. samstack-0.1.0/.github/workflows/_ci.yml +131 -0
  3. samstack-0.1.0/.github/workflows/ci.yml +14 -0
  4. samstack-0.1.0/.github/workflows/claude.yml +39 -0
  5. samstack-0.1.0/.github/workflows/publish-pypi.yml +56 -0
  6. samstack-0.1.0/.github/workflows/publish-testpypi.yml +60 -0
  7. samstack-0.1.0/.github/workflows/release-please.yml +23 -0
  8. samstack-0.1.0/.gitignore +10 -0
  9. samstack-0.1.0/.python-version +1 -0
  10. samstack-0.1.0/CHANGELOG.md +26 -0
  11. samstack-0.1.0/CLAUDE.md +162 -0
  12. samstack-0.1.0/CONTRIBUTING.md +243 -0
  13. samstack-0.1.0/LICENSE +21 -0
  14. samstack-0.1.0/PKG-INFO +456 -0
  15. samstack-0.1.0/README.md +431 -0
  16. samstack-0.1.0/docs/superpowers/plans/2026-04-03-samstack-implementation.md +1698 -0
  17. samstack-0.1.0/docs/superpowers/specs/2026-04-03-samstack-design.md +291 -0
  18. samstack-0.1.0/docs/superpowers/specs/2026-04-09-localstack-resource-fixtures-design.md +192 -0
  19. samstack-0.1.0/pyproject.toml +57 -0
  20. samstack-0.1.0/src/samstack/__init__.py +18 -0
  21. samstack-0.1.0/src/samstack/_constants.py +6 -0
  22. samstack-0.1.0/src/samstack/_errors.py +38 -0
  23. samstack-0.1.0/src/samstack/_process.py +124 -0
  24. samstack-0.1.0/src/samstack/fixtures/__init__.py +0 -0
  25. samstack-0.1.0/src/samstack/fixtures/_sam_container.py +133 -0
  26. samstack-0.1.0/src/samstack/fixtures/localstack.py +92 -0
  27. samstack-0.1.0/src/samstack/fixtures/resources.py +376 -0
  28. samstack-0.1.0/src/samstack/fixtures/sam_api.py +47 -0
  29. samstack-0.1.0/src/samstack/fixtures/sam_build.py +95 -0
  30. samstack-0.1.0/src/samstack/fixtures/sam_lambda.py +74 -0
  31. samstack-0.1.0/src/samstack/plugin.py +94 -0
  32. samstack-0.1.0/src/samstack/py.typed +0 -0
  33. samstack-0.1.0/src/samstack/resources/__init__.py +6 -0
  34. samstack-0.1.0/src/samstack/resources/dynamodb.py +77 -0
  35. samstack-0.1.0/src/samstack/resources/s3.py +51 -0
  36. samstack-0.1.0/src/samstack/resources/sns.py +56 -0
  37. samstack-0.1.0/src/samstack/resources/sqs.py +55 -0
  38. samstack-0.1.0/src/samstack/settings.py +85 -0
  39. samstack-0.1.0/tests/conftest.py +51 -0
  40. samstack-0.1.0/tests/fixtures/hello_world/src/handler.py +43 -0
  41. samstack-0.1.0/tests/fixtures/hello_world/template.yaml +35 -0
  42. samstack-0.1.0/tests/integration/__init__.py +0 -0
  43. samstack-0.1.0/tests/integration/conftest.py +17 -0
  44. samstack-0.1.0/tests/integration/test_dynamodb_fixtures.py +72 -0
  45. samstack-0.1.0/tests/integration/test_s3_fixtures.py +67 -0
  46. samstack-0.1.0/tests/integration/test_sns_fixtures.py +75 -0
  47. samstack-0.1.0/tests/integration/test_sqs_fixtures.py +72 -0
  48. samstack-0.1.0/tests/test_errors.py +33 -0
  49. samstack-0.1.0/tests/test_localstack_integration.py +34 -0
  50. samstack-0.1.0/tests/test_process.py +43 -0
  51. samstack-0.1.0/tests/test_sam_api.py +20 -0
  52. samstack-0.1.0/tests/test_sam_build.py +17 -0
  53. samstack-0.1.0/tests/test_sam_lambda.py +31 -0
  54. samstack-0.1.0/tests/test_settings.py +65 -0
  55. samstack-0.1.0/tests/unit/__init__.py +0 -0
  56. samstack-0.1.0/tests/unit/test_dynamo_table.py +124 -0
  57. samstack-0.1.0/tests/unit/test_s3_bucket.py +126 -0
  58. samstack-0.1.0/tests/unit/test_sns_topic.py +93 -0
  59. samstack-0.1.0/tests/unit/test_sqs_queue.py +103 -0
  60. samstack-0.1.0/uv.lock +557 -0
@@ -0,0 +1 @@
1
+ * @PhishStick-hub
@@ -0,0 +1,131 @@
1
+ name: CI Pipeline
2
+
3
+ on:
4
+ workflow_call:
5
+ inputs:
6
+ ref:
7
+ description: 'Git ref to checkout'
8
+ required: false
9
+ type: string
10
+ default: ''
11
+ run-build:
12
+ description: 'Whether to run the build job'
13
+ required: false
14
+ type: boolean
15
+ default: false
16
+
17
+ jobs:
18
+ quality-checks:
19
+ name: Quality Checks
20
+ runs-on: ubuntu-latest
21
+ steps:
22
+ - name: Checkout code
23
+ uses: actions/checkout@v4
24
+ with:
25
+ ref: ${{ inputs.ref || github.ref }}
26
+
27
+ - name: Install uv
28
+ uses: astral-sh/setup-uv@v4
29
+ with:
30
+ enable-cache: true
31
+
32
+ - name: Set up Python
33
+ uses: actions/setup-python@v5
34
+ with:
35
+ python-version: "3.13"
36
+
37
+ - name: Install dependencies
38
+ run: uv sync --all-groups
39
+
40
+ - name: Check formatting with ruff
41
+ run: uv run ruff format --check .
42
+
43
+ - name: Lint with ruff
44
+ run: uv run ruff check .
45
+
46
+ - name: Type check with ty
47
+ run: uv run ty check
48
+
49
+ unit-tests:
50
+ name: Unit Tests
51
+ runs-on: ubuntu-latest
52
+ steps:
53
+ - name: Checkout code
54
+ uses: actions/checkout@v4
55
+ with:
56
+ ref: ${{ inputs.ref || github.ref }}
57
+
58
+ - name: Install uv
59
+ uses: astral-sh/setup-uv@v4
60
+ with:
61
+ enable-cache: true
62
+
63
+ - name: Set up Python
64
+ uses: actions/setup-python@v5
65
+ with:
66
+ python-version: "3.13"
67
+
68
+ - name: Install dependencies
69
+ run: uv sync --all-groups
70
+
71
+ - name: Run unit tests
72
+ run: uv run pytest tests/unit/ tests/test_settings.py tests/test_process.py tests/test_errors.py -v
73
+
74
+ integration-tests:
75
+ name: Integration Tests
76
+ runs-on: ubuntu-latest
77
+ needs: [quality-checks]
78
+ timeout-minutes: 20
79
+ steps:
80
+ - name: Checkout code
81
+ uses: actions/checkout@v4
82
+ with:
83
+ ref: ${{ inputs.ref || github.ref }}
84
+
85
+ - name: Install uv
86
+ uses: astral-sh/setup-uv@v4
87
+ with:
88
+ enable-cache: true
89
+
90
+ - name: Set up Python
91
+ uses: actions/setup-python@v5
92
+ with:
93
+ python-version: "3.13"
94
+
95
+ - name: Install dependencies
96
+ run: uv sync --all-groups
97
+
98
+ - name: Run integration tests
99
+ run: |
100
+ uv run pytest tests/ -v --timeout=300 \
101
+ --ignore=tests/unit \
102
+ --ignore=tests/test_settings.py \
103
+ --ignore=tests/test_process.py \
104
+ --ignore=tests/test_errors.py
105
+
106
+ build:
107
+ name: Build Package
108
+ if: ${{ inputs.run-build }}
109
+ runs-on: ubuntu-latest
110
+ needs: [quality-checks, unit-tests, integration-tests]
111
+ steps:
112
+ - name: Checkout code
113
+ uses: actions/checkout@v4
114
+ with:
115
+ ref: ${{ inputs.ref || github.ref }}
116
+
117
+ - name: Install uv
118
+ uses: astral-sh/setup-uv@v4
119
+ with:
120
+ enable-cache: true
121
+
122
+ - name: Set up Python
123
+ uses: actions/setup-python@v5
124
+ with:
125
+ python-version: "3.13"
126
+
127
+ - name: Install dependencies
128
+ run: uv sync --all-groups
129
+
130
+ - name: Build package
131
+ run: uv build
@@ -0,0 +1,14 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+ workflow_dispatch:
9
+
10
+ jobs:
11
+ ci:
12
+ uses: ./.github/workflows/_ci.yml
13
+ with:
14
+ run-build: ${{ github.event_name == 'pull_request' }}
@@ -0,0 +1,39 @@
1
+ name: Claude Code
2
+
3
+ on:
4
+ issue_comment:
5
+ types: [created]
6
+ pull_request_review_comment:
7
+ types: [created]
8
+ issues:
9
+ types: [opened, assigned]
10
+ pull_request_review:
11
+ types: [submitted]
12
+
13
+ jobs:
14
+ claude:
15
+ if: |
16
+ (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
17
+ (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
18
+ (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
19
+ (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
20
+ runs-on: ubuntu-latest
21
+ permissions:
22
+ contents: read
23
+ pull-requests: read
24
+ issues: read
25
+ id-token: write
26
+ actions: read
27
+ steps:
28
+ - name: Checkout repository
29
+ uses: actions/checkout@v4
30
+ with:
31
+ fetch-depth: 1
32
+
33
+ - name: Run Claude Code
34
+ id: claude
35
+ uses: anthropics/claude-code-action@v1
36
+ with:
37
+ claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
38
+ additional_permissions: |
39
+ actions: read
@@ -0,0 +1,56 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - "v[0-9]+.[0-9]+.[0-9]+"
7
+ workflow_dispatch:
8
+ inputs:
9
+ tag:
10
+ description: 'Git tag to publish (e.g., v0.1.0)'
11
+ required: true
12
+ type: string
13
+
14
+ jobs:
15
+ ci:
16
+ uses: ./.github/workflows/_ci.yml
17
+ with:
18
+ ref: ${{ github.event.inputs.tag || github.ref }}
19
+ run-build: true
20
+
21
+ publish:
22
+ name: Build and Publish to Production PyPI
23
+ runs-on: ubuntu-latest
24
+ needs: [ci]
25
+ permissions:
26
+ contents: write
27
+ environment:
28
+ name: pypi
29
+ url: https://pypi.org/project/samstack/
30
+ steps:
31
+ - name: Checkout code
32
+ uses: actions/checkout@v4
33
+ with:
34
+ ref: ${{ github.event.inputs.tag || github.ref }}
35
+
36
+ - name: Install uv
37
+ uses: astral-sh/setup-uv@v4
38
+ with:
39
+ enable-cache: true
40
+
41
+ - name: Set up Python
42
+ uses: actions/setup-python@v5
43
+ with:
44
+ python-version: "3.13"
45
+
46
+ - name: Install dependencies
47
+ run: uv sync --all-groups
48
+
49
+ - name: Build package
50
+ run: uv build
51
+
52
+ - name: Publish to PyPI
53
+ env:
54
+ UV_PUBLISH_TOKEN: ${{ secrets.PYPI_TOKEN }}
55
+ run: uv publish
56
+
@@ -0,0 +1,60 @@
1
+ name: Publish to TestPyPI
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - "release/**"
7
+ workflow_dispatch:
8
+
9
+ jobs:
10
+ ci:
11
+ uses: ./.github/workflows/_ci.yml
12
+ with:
13
+ run-build: true
14
+
15
+ publish:
16
+ name: Build and Publish to TestPyPI
17
+ runs-on: ubuntu-latest
18
+ needs: [ci]
19
+ if: needs.ci.result == 'success'
20
+ environment:
21
+ name: testpypi
22
+ url: https://test.pypi.org/project/samstack/
23
+ steps:
24
+ - name: Checkout code
25
+ uses: actions/checkout@v4
26
+ with:
27
+ fetch-depth: 0
28
+
29
+ - name: Install uv
30
+ uses: astral-sh/setup-uv@v4
31
+ with:
32
+ enable-cache: true
33
+
34
+ - name: Set up Python
35
+ uses: actions/setup-python@v5
36
+ with:
37
+ python-version: "3.13"
38
+
39
+ - name: Install dependencies
40
+ run: uv sync --all-groups
41
+
42
+ - name: Set dev version
43
+ run: |
44
+ BASE=$(grep -Po 'version = "\K[^"]*' pyproject.toml)
45
+ N=$(git rev-list HEAD --count)
46
+ DEV_VERSION="${BASE}.dev${N}"
47
+ sed -i "s/version = \"${BASE}\"/version = \"${DEV_VERSION}\"/" pyproject.toml
48
+ echo "DEV_VERSION=${DEV_VERSION}" >> $GITHUB_ENV
49
+ echo "Building version: ${DEV_VERSION}"
50
+
51
+ - name: Build package
52
+ run: uv build
53
+
54
+ - name: Publish to TestPyPI
55
+ env:
56
+ UV_PUBLISH_TOKEN: ${{ secrets.PYPI_TEST_TOKEN }}
57
+ run: uv publish --publish-url https://test.pypi.org/legacy/
58
+
59
+ - name: Print TestPyPI link
60
+ run: echo "Published to https://test.pypi.org/project/samstack/${{ env.DEV_VERSION }}/"
@@ -0,0 +1,23 @@
1
+ name: Release Please
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - main
7
+
8
+ permissions:
9
+ contents: write
10
+ pull-requests: write
11
+
12
+ jobs:
13
+ release-please:
14
+ name: Release Please
15
+ runs-on: ubuntu-latest
16
+ outputs:
17
+ release_created: ${{ steps.release.outputs.release_created }}
18
+ tag_name: ${{ steps.release.outputs.tag_name }}
19
+ steps:
20
+ - uses: googleapis/release-please-action@v4
21
+ id: release
22
+ with:
23
+ release-type: python
@@ -0,0 +1,10 @@
1
+ # Python-generated files
2
+ __pycache__/
3
+ *.py[oc]
4
+ build/
5
+ dist/
6
+ wheels/
7
+ *.egg-info
8
+
9
+ # Virtual environments
10
+ .venv
@@ -0,0 +1 @@
1
+ 3.13
@@ -0,0 +1,26 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0 (2026-04-12)
4
+
5
+
6
+ ### Features
7
+
8
+ * **ci:** add release-please workflow for automated releases ([a0d6411](https://github.com/PhishStick-hub/samstack/commit/a0d6411a7b7f76c16ed22fd29cf5bddd648635dc))
9
+ * initial release of samstack pytest plugin ([89f855a](https://github.com/PhishStick-hub/samstack/commit/89f855af2b6702a436bcf7b85800f230f672a774))
10
+ * **release:** auto-increment dev version per commit via hatch-vcs local_scheme ([eaf0897](https://github.com/PhishStick-hub/samstack/commit/eaf0897eaf45d77f8a58bdef4cdff0bb96d81530))
11
+ * **release:** dynamic versioning via hatch-vcs, switch pre-release tags to PEP 440 alpha format ([e6e3eac](https://github.com/PhishStick-hub/samstack/commit/e6e3eace30f330cc0e3282af383a8ec297dcc66e))
12
+
13
+
14
+ ### Bug Fixes
15
+
16
+ * **ci:** decouple ci from validate to prevent skip propagation in publish job ([6d35017](https://github.com/PhishStick-hub/samstack/commit/6d350172fce98a52b148c1772a7a71695d409f9b))
17
+ * **ci:** format hatch-vcs generated _version.py ([e98cc1b](https://github.com/PhishStick-hub/samstack/commit/e98cc1be305677b7fcae92bc8243f557d1606890))
18
+ * **ci:** restrict publish-pypi trigger to stable version tags only ([b7506d2](https://github.com/PhishStick-hub/samstack/commit/b7506d267f9a33bfafc0160e8e7395a264b40ad3))
19
+ * **fixtures:** pass --template to sam build and sam local commands ([e1ca8e9](https://github.com/PhishStick-hub/samstack/commit/e1ca8e909db686a09a5b62c7d6c5eb3c986453a7))
20
+ * **fixtures:** remove arm64 architecture from test template ([3be14d7](https://github.com/PhishStick-hub/samstack/commit/3be14d78bab55d35543d090ce140680101122692))
21
+ * **fixtures:** skip --skip-pull-image in CI so Lambda runtime image is pulled ([088d5ef](https://github.com/PhishStick-hub/samstack/commit/088d5efa1d3f2d3da6f486957a2d3c7f43b70a16))
22
+
23
+
24
+ ### Documentation
25
+
26
+ * add CONTRIBUTING.md with workflow and release guide ([50c3450](https://github.com/PhishStick-hub/samstack/commit/50c3450ff79a277fa2b7e71d51dc40a0a663a14f))
@@ -0,0 +1,162 @@
1
+ # CLAUDE.md
2
+
3
+ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
+
5
+ ## What this project is
6
+
7
+ `samstack` is a pytest plugin library (registered via `pytest11` entry point) that provides session-scoped fixtures for testing AWS Lambda functions locally. It runs SAM CLI and Lambda containers entirely inside Docker — no host `sam` install required. LocalStack provides the local AWS backend.
8
+
9
+ ## Commands
10
+
11
+ ```bash
12
+ # Install deps (including dev)
13
+ uv sync
14
+
15
+ # Lint
16
+ uv run ruff check .
17
+
18
+ # Format check (CI-safe)
19
+ uv run ruff format --check .
20
+
21
+ # Auto-fix formatting
22
+ uv run ruff format .
23
+
24
+ # Type check
25
+ uv run ty check
26
+
27
+ # All checks at once
28
+ uv run ruff check . && uv run ruff format --check . && uv run ty check
29
+
30
+ # Unit tests only (no Docker required)
31
+ uv run pytest tests/unit/ tests/test_settings.py tests/test_process.py tests/test_errors.py -v
32
+
33
+ # Single test
34
+ uv run pytest tests/test_settings.py::test_defaults_applied -v
35
+
36
+ # Full integration tests (requires Docker, pulls images on first run)
37
+ uv run pytest tests/ -v --timeout=300
38
+ ```
39
+
40
+ ## Architecture
41
+
42
+ ### Type checking gotchas
43
+
44
+ `ty` does **not** support `# type: ignore[...]` (mypy-only). Use `cast()` from `typing` for coercions ty can't infer. In unit test files, annotate mock parameters as `MagicMock` — annotating them as the real boto3 type causes ty to flag missing mock attributes.
45
+
46
+ ### LocalStack resource fixtures
47
+
48
+ `src/samstack/fixtures/resources.py` provides 12 fixtures for S3, DynamoDB, SQS, and SNS. Each service has three fixtures:
49
+
50
+ - `{service}_client` — session-scoped boto3 low-level client pointed at LocalStack
51
+ - `{service}_{resource}_factory` — session-scoped factory; call it with a name (and `keys` dict for DynamoDB) to get a wrapper instance with UUID suffix; all resources deleted at session teardown
52
+ - `{service}_{resource}` — function-scoped convenience fixture; one fresh resource per test, deleted after
53
+
54
+ Wrapper classes live in `src/samstack/resources/`:
55
+ - `S3Bucket` — `put(key, data)`, `get(key)`, `get_json(key)`, `delete(key)`, `list_keys(prefix)`, `.name`, `.client`
56
+ - `DynamoTable` — `put_item(item)`, `get_item(key)`, `delete_item(key)`, `query(key_condition, **kw)`, `scan()`, `.name`, `.client`
57
+ - `SqsQueue` — `send(body, **kw)`, `receive(max=10, wait=1)`, `purge()`, `.url`, `.client`
58
+ - `SnsTopic` — `publish(message, subject=None)`, `subscribe_sqs(queue_arn)`, `.arn`, `.client`
59
+
60
+ `DynamoTable` wraps `boto3.resource('dynamodb').Table` (high-level resource API) — item values are plain Python types, not `AttributeValueTypeDef` maps. `_dynamodb_resource` is an internal session fixture (prefixed `_`); it must be imported with `# noqa: F401` in `plugin.py`.
61
+
62
+ ### Fixture dependency chain
63
+
64
+ All fixtures are `scope="session"` unless noted. The dependency graph is:
65
+
66
+ ```
67
+ samstack_settings (no deps)
68
+ docker_network (no deps)
69
+ sam_env_vars → samstack_settings
70
+ localstack_container → samstack_settings, docker_network
71
+ localstack_endpoint → localstack_container
72
+ sam_build → samstack_settings, sam_env_vars
73
+ sam_api → samstack_settings, sam_build, docker_network, sam_api_extra_args
74
+ sam_lambda_endpoint → samstack_settings, sam_build, docker_network, sam_lambda_extra_args
75
+ lambda_client → samstack_settings, sam_lambda_endpoint
76
+
77
+ # Resource fixtures (all depend on localstack_endpoint + samstack_settings)
78
+ s3_client → localstack_endpoint, samstack_settings
79
+ s3_bucket_factory → s3_client
80
+ s3_bucket [func] → s3_client
81
+ dynamodb_client → localstack_endpoint, samstack_settings
82
+ _dynamodb_resource → localstack_endpoint, samstack_settings
83
+ dynamodb_table_factory → dynamodb_client, _dynamodb_resource
84
+ dynamodb_table [func] → dynamodb_client, _dynamodb_resource
85
+ sqs_client → localstack_endpoint, samstack_settings
86
+ sqs_queue_factory → sqs_client
87
+ sqs_queue [func] → sqs_client
88
+ sns_client → localstack_endpoint, samstack_settings
89
+ sns_topic_factory → sns_client
90
+ sns_topic [func] → sns_client
91
+ ```
92
+
93
+ `sam_build` intentionally does not depend on `localstack_container` — the build step doesn't need LocalStack running. The network dependency is implicit: `sam_api` and `sam_lambda_endpoint` bring in `docker_network`, which ensures network exists before SAM containers start.
94
+
95
+ ### How Docker networking works
96
+
97
+ 1. `docker_network` creates a named Docker bridge network (`samstack-{uuid8}`)
98
+ 2. `localstack_container` starts LocalStack, then connects it to that network with alias `localstack`
99
+ 3. SAM containers (start-api, start-lambda) join the same network via `.with_kwargs(network=docker_network)`
100
+ 4. Lambda code inside SAM reaches LocalStack at `http://localstack:4566` — injected via `sam_env_vars` as `AWS_ENDPOINT_URL`
101
+
102
+ ### SAM containers
103
+
104
+ Both `sam_api` and `sam_lambda_endpoint` use `testcontainers.core.container.DockerContainer`. The SAM image runs the CLI inside Docker (not on the host). Volume mounts:
105
+ - `{project_root}` → `{project_root}` (real host path — **not** `/var/task`)
106
+ - `/var/run/docker.sock` → `/var/run/docker.sock` (Docker-in-Docker for Lambda containers)
107
+
108
+ The project is mounted at its **real host path** (not `/var/task`) so that Lambda containers created by SAM via the Docker socket receive volume paths that Docker Desktop can resolve. The SAM container's `working_dir` is also set to this host path.
109
+
110
+ Default CLI flags on both commands: `--skip-pull-image --warm-containers LAZY --host 0.0.0.0 --port {port} --env-vars {host_path}/{log_dir}/env_vars.json --docker-network {network} --container-host host.docker.internal --container-host-interface 0.0.0.0`
111
+
112
+ - `--host 0.0.0.0` — bind Flask inside the container on all interfaces so Docker port-mapping works
113
+ - `--docker-network` — puts Lambda containers on the same network so they can reach LocalStack
114
+ - `--container-host host.docker.internal` — tells SAM to reach Lambda containers via Docker Desktop's host gateway (required when SAM runs inside Docker on macOS)
115
+ - `--container-host-interface 0.0.0.0` — binds Lambda container ports on all interfaces
116
+
117
+ `sam_api` uses `wait_for_http` (not `wait_for_port`) to wait for Flask to be ready. Docker Desktop's port forwarder starts listening before Flask binds the port, so a TCP-only probe would succeed too early and result in connection resets.
118
+
119
+ The `env_vars.json` file is written to `{project_root}/{log_dir}/` by `sam_build`. The host path is passed directly to `--env-vars` since the project is mounted at its real path.
120
+
121
+ ### Plugin registration
122
+
123
+ `plugin.py` is the `pytest11` entry point. It re-exports all fixtures from the four `fixtures/` modules so pytest discovers them automatically — child projects get all fixtures without any imports. `samstack_settings` is defined directly in `plugin.py` and searches upward from `Path.cwd()` for `pyproject.toml`.
124
+
125
+ ### Settings
126
+
127
+ `SamStackSettings` is a **frozen** dataclass parsed from `[tool.samstack]` in the child project's `pyproject.toml`. `sam_image` is the only required field (no default). Has a `docker_platform` property (`linux/arm64` / `linux/amd64`). Child projects override `samstack_settings` fixture in their `conftest.py` to supply settings programmatically, which is how the library's own tests work (see `tests/conftest.py`).
128
+
129
+ ### Overridable fixtures
130
+
131
+ These fixtures exist specifically to be overridden in child `conftest.py`:
132
+ - `samstack_settings` — swap the entire config
133
+ - `sam_env_vars` — extend or replace Lambda runtime env vars (dict is mutable; mutate it directly as shown in `tests/conftest.py`)
134
+ - `sam_api_extra_args` / `sam_lambda_extra_args` — append extra CLI flags
135
+ - `localstack_container`, `docker_network`, `localstack_endpoint` — swap infrastructure
136
+
137
+ ### Test fixture Lambda
138
+
139
+ `tests/fixtures/hello_world/` contains a minimal Lambda + `template.yaml` used by the library's own integration tests. It is not a Python package. The handler (`src/handler.py`) handles GET `/hello` → 200, POST `/hello` → writes to S3 → 201, direct invoke → 200. Tests in `tests/conftest.py` extend `sam_env_vars` to inject `TEST_BUCKET` before containers start.
140
+
141
+ ### `_process.py` utilities
142
+
143
+ - `wait_for_port` — TCP probe loop; raises `SamStartupError` with log tail on timeout
144
+ - `wait_for_http` — HTTP probe loop (any HTTP response = ready); used by `sam_api` because Docker Desktop's port forwarder accepts TCP before Flask starts, making a TCP-only probe succeed too early
145
+ - `stream_logs_to_file(container, log_path)` — daemon thread streaming container logs; accepts a Docker SDK container object (not an ID string)
146
+ - `run_one_shot_container` — runs a container to completion (used for `sam build`), returns `(logs, exit_code)`
147
+
148
+ `fixtures/_sam_container.py` — shared helpers for `sam_api` and `sam_lambda`: `build_sam_args()` (CLI arg list), `create_sam_container()` (full container builder), `_run_sam_service()` (context manager — starts a SAM container, streams logs, waits for readiness, yields endpoint URL, stops on exit), `DOCKER_SOCKET` constant. Edit this when changing how SAM containers are configured.
149
+
150
+ `_constants.py` — internal constants shared across fixtures: `LOCALSTACK_ACCESS_KEY` / `LOCALSTACK_SECRET_KEY` (both `"test"` — LocalStack's documented default). Import from here; do not re-define per-module.
151
+
152
+ Docker SDK (`import docker`) is imported lazily in `run_one_shot_container` to avoid import-time failures if Docker is not available.
153
+
154
+ ### Architecture and cross-platform support
155
+
156
+ `SamStackSettings` has an `architecture` field (`arm64` or `x86_64`) auto-detected from `platform.machine()`. This sets `DOCKER_DEFAULT_PLATFORM` (`linux/arm64` / `linux/amd64`) on SAM and build containers so the correct Lambda emulation image is pulled.
157
+
158
+ On Linux, `host.docker.internal` is not available by default. The fixtures add `--add-host host.docker.internal:host-gateway` to SAM containers on non-Darwin platforms.
159
+
160
+ ### Lambda container cleanup
161
+
162
+ SAM creates Lambda runtime containers (via the Docker socket) on `docker_network`. These are not tracked by testcontainers/Ryuk. The `docker_network` fixture teardown stops and removes all containers still connected to the network before destroying it.