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.
Files changed (27) hide show
  1. local_web_services_python_sdk-0.1.0/.github/workflows/publish.yml +51 -0
  2. local_web_services_python_sdk-0.1.0/.gitignore +37 -0
  3. local_web_services_python_sdk-0.1.0/Makefile +17 -0
  4. local_web_services_python_sdk-0.1.0/PKG-INFO +96 -0
  5. local_web_services_python_sdk-0.1.0/README.md +69 -0
  6. local_web_services_python_sdk-0.1.0/pyproject.toml +83 -0
  7. local_web_services_python_sdk-0.1.0/src/lws_testing/__init__.py +26 -0
  8. local_web_services_python_sdk-0.1.0/src/lws_testing/_builders/__init__.py +1 -0
  9. local_web_services_python_sdk-0.1.0/src/lws_testing/_builders/chaos.py +56 -0
  10. local_web_services_python_sdk-0.1.0/src/lws_testing/_builders/iam.py +125 -0
  11. local_web_services_python_sdk-0.1.0/src/lws_testing/_builders/mock.py +107 -0
  12. local_web_services_python_sdk-0.1.0/src/lws_testing/_discovery/__init__.py +1 -0
  13. local_web_services_python_sdk-0.1.0/src/lws_testing/_discovery/cdk.py +115 -0
  14. local_web_services_python_sdk-0.1.0/src/lws_testing/_discovery/hcl.py +187 -0
  15. local_web_services_python_sdk-0.1.0/src/lws_testing/_logs.py +104 -0
  16. local_web_services_python_sdk-0.1.0/src/lws_testing/_resources/__init__.py +1 -0
  17. local_web_services_python_sdk-0.1.0/src/lws_testing/_resources/dynamodb.py +68 -0
  18. local_web_services_python_sdk-0.1.0/src/lws_testing/_resources/s3.py +69 -0
  19. local_web_services_python_sdk-0.1.0/src/lws_testing/_resources/sqs.py +76 -0
  20. local_web_services_python_sdk-0.1.0/src/lws_testing/_transport/__init__.py +1 -0
  21. local_web_services_python_sdk-0.1.0/src/lws_testing/_transport/inprocess.py +279 -0
  22. local_web_services_python_sdk-0.1.0/src/lws_testing/fixtures.py +99 -0
  23. local_web_services_python_sdk-0.1.0/src/lws_testing/session.py +330 -0
  24. local_web_services_python_sdk-0.1.0/tests/__init__.py +0 -0
  25. local_web_services_python_sdk-0.1.0/tests/unit/__init__.py +0 -0
  26. local_web_services_python_sdk-0.1.0/tests/unit/test_session_imports.py +50 -0
  27. 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."""