local-web-services-python-sdk 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.
- local_web_services_python_sdk-0.1.0/.github/workflows/publish.yml +51 -0
- local_web_services_python_sdk-0.1.0/.gitignore +37 -0
- local_web_services_python_sdk-0.1.0/Makefile +17 -0
- local_web_services_python_sdk-0.1.0/PKG-INFO +96 -0
- local_web_services_python_sdk-0.1.0/README.md +69 -0
- local_web_services_python_sdk-0.1.0/pyproject.toml +83 -0
- local_web_services_python_sdk-0.1.0/src/lws_testing/__init__.py +26 -0
- local_web_services_python_sdk-0.1.0/src/lws_testing/_builders/__init__.py +1 -0
- local_web_services_python_sdk-0.1.0/src/lws_testing/_builders/chaos.py +56 -0
- local_web_services_python_sdk-0.1.0/src/lws_testing/_builders/iam.py +125 -0
- local_web_services_python_sdk-0.1.0/src/lws_testing/_builders/mock.py +107 -0
- local_web_services_python_sdk-0.1.0/src/lws_testing/_discovery/__init__.py +1 -0
- local_web_services_python_sdk-0.1.0/src/lws_testing/_discovery/cdk.py +115 -0
- local_web_services_python_sdk-0.1.0/src/lws_testing/_discovery/hcl.py +187 -0
- local_web_services_python_sdk-0.1.0/src/lws_testing/_logs.py +104 -0
- local_web_services_python_sdk-0.1.0/src/lws_testing/_resources/__init__.py +1 -0
- local_web_services_python_sdk-0.1.0/src/lws_testing/_resources/dynamodb.py +68 -0
- local_web_services_python_sdk-0.1.0/src/lws_testing/_resources/s3.py +69 -0
- local_web_services_python_sdk-0.1.0/src/lws_testing/_resources/sqs.py +76 -0
- local_web_services_python_sdk-0.1.0/src/lws_testing/_transport/__init__.py +1 -0
- local_web_services_python_sdk-0.1.0/src/lws_testing/_transport/inprocess.py +279 -0
- local_web_services_python_sdk-0.1.0/src/lws_testing/fixtures.py +99 -0
- local_web_services_python_sdk-0.1.0/src/lws_testing/session.py +330 -0
- local_web_services_python_sdk-0.1.0/tests/__init__.py +0 -0
- local_web_services_python_sdk-0.1.0/tests/unit/__init__.py +0 -0
- local_web_services_python_sdk-0.1.0/tests/unit/test_session_imports.py +50 -0
- local_web_services_python_sdk-0.1.0/tests/unit/test_session_lifecycle.py +104 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
|
|
7
|
+
permissions:
|
|
8
|
+
contents: read
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
build:
|
|
12
|
+
name: Build distribution
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v4
|
|
16
|
+
|
|
17
|
+
- name: Set up Python
|
|
18
|
+
uses: actions/setup-python@v5
|
|
19
|
+
with:
|
|
20
|
+
python-version: "3.11"
|
|
21
|
+
|
|
22
|
+
- name: Install uv
|
|
23
|
+
uses: astral-sh/setup-uv@v4
|
|
24
|
+
|
|
25
|
+
- name: Build wheel and sdist
|
|
26
|
+
run: uv build
|
|
27
|
+
|
|
28
|
+
- name: Upload build artifacts
|
|
29
|
+
uses: actions/upload-artifact@v4
|
|
30
|
+
with:
|
|
31
|
+
name: dist
|
|
32
|
+
path: dist/
|
|
33
|
+
|
|
34
|
+
publish:
|
|
35
|
+
name: Publish to PyPI
|
|
36
|
+
needs: build
|
|
37
|
+
runs-on: ubuntu-latest
|
|
38
|
+
environment:
|
|
39
|
+
name: pypi
|
|
40
|
+
url: https://pypi.org/p/local-web-services-python-sdk
|
|
41
|
+
permissions:
|
|
42
|
+
id-token: write # required for OIDC trusted publishing
|
|
43
|
+
steps:
|
|
44
|
+
- name: Download build artifacts
|
|
45
|
+
uses: actions/download-artifact@v4
|
|
46
|
+
with:
|
|
47
|
+
name: dist
|
|
48
|
+
path: dist/
|
|
49
|
+
|
|
50
|
+
- name: Publish to PyPI
|
|
51
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.pyo
|
|
5
|
+
*.pyd
|
|
6
|
+
*.egg
|
|
7
|
+
*.egg-info/
|
|
8
|
+
dist/
|
|
9
|
+
build/
|
|
10
|
+
.eggs/
|
|
11
|
+
|
|
12
|
+
# Virtual environments
|
|
13
|
+
.venv/
|
|
14
|
+
venv/
|
|
15
|
+
env/
|
|
16
|
+
|
|
17
|
+
# uv
|
|
18
|
+
uv.lock
|
|
19
|
+
|
|
20
|
+
# Testing
|
|
21
|
+
.pytest_cache/
|
|
22
|
+
.coverage
|
|
23
|
+
htmlcov/
|
|
24
|
+
.tox/
|
|
25
|
+
|
|
26
|
+
# IDE
|
|
27
|
+
.vscode/
|
|
28
|
+
.idea/
|
|
29
|
+
*.swp
|
|
30
|
+
*.swo
|
|
31
|
+
|
|
32
|
+
# macOS
|
|
33
|
+
.DS_Store
|
|
34
|
+
|
|
35
|
+
# Temp
|
|
36
|
+
*.tmp
|
|
37
|
+
lws_testing_*/
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
.PHONY: install check lint format test
|
|
2
|
+
|
|
3
|
+
install:
|
|
4
|
+
uv sync --all-groups
|
|
5
|
+
|
|
6
|
+
lint:
|
|
7
|
+
uv run ruff check src/ tests/
|
|
8
|
+
uv run pylint src/
|
|
9
|
+
|
|
10
|
+
format:
|
|
11
|
+
uv run black src/ tests/
|
|
12
|
+
uv run ruff check --fix src/ tests/
|
|
13
|
+
|
|
14
|
+
test:
|
|
15
|
+
uv run pytest tests/ -v
|
|
16
|
+
|
|
17
|
+
check: lint test
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: local-web-services-python-sdk
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python testing SDK for local-web-services — in-process pytest fixtures and boto3 helpers
|
|
5
|
+
Project-URL: Homepage, https://local-web-services.github.io/www
|
|
6
|
+
Project-URL: Repository, https://github.com/local-web-services/local-web-services-sdk-python
|
|
7
|
+
Project-URL: Issues, https://github.com/local-web-services/local-web-services-sdk-python/issues
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
Keywords: aws,boto3,dynamodb,local,mocking,pytest,s3,sqs,testing
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Framework :: Pytest
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Topic :: Software Development :: Testing
|
|
19
|
+
Requires-Python: >=3.11
|
|
20
|
+
Requires-Dist: botocore>=1.34.0
|
|
21
|
+
Requires-Dist: httpx>=0.27.0
|
|
22
|
+
Requires-Dist: local-web-services>=0.17.2
|
|
23
|
+
Requires-Dist: pyyaml>=6.0
|
|
24
|
+
Provides-Extra: pytest
|
|
25
|
+
Requires-Dist: pytest>=8.0.0; extra == 'pytest'
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
# local-web-services-testing
|
|
29
|
+
|
|
30
|
+
Python testing SDK for [local-web-services](https://github.com/local-web-services/local-web-services) — in-process pytest fixtures and boto3 helpers for testing AWS applications without needing a running `ldk dev`.
|
|
31
|
+
|
|
32
|
+
## Installation
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pip install local-web-services-python-sdk
|
|
36
|
+
# or
|
|
37
|
+
uv add local-web-services-python-sdk
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Quick start
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
from lws_testing import LwsSession
|
|
44
|
+
|
|
45
|
+
# Auto-discover resources from a CDK project
|
|
46
|
+
with LwsSession.from_cdk("../my-cdk-project") as session:
|
|
47
|
+
dynamo = session.client("dynamodb")
|
|
48
|
+
dynamo.put_item(TableName="Orders", Item={"id": {"S": "1"}})
|
|
49
|
+
|
|
50
|
+
# Auto-discover resources from a Terraform project
|
|
51
|
+
with LwsSession.from_hcl("../my-terraform-project") as session:
|
|
52
|
+
s3 = session.client("s3")
|
|
53
|
+
s3.put_object(Bucket="my-bucket", Key="test.txt", Body=b"hello")
|
|
54
|
+
|
|
55
|
+
# Explicit resource declaration
|
|
56
|
+
with LwsSession(
|
|
57
|
+
tables=[{"name": "Orders", "partition_key": "id"}],
|
|
58
|
+
queues=["OrderQueue"],
|
|
59
|
+
buckets=["ReceiptsBucket"],
|
|
60
|
+
) as session:
|
|
61
|
+
table = session.dynamodb("Orders")
|
|
62
|
+
table.put({"id": {"S": "1"}, "status": {"S": "pending"}})
|
|
63
|
+
items = table.scan()
|
|
64
|
+
assert len(items) == 1
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## pytest integration
|
|
68
|
+
|
|
69
|
+
The package registers pytest fixtures automatically via the `pytest11` entry point. Add a session fixture to your `conftest.py`:
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
# conftest.py
|
|
73
|
+
import pytest
|
|
74
|
+
|
|
75
|
+
@pytest.fixture(scope="session")
|
|
76
|
+
def lws_session_spec():
|
|
77
|
+
return {
|
|
78
|
+
"tables": [{"name": "Orders", "partition_key": "id"}],
|
|
79
|
+
"queues": ["OrderQueue"],
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Then use the `lws_session` fixture in your tests:
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
def test_create_order(lws_session):
|
|
87
|
+
client = lws_session.client("dynamodb")
|
|
88
|
+
client.put_item(TableName="Orders", Item={"id": {"S": "42"}})
|
|
89
|
+
|
|
90
|
+
table = lws_session.dynamodb("Orders")
|
|
91
|
+
table.assert_item_exists({"id": {"S": "42"}})
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## License
|
|
95
|
+
|
|
96
|
+
MIT
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# local-web-services-testing
|
|
2
|
+
|
|
3
|
+
Python testing SDK for [local-web-services](https://github.com/local-web-services/local-web-services) — in-process pytest fixtures and boto3 helpers for testing AWS applications without needing a running `ldk dev`.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install local-web-services-python-sdk
|
|
9
|
+
# or
|
|
10
|
+
uv add local-web-services-python-sdk
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Quick start
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
from lws_testing import LwsSession
|
|
17
|
+
|
|
18
|
+
# Auto-discover resources from a CDK project
|
|
19
|
+
with LwsSession.from_cdk("../my-cdk-project") as session:
|
|
20
|
+
dynamo = session.client("dynamodb")
|
|
21
|
+
dynamo.put_item(TableName="Orders", Item={"id": {"S": "1"}})
|
|
22
|
+
|
|
23
|
+
# Auto-discover resources from a Terraform project
|
|
24
|
+
with LwsSession.from_hcl("../my-terraform-project") as session:
|
|
25
|
+
s3 = session.client("s3")
|
|
26
|
+
s3.put_object(Bucket="my-bucket", Key="test.txt", Body=b"hello")
|
|
27
|
+
|
|
28
|
+
# Explicit resource declaration
|
|
29
|
+
with LwsSession(
|
|
30
|
+
tables=[{"name": "Orders", "partition_key": "id"}],
|
|
31
|
+
queues=["OrderQueue"],
|
|
32
|
+
buckets=["ReceiptsBucket"],
|
|
33
|
+
) as session:
|
|
34
|
+
table = session.dynamodb("Orders")
|
|
35
|
+
table.put({"id": {"S": "1"}, "status": {"S": "pending"}})
|
|
36
|
+
items = table.scan()
|
|
37
|
+
assert len(items) == 1
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## pytest integration
|
|
41
|
+
|
|
42
|
+
The package registers pytest fixtures automatically via the `pytest11` entry point. Add a session fixture to your `conftest.py`:
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
# conftest.py
|
|
46
|
+
import pytest
|
|
47
|
+
|
|
48
|
+
@pytest.fixture(scope="session")
|
|
49
|
+
def lws_session_spec():
|
|
50
|
+
return {
|
|
51
|
+
"tables": [{"name": "Orders", "partition_key": "id"}],
|
|
52
|
+
"queues": ["OrderQueue"],
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Then use the `lws_session` fixture in your tests:
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
def test_create_order(lws_session):
|
|
60
|
+
client = lws_session.client("dynamodb")
|
|
61
|
+
client.put_item(TableName="Orders", Item={"id": {"S": "42"}})
|
|
62
|
+
|
|
63
|
+
table = lws_session.dynamodb("Orders")
|
|
64
|
+
table.assert_item_exists({"id": {"S": "42"}})
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## License
|
|
68
|
+
|
|
69
|
+
MIT
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "local-web-services-python-sdk"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Python testing SDK for local-web-services — in-process pytest fixtures and boto3 helpers"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
requires-python = ">=3.11"
|
|
8
|
+
keywords = ["aws", "testing", "pytest", "boto3", "local", "dynamodb", "sqs", "s3", "mocking"]
|
|
9
|
+
classifiers = [
|
|
10
|
+
"Development Status :: 3 - Alpha",
|
|
11
|
+
"Intended Audience :: Developers",
|
|
12
|
+
"License :: OSI Approved :: MIT License",
|
|
13
|
+
"Programming Language :: Python :: 3",
|
|
14
|
+
"Programming Language :: Python :: 3.11",
|
|
15
|
+
"Programming Language :: Python :: 3.12",
|
|
16
|
+
"Programming Language :: Python :: 3.13",
|
|
17
|
+
"Topic :: Software Development :: Testing",
|
|
18
|
+
"Framework :: Pytest",
|
|
19
|
+
]
|
|
20
|
+
dependencies = [
|
|
21
|
+
"httpx>=0.27.0",
|
|
22
|
+
"botocore>=1.34.0",
|
|
23
|
+
"pyyaml>=6.0",
|
|
24
|
+
"local-web-services>=0.17.2",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
[project.optional-dependencies]
|
|
28
|
+
pytest = [
|
|
29
|
+
"pytest>=8.0.0",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
[project.urls]
|
|
33
|
+
Homepage = "https://local-web-services.github.io/www"
|
|
34
|
+
Repository = "https://github.com/local-web-services/local-web-services-sdk-python"
|
|
35
|
+
Issues = "https://github.com/local-web-services/local-web-services-sdk-python/issues"
|
|
36
|
+
|
|
37
|
+
[project.entry-points."pytest11"]
|
|
38
|
+
lws_testing = "lws_testing.fixtures"
|
|
39
|
+
|
|
40
|
+
[build-system]
|
|
41
|
+
requires = ["hatchling"]
|
|
42
|
+
build-backend = "hatchling.build"
|
|
43
|
+
|
|
44
|
+
[tool.hatch.build.targets.wheel]
|
|
45
|
+
packages = ["src/lws_testing"]
|
|
46
|
+
|
|
47
|
+
[tool.pytest.ini_options]
|
|
48
|
+
testpaths = ["tests"]
|
|
49
|
+
asyncio_mode = "auto"
|
|
50
|
+
|
|
51
|
+
[dependency-groups]
|
|
52
|
+
dev = [
|
|
53
|
+
"pytest>=8.0.0",
|
|
54
|
+
"pytest-asyncio>=0.23.0",
|
|
55
|
+
"boto3>=1.34.0",
|
|
56
|
+
"black>=24.0.0",
|
|
57
|
+
"ruff>=0.4.0",
|
|
58
|
+
"radon>=6.0.0",
|
|
59
|
+
]
|
|
60
|
+
|
|
61
|
+
[tool.black]
|
|
62
|
+
line-length = 100
|
|
63
|
+
target-version = ["py311"]
|
|
64
|
+
|
|
65
|
+
[tool.ruff]
|
|
66
|
+
line-length = 100
|
|
67
|
+
target-version = "py311"
|
|
68
|
+
|
|
69
|
+
[tool.ruff.lint]
|
|
70
|
+
select = ["E", "F", "I", "W", "UP", "C90"]
|
|
71
|
+
|
|
72
|
+
[tool.ruff.lint.isort]
|
|
73
|
+
known-first-party = ["lws_testing"]
|
|
74
|
+
|
|
75
|
+
[tool.ruff.lint.mccabe]
|
|
76
|
+
max-complexity = 10
|
|
77
|
+
|
|
78
|
+
[tool.pylint."messages control"]
|
|
79
|
+
disable = [
|
|
80
|
+
"E0401", "W0718", "R0801", "C0301", "C0302", "C0114", "C0115",
|
|
81
|
+
"R0903", "R0902", "R0913", "R0917", "R0904", "R0911", "R0914", "C0103",
|
|
82
|
+
"W0603", "W0621",
|
|
83
|
+
]
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""local-web-services testing SDK.
|
|
2
|
+
|
|
3
|
+
In-process pytest fixtures and boto3 helpers for testing AWS applications
|
|
4
|
+
against real LWS service implementations without needing a running ldk dev.
|
|
5
|
+
|
|
6
|
+
Usage::
|
|
7
|
+
|
|
8
|
+
from lws_testing import LwsSession
|
|
9
|
+
|
|
10
|
+
# Auto-detect CDK or HCL project in current directory
|
|
11
|
+
with LwsSession.from_cdk("../") as session: ...
|
|
12
|
+
with LwsSession.from_hcl("../") as session: ...
|
|
13
|
+
|
|
14
|
+
# Explicit resource declaration
|
|
15
|
+
with LwsSession(
|
|
16
|
+
tables=[{"name": "Orders", "partition_key": "id"}],
|
|
17
|
+
queues=["OrderQueue"],
|
|
18
|
+
buckets=["ReceiptsBucket"],
|
|
19
|
+
) as session: ...
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
from lws_testing.session import LwsSession
|
|
25
|
+
|
|
26
|
+
__all__ = ["LwsSession"]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Fluent builders for mock, chaos, and IAM configuration."""
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""Fluent builder for configuring chaos engineering rules."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import httpx
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ChaosBuilder:
|
|
9
|
+
"""Fluent builder for AWS service chaos configuration.
|
|
10
|
+
|
|
11
|
+
Usage::
|
|
12
|
+
|
|
13
|
+
session.chaos("dynamodb").error_rate(0.3).latency(min_ms=50, max_ms=200).apply()
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
def __init__(self, service: str, mgmt_port: int) -> None:
|
|
17
|
+
self._service = service
|
|
18
|
+
self._mgmt_port = mgmt_port
|
|
19
|
+
self._config: dict = {"enabled": True}
|
|
20
|
+
|
|
21
|
+
def error_rate(self, rate: float) -> "ChaosBuilder":
|
|
22
|
+
"""Set the fraction of requests that return an error (0.0–1.0)."""
|
|
23
|
+
self._config["error_rate"] = float(rate)
|
|
24
|
+
return self
|
|
25
|
+
|
|
26
|
+
def latency(self, min_ms: int = 0, max_ms: int = 0) -> "ChaosBuilder":
|
|
27
|
+
"""Inject artificial latency into responses."""
|
|
28
|
+
self._config["latency_min_ms"] = min_ms
|
|
29
|
+
self._config["latency_max_ms"] = max_ms
|
|
30
|
+
return self
|
|
31
|
+
|
|
32
|
+
def connection_reset_rate(self, rate: float) -> "ChaosBuilder":
|
|
33
|
+
"""Set the fraction of requests that receive a connection reset (0.0–1.0)."""
|
|
34
|
+
self._config["connection_reset_rate"] = float(rate)
|
|
35
|
+
return self
|
|
36
|
+
|
|
37
|
+
def timeout_rate(self, rate: float) -> "ChaosBuilder":
|
|
38
|
+
"""Set the fraction of requests that time out (0.0–1.0)."""
|
|
39
|
+
self._config["timeout_rate"] = float(rate)
|
|
40
|
+
return self
|
|
41
|
+
|
|
42
|
+
def apply(self) -> None:
|
|
43
|
+
"""Push the chaos configuration to the management API."""
|
|
44
|
+
httpx.post(
|
|
45
|
+
f"http://127.0.0.1:{self._mgmt_port}/_ldk/chaos",
|
|
46
|
+
json={self._service: self._config},
|
|
47
|
+
timeout=5.0,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
def clear(self) -> None:
|
|
51
|
+
"""Disable chaos for this service."""
|
|
52
|
+
httpx.post(
|
|
53
|
+
f"http://127.0.0.1:{self._mgmt_port}/_ldk/chaos",
|
|
54
|
+
json={self._service: {"enabled": False, "error_rate": 0.0}},
|
|
55
|
+
timeout=5.0,
|
|
56
|
+
)
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""Fluent builder for configuring IAM authorization at runtime."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class IamBuilder:
|
|
11
|
+
"""Fluent builder for runtime IAM auth configuration.
|
|
12
|
+
|
|
13
|
+
Usage::
|
|
14
|
+
|
|
15
|
+
# Switch to enforce mode
|
|
16
|
+
session.iam.mode("enforce").apply()
|
|
17
|
+
|
|
18
|
+
# Register a read-only identity
|
|
19
|
+
session.iam.identity("readonly").allow(["dynamodb:GetItem"]).apply()
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def __init__(self, mgmt_port: int) -> None:
|
|
23
|
+
self._mgmt_port = mgmt_port
|
|
24
|
+
self._updates: dict[str, Any] = {}
|
|
25
|
+
self._identities: dict[str, Any] = {}
|
|
26
|
+
|
|
27
|
+
def mode(self, mode: str) -> "IamBuilder":
|
|
28
|
+
"""Set the global IAM auth mode (``"enforce"``, ``"audit"``, ``"disabled"``)."""
|
|
29
|
+
self._updates["mode"] = mode
|
|
30
|
+
return self
|
|
31
|
+
|
|
32
|
+
def default_identity(self, name: str) -> "IamBuilder":
|
|
33
|
+
"""Set the default identity used when no identity header is present."""
|
|
34
|
+
self._updates["default_identity"] = name
|
|
35
|
+
return self
|
|
36
|
+
|
|
37
|
+
def identity(self, name: str) -> "_IdentityBuilder":
|
|
38
|
+
"""Start configuring a named identity."""
|
|
39
|
+
return _IdentityBuilder(self, name)
|
|
40
|
+
|
|
41
|
+
def _register_identity(self, name: str, config: dict[str, Any]) -> None:
|
|
42
|
+
self._identities[name] = config
|
|
43
|
+
|
|
44
|
+
def apply(self) -> None:
|
|
45
|
+
"""Push all pending IAM configuration to the management API."""
|
|
46
|
+
payload: dict[str, Any] = dict(self._updates)
|
|
47
|
+
if self._identities:
|
|
48
|
+
payload["identities"] = self._identities
|
|
49
|
+
httpx.post(
|
|
50
|
+
f"http://127.0.0.1:{self._mgmt_port}/_ldk/iam-auth",
|
|
51
|
+
json=payload,
|
|
52
|
+
timeout=5.0,
|
|
53
|
+
)
|
|
54
|
+
self._updates = {}
|
|
55
|
+
self._identities = {}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class _IdentityBuilder:
|
|
59
|
+
"""Configure a single named IAM identity."""
|
|
60
|
+
|
|
61
|
+
def __init__(self, parent: IamBuilder, name: str) -> None:
|
|
62
|
+
self._parent = parent
|
|
63
|
+
self._name = name
|
|
64
|
+
self._inline_policies: list[dict[str, Any]] = []
|
|
65
|
+
self._boundary: dict[str, Any] | None = None
|
|
66
|
+
|
|
67
|
+
def allow(self, actions: list[str], resource: str = "*") -> "_IdentityBuilder":
|
|
68
|
+
"""Add an Allow statement to this identity's inline policy."""
|
|
69
|
+
self._inline_policies.append(
|
|
70
|
+
{
|
|
71
|
+
"name": f"inline-{len(self._inline_policies)}",
|
|
72
|
+
"document": {
|
|
73
|
+
"Version": "2012-10-17",
|
|
74
|
+
"Statement": [
|
|
75
|
+
{
|
|
76
|
+
"Effect": "Allow",
|
|
77
|
+
"Action": actions,
|
|
78
|
+
"Resource": resource,
|
|
79
|
+
}
|
|
80
|
+
],
|
|
81
|
+
},
|
|
82
|
+
}
|
|
83
|
+
)
|
|
84
|
+
return self
|
|
85
|
+
|
|
86
|
+
def deny(self, actions: list[str], resource: str = "*") -> "_IdentityBuilder":
|
|
87
|
+
"""Add an explicit Deny statement to this identity's inline policy."""
|
|
88
|
+
self._inline_policies.append(
|
|
89
|
+
{
|
|
90
|
+
"name": f"inline-deny-{len(self._inline_policies)}",
|
|
91
|
+
"document": {
|
|
92
|
+
"Version": "2012-10-17",
|
|
93
|
+
"Statement": [
|
|
94
|
+
{
|
|
95
|
+
"Effect": "Deny",
|
|
96
|
+
"Action": actions,
|
|
97
|
+
"Resource": resource,
|
|
98
|
+
}
|
|
99
|
+
],
|
|
100
|
+
},
|
|
101
|
+
}
|
|
102
|
+
)
|
|
103
|
+
return self
|
|
104
|
+
|
|
105
|
+
def boundary(self, actions: list[str], resource: str = "*") -> "_IdentityBuilder":
|
|
106
|
+
"""Set a permissions boundary for this identity."""
|
|
107
|
+
self._boundary = {
|
|
108
|
+
"Version": "2012-10-17",
|
|
109
|
+
"Statement": [
|
|
110
|
+
{
|
|
111
|
+
"Effect": "Allow",
|
|
112
|
+
"Action": actions,
|
|
113
|
+
"Resource": resource,
|
|
114
|
+
}
|
|
115
|
+
],
|
|
116
|
+
}
|
|
117
|
+
return self
|
|
118
|
+
|
|
119
|
+
def apply(self) -> "IamBuilder":
|
|
120
|
+
"""Register the identity and return the parent builder for chaining."""
|
|
121
|
+
config: dict[str, Any] = {"inline_policies": self._inline_policies}
|
|
122
|
+
if self._boundary is not None:
|
|
123
|
+
config["boundary_policy"] = self._boundary
|
|
124
|
+
self._parent._register_identity(self._name, config)
|
|
125
|
+
return self._parent
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""Fluent builder for configuring AWS operation mocks."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class MockBuilder:
|
|
11
|
+
"""Fluent builder for AWS service mock responses.
|
|
12
|
+
|
|
13
|
+
Usage::
|
|
14
|
+
|
|
15
|
+
session.mock("dynamodb").operation("PutItem").respond(
|
|
16
|
+
status=500,
|
|
17
|
+
body={"__type": "ProvisionedThroughputExceededException"},
|
|
18
|
+
)
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(self, service: str, mgmt_port: int) -> None:
|
|
22
|
+
self._service = service
|
|
23
|
+
self._mgmt_port = mgmt_port
|
|
24
|
+
self._rules: list[dict[str, Any]] = []
|
|
25
|
+
|
|
26
|
+
def operation(self, operation_name: str) -> "_MockRuleBuilder":
|
|
27
|
+
"""Start building a mock rule for *operation_name*."""
|
|
28
|
+
return _MockRuleBuilder(self, operation_name)
|
|
29
|
+
|
|
30
|
+
def _add_rule(self, rule: dict[str, Any]) -> "MockBuilder":
|
|
31
|
+
self._rules.append(rule)
|
|
32
|
+
self._apply()
|
|
33
|
+
return self
|
|
34
|
+
|
|
35
|
+
def _apply(self) -> None:
|
|
36
|
+
"""Push the current rules to the management API."""
|
|
37
|
+
httpx.post(
|
|
38
|
+
f"http://127.0.0.1:{self._mgmt_port}/_ldk/aws-mock",
|
|
39
|
+
json={self._service: {"enabled": True, "rules": self._rules}},
|
|
40
|
+
timeout=5.0,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
def clear(self) -> None:
|
|
44
|
+
"""Remove all mock rules for this service."""
|
|
45
|
+
self._rules = []
|
|
46
|
+
httpx.post(
|
|
47
|
+
f"http://127.0.0.1:{self._mgmt_port}/_ldk/aws-mock",
|
|
48
|
+
json={self._service: {"enabled": False, "rules": []}},
|
|
49
|
+
timeout=5.0,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class _MockRuleBuilder:
|
|
54
|
+
"""Intermediate builder — configure a single mock rule."""
|
|
55
|
+
|
|
56
|
+
def __init__(self, parent: MockBuilder, operation: str) -> None:
|
|
57
|
+
self._parent = parent
|
|
58
|
+
self._operation = operation
|
|
59
|
+
self._match_headers: dict[str, str] = {}
|
|
60
|
+
|
|
61
|
+
def with_header(self, name: str, value: str) -> "_MockRuleBuilder":
|
|
62
|
+
"""Add an additional header match constraint."""
|
|
63
|
+
self._match_headers[name] = value
|
|
64
|
+
return self
|
|
65
|
+
|
|
66
|
+
def respond(
|
|
67
|
+
self,
|
|
68
|
+
status: int = 200,
|
|
69
|
+
body: str | dict[str, Any] = "",
|
|
70
|
+
content_type: str = "application/json",
|
|
71
|
+
delay_ms: int = 0,
|
|
72
|
+
) -> MockBuilder:
|
|
73
|
+
"""Set the response and register the rule.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
status: HTTP status code to return.
|
|
77
|
+
body: Response body (dict will be JSON-encoded).
|
|
78
|
+
content_type: Response Content-Type header.
|
|
79
|
+
delay_ms: Artificial delay before responding.
|
|
80
|
+
"""
|
|
81
|
+
import json
|
|
82
|
+
|
|
83
|
+
if isinstance(body, dict):
|
|
84
|
+
body_str = json.dumps(body)
|
|
85
|
+
else:
|
|
86
|
+
body_str = body
|
|
87
|
+
|
|
88
|
+
rule: dict[str, Any] = {
|
|
89
|
+
"operation": self._operation,
|
|
90
|
+
"match_headers": self._match_headers,
|
|
91
|
+
"response": {
|
|
92
|
+
"status": status,
|
|
93
|
+
"content_type": content_type,
|
|
94
|
+
"delay_ms": delay_ms,
|
|
95
|
+
},
|
|
96
|
+
}
|
|
97
|
+
if body_str:
|
|
98
|
+
rule["response"]["body"] = body_str
|
|
99
|
+
|
|
100
|
+
return self._parent._add_rule(rule)
|
|
101
|
+
|
|
102
|
+
def error(self, error_type: str, message: str = "", status: int = 400) -> MockBuilder:
|
|
103
|
+
"""Respond with a structured AWS error."""
|
|
104
|
+
import json
|
|
105
|
+
|
|
106
|
+
body = json.dumps({"__type": error_type, "message": message})
|
|
107
|
+
return self.respond(status=status, body=body)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Resource discovery from CDK and HCL projects."""
|