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.
- samstack-0.1.0/.github/CODEOWNERS +1 -0
- samstack-0.1.0/.github/workflows/_ci.yml +131 -0
- samstack-0.1.0/.github/workflows/ci.yml +14 -0
- samstack-0.1.0/.github/workflows/claude.yml +39 -0
- samstack-0.1.0/.github/workflows/publish-pypi.yml +56 -0
- samstack-0.1.0/.github/workflows/publish-testpypi.yml +60 -0
- samstack-0.1.0/.github/workflows/release-please.yml +23 -0
- samstack-0.1.0/.gitignore +10 -0
- samstack-0.1.0/.python-version +1 -0
- samstack-0.1.0/CHANGELOG.md +26 -0
- samstack-0.1.0/CLAUDE.md +162 -0
- samstack-0.1.0/CONTRIBUTING.md +243 -0
- samstack-0.1.0/LICENSE +21 -0
- samstack-0.1.0/PKG-INFO +456 -0
- samstack-0.1.0/README.md +431 -0
- samstack-0.1.0/docs/superpowers/plans/2026-04-03-samstack-implementation.md +1698 -0
- samstack-0.1.0/docs/superpowers/specs/2026-04-03-samstack-design.md +291 -0
- samstack-0.1.0/docs/superpowers/specs/2026-04-09-localstack-resource-fixtures-design.md +192 -0
- samstack-0.1.0/pyproject.toml +57 -0
- samstack-0.1.0/src/samstack/__init__.py +18 -0
- samstack-0.1.0/src/samstack/_constants.py +6 -0
- samstack-0.1.0/src/samstack/_errors.py +38 -0
- samstack-0.1.0/src/samstack/_process.py +124 -0
- samstack-0.1.0/src/samstack/fixtures/__init__.py +0 -0
- samstack-0.1.0/src/samstack/fixtures/_sam_container.py +133 -0
- samstack-0.1.0/src/samstack/fixtures/localstack.py +92 -0
- samstack-0.1.0/src/samstack/fixtures/resources.py +376 -0
- samstack-0.1.0/src/samstack/fixtures/sam_api.py +47 -0
- samstack-0.1.0/src/samstack/fixtures/sam_build.py +95 -0
- samstack-0.1.0/src/samstack/fixtures/sam_lambda.py +74 -0
- samstack-0.1.0/src/samstack/plugin.py +94 -0
- samstack-0.1.0/src/samstack/py.typed +0 -0
- samstack-0.1.0/src/samstack/resources/__init__.py +6 -0
- samstack-0.1.0/src/samstack/resources/dynamodb.py +77 -0
- samstack-0.1.0/src/samstack/resources/s3.py +51 -0
- samstack-0.1.0/src/samstack/resources/sns.py +56 -0
- samstack-0.1.0/src/samstack/resources/sqs.py +55 -0
- samstack-0.1.0/src/samstack/settings.py +85 -0
- samstack-0.1.0/tests/conftest.py +51 -0
- samstack-0.1.0/tests/fixtures/hello_world/src/handler.py +43 -0
- samstack-0.1.0/tests/fixtures/hello_world/template.yaml +35 -0
- samstack-0.1.0/tests/integration/__init__.py +0 -0
- samstack-0.1.0/tests/integration/conftest.py +17 -0
- samstack-0.1.0/tests/integration/test_dynamodb_fixtures.py +72 -0
- samstack-0.1.0/tests/integration/test_s3_fixtures.py +67 -0
- samstack-0.1.0/tests/integration/test_sns_fixtures.py +75 -0
- samstack-0.1.0/tests/integration/test_sqs_fixtures.py +72 -0
- samstack-0.1.0/tests/test_errors.py +33 -0
- samstack-0.1.0/tests/test_localstack_integration.py +34 -0
- samstack-0.1.0/tests/test_process.py +43 -0
- samstack-0.1.0/tests/test_sam_api.py +20 -0
- samstack-0.1.0/tests/test_sam_build.py +17 -0
- samstack-0.1.0/tests/test_sam_lambda.py +31 -0
- samstack-0.1.0/tests/test_settings.py +65 -0
- samstack-0.1.0/tests/unit/__init__.py +0 -0
- samstack-0.1.0/tests/unit/test_dynamo_table.py +124 -0
- samstack-0.1.0/tests/unit/test_s3_bucket.py +126 -0
- samstack-0.1.0/tests/unit/test_sns_topic.py +93 -0
- samstack-0.1.0/tests/unit/test_sqs_queue.py +103 -0
- 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,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 @@
|
|
|
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))
|
samstack-0.1.0/CLAUDE.md
ADDED
|
@@ -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.
|