local-web-services-python-sdk 0.1.0__tar.gz → 0.1.1__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.1/.github/workflows/ci.yml +48 -0
- local_web_services_python_sdk-0.1.1/Makefile +36 -0
- {local_web_services_python_sdk-0.1.0 → local_web_services_python_sdk-0.1.1}/PKG-INFO +1 -1
- {local_web_services_python_sdk-0.1.0 → local_web_services_python_sdk-0.1.1}/pyproject.toml +2 -2
- {local_web_services_python_sdk-0.1.0 → local_web_services_python_sdk-0.1.1}/src/lws_testing/_builders/chaos.py +4 -4
- {local_web_services_python_sdk-0.1.0 → local_web_services_python_sdk-0.1.1}/src/lws_testing/_builders/iam.py +7 -7
- {local_web_services_python_sdk-0.1.0 → local_web_services_python_sdk-0.1.1}/src/lws_testing/_builders/mock.py +3 -3
- {local_web_services_python_sdk-0.1.0 → local_web_services_python_sdk-0.1.1}/src/lws_testing/_discovery/hcl.py +70 -55
- {local_web_services_python_sdk-0.1.0 → local_web_services_python_sdk-0.1.1}/src/lws_testing/_logs.py +7 -11
- {local_web_services_python_sdk-0.1.0 → local_web_services_python_sdk-0.1.1}/src/lws_testing/_resources/dynamodb.py +2 -1
- {local_web_services_python_sdk-0.1.0 → local_web_services_python_sdk-0.1.1}/src/lws_testing/_resources/sqs.py +1 -3
- {local_web_services_python_sdk-0.1.0 → local_web_services_python_sdk-0.1.1}/src/lws_testing/_transport/inprocess.py +161 -111
- {local_web_services_python_sdk-0.1.0 → local_web_services_python_sdk-0.1.1}/src/lws_testing/fixtures.py +2 -1
- {local_web_services_python_sdk-0.1.0 → local_web_services_python_sdk-0.1.1}/src/lws_testing/session.py +9 -9
- {local_web_services_python_sdk-0.1.0 → local_web_services_python_sdk-0.1.1}/tests/unit/test_session_lifecycle.py +1 -0
- local_web_services_python_sdk-0.1.0/Makefile +0 -17
- {local_web_services_python_sdk-0.1.0 → local_web_services_python_sdk-0.1.1}/.github/workflows/publish.yml +0 -0
- {local_web_services_python_sdk-0.1.0 → local_web_services_python_sdk-0.1.1}/.gitignore +0 -0
- {local_web_services_python_sdk-0.1.0 → local_web_services_python_sdk-0.1.1}/README.md +0 -0
- {local_web_services_python_sdk-0.1.0 → local_web_services_python_sdk-0.1.1}/src/lws_testing/__init__.py +0 -0
- {local_web_services_python_sdk-0.1.0 → local_web_services_python_sdk-0.1.1}/src/lws_testing/_builders/__init__.py +0 -0
- {local_web_services_python_sdk-0.1.0 → local_web_services_python_sdk-0.1.1}/src/lws_testing/_discovery/__init__.py +0 -0
- {local_web_services_python_sdk-0.1.0 → local_web_services_python_sdk-0.1.1}/src/lws_testing/_discovery/cdk.py +0 -0
- {local_web_services_python_sdk-0.1.0 → local_web_services_python_sdk-0.1.1}/src/lws_testing/_resources/__init__.py +0 -0
- {local_web_services_python_sdk-0.1.0 → local_web_services_python_sdk-0.1.1}/src/lws_testing/_resources/s3.py +0 -0
- {local_web_services_python_sdk-0.1.0 → local_web_services_python_sdk-0.1.1}/src/lws_testing/_transport/__init__.py +0 -0
- {local_web_services_python_sdk-0.1.0 → local_web_services_python_sdk-0.1.1}/tests/__init__.py +0 -0
- {local_web_services_python_sdk-0.1.0 → local_web_services_python_sdk-0.1.1}/tests/unit/__init__.py +0 -0
- {local_web_services_python_sdk-0.1.0 → local_web_services_python_sdk-0.1.1}/tests/unit/test_session_imports.py +0 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
pull-requests: write
|
|
10
|
+
checks: write
|
|
11
|
+
contents: read
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
lint:
|
|
15
|
+
runs-on: ubuntu-latest
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v4
|
|
18
|
+
- uses: astral-sh/setup-uv@v4
|
|
19
|
+
- uses: actions/setup-python@v5
|
|
20
|
+
with:
|
|
21
|
+
python-version: "3.11"
|
|
22
|
+
- run: make install
|
|
23
|
+
- run: make lint
|
|
24
|
+
- run: make format-check
|
|
25
|
+
|
|
26
|
+
analysis:
|
|
27
|
+
runs-on: ubuntu-latest
|
|
28
|
+
steps:
|
|
29
|
+
- uses: actions/checkout@v4
|
|
30
|
+
- uses: astral-sh/setup-uv@v4
|
|
31
|
+
- uses: actions/setup-python@v5
|
|
32
|
+
with:
|
|
33
|
+
python-version: "3.11"
|
|
34
|
+
- run: make install
|
|
35
|
+
- run: make complexity
|
|
36
|
+
- run: make cpd
|
|
37
|
+
- run: make pylint
|
|
38
|
+
|
|
39
|
+
test:
|
|
40
|
+
runs-on: ubuntu-latest
|
|
41
|
+
steps:
|
|
42
|
+
- uses: actions/checkout@v4
|
|
43
|
+
- uses: astral-sh/setup-uv@v4
|
|
44
|
+
- uses: actions/setup-python@v5
|
|
45
|
+
with:
|
|
46
|
+
python-version: "3.11"
|
|
47
|
+
- run: make install
|
|
48
|
+
- run: make test
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
.DEFAULT_GOAL := help
|
|
2
|
+
|
|
3
|
+
.PHONY: help install lint format format-check complexity cpd pylint test check
|
|
4
|
+
|
|
5
|
+
help: ## Show available targets
|
|
6
|
+
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf " %-15s %s\n", $$1, $$2}'
|
|
7
|
+
|
|
8
|
+
install: ## Install dependencies
|
|
9
|
+
uv sync --all-groups
|
|
10
|
+
|
|
11
|
+
lint: ## Run linter
|
|
12
|
+
uvx ruff check src tests
|
|
13
|
+
|
|
14
|
+
format: ## Auto-format code
|
|
15
|
+
uvx black src tests
|
|
16
|
+
|
|
17
|
+
format-check: ## Check formatting without changing files
|
|
18
|
+
uvx black --check src tests
|
|
19
|
+
|
|
20
|
+
complexity: ## Check cyclomatic complexity (all functions must be grade B or better)
|
|
21
|
+
@output=$$(uvx radon cc src -a -nc); \
|
|
22
|
+
echo "$$output"; \
|
|
23
|
+
echo "$$output" | grep -qE '^ +[A-Z]' && { echo "FAIL: complexity grade C or worse detected"; exit 1; } || true
|
|
24
|
+
|
|
25
|
+
cpd: ## Check for copy-pasted code (5+ similar lines)
|
|
26
|
+
@output=$$(uvx --from pylint symilar -d 5 --ignore-imports --ignore-docstrings --ignore-signatures $$(find src -name "*.py")); \
|
|
27
|
+
echo "$$output" | tail -1; \
|
|
28
|
+
echo "$$output" | tail -1 | grep -q "duplicates=0 " || { echo "$$output"; exit 1; }
|
|
29
|
+
|
|
30
|
+
pylint: ## Run pylint checks
|
|
31
|
+
uvx --from pylint pylint src/lws_testing --recursive=y
|
|
32
|
+
|
|
33
|
+
test: ## Run test suite
|
|
34
|
+
uv run pytest tests/ -v
|
|
35
|
+
|
|
36
|
+
check: lint format-check complexity cpd pylint test ## Run all checks (what CI runs)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: local-web-services-python-sdk
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.1
|
|
4
4
|
Summary: Python testing SDK for local-web-services — in-process pytest fixtures and boto3 helpers
|
|
5
5
|
Project-URL: Homepage, https://local-web-services.github.io/www
|
|
6
6
|
Project-URL: Repository, https://github.com/local-web-services/local-web-services-sdk-python
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "local-web-services-python-sdk"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.1"
|
|
4
4
|
description = "Python testing SDK for local-web-services — in-process pytest fixtures and boto3 helpers"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "MIT"
|
|
@@ -79,5 +79,5 @@ max-complexity = 10
|
|
|
79
79
|
disable = [
|
|
80
80
|
"E0401", "W0718", "R0801", "C0301", "C0302", "C0114", "C0115",
|
|
81
81
|
"R0903", "R0902", "R0913", "R0917", "R0904", "R0911", "R0914", "C0103",
|
|
82
|
-
"W0603", "W0621",
|
|
82
|
+
"W0603", "W0621", "C0415", "C0116", "W0212",
|
|
83
83
|
]
|
|
@@ -18,23 +18,23 @@ class ChaosBuilder:
|
|
|
18
18
|
self._mgmt_port = mgmt_port
|
|
19
19
|
self._config: dict = {"enabled": True}
|
|
20
20
|
|
|
21
|
-
def error_rate(self, rate: float) ->
|
|
21
|
+
def error_rate(self, rate: float) -> ChaosBuilder:
|
|
22
22
|
"""Set the fraction of requests that return an error (0.0–1.0)."""
|
|
23
23
|
self._config["error_rate"] = float(rate)
|
|
24
24
|
return self
|
|
25
25
|
|
|
26
|
-
def latency(self, min_ms: int = 0, max_ms: int = 0) ->
|
|
26
|
+
def latency(self, min_ms: int = 0, max_ms: int = 0) -> ChaosBuilder:
|
|
27
27
|
"""Inject artificial latency into responses."""
|
|
28
28
|
self._config["latency_min_ms"] = min_ms
|
|
29
29
|
self._config["latency_max_ms"] = max_ms
|
|
30
30
|
return self
|
|
31
31
|
|
|
32
|
-
def connection_reset_rate(self, rate: float) ->
|
|
32
|
+
def connection_reset_rate(self, rate: float) -> ChaosBuilder:
|
|
33
33
|
"""Set the fraction of requests that receive a connection reset (0.0–1.0)."""
|
|
34
34
|
self._config["connection_reset_rate"] = float(rate)
|
|
35
35
|
return self
|
|
36
36
|
|
|
37
|
-
def timeout_rate(self, rate: float) ->
|
|
37
|
+
def timeout_rate(self, rate: float) -> ChaosBuilder:
|
|
38
38
|
"""Set the fraction of requests that time out (0.0–1.0)."""
|
|
39
39
|
self._config["timeout_rate"] = float(rate)
|
|
40
40
|
return self
|
|
@@ -24,17 +24,17 @@ class IamBuilder:
|
|
|
24
24
|
self._updates: dict[str, Any] = {}
|
|
25
25
|
self._identities: dict[str, Any] = {}
|
|
26
26
|
|
|
27
|
-
def mode(self, mode: str) ->
|
|
27
|
+
def mode(self, mode: str) -> IamBuilder:
|
|
28
28
|
"""Set the global IAM auth mode (``"enforce"``, ``"audit"``, ``"disabled"``)."""
|
|
29
29
|
self._updates["mode"] = mode
|
|
30
30
|
return self
|
|
31
31
|
|
|
32
|
-
def default_identity(self, name: str) ->
|
|
32
|
+
def default_identity(self, name: str) -> IamBuilder:
|
|
33
33
|
"""Set the default identity used when no identity header is present."""
|
|
34
34
|
self._updates["default_identity"] = name
|
|
35
35
|
return self
|
|
36
36
|
|
|
37
|
-
def identity(self, name: str) ->
|
|
37
|
+
def identity(self, name: str) -> _IdentityBuilder:
|
|
38
38
|
"""Start configuring a named identity."""
|
|
39
39
|
return _IdentityBuilder(self, name)
|
|
40
40
|
|
|
@@ -64,7 +64,7 @@ class _IdentityBuilder:
|
|
|
64
64
|
self._inline_policies: list[dict[str, Any]] = []
|
|
65
65
|
self._boundary: dict[str, Any] | None = None
|
|
66
66
|
|
|
67
|
-
def allow(self, actions: list[str], resource: str = "*") ->
|
|
67
|
+
def allow(self, actions: list[str], resource: str = "*") -> _IdentityBuilder:
|
|
68
68
|
"""Add an Allow statement to this identity's inline policy."""
|
|
69
69
|
self._inline_policies.append(
|
|
70
70
|
{
|
|
@@ -83,7 +83,7 @@ class _IdentityBuilder:
|
|
|
83
83
|
)
|
|
84
84
|
return self
|
|
85
85
|
|
|
86
|
-
def deny(self, actions: list[str], resource: str = "*") ->
|
|
86
|
+
def deny(self, actions: list[str], resource: str = "*") -> _IdentityBuilder:
|
|
87
87
|
"""Add an explicit Deny statement to this identity's inline policy."""
|
|
88
88
|
self._inline_policies.append(
|
|
89
89
|
{
|
|
@@ -102,7 +102,7 @@ class _IdentityBuilder:
|
|
|
102
102
|
)
|
|
103
103
|
return self
|
|
104
104
|
|
|
105
|
-
def boundary(self, actions: list[str], resource: str = "*") ->
|
|
105
|
+
def boundary(self, actions: list[str], resource: str = "*") -> _IdentityBuilder:
|
|
106
106
|
"""Set a permissions boundary for this identity."""
|
|
107
107
|
self._boundary = {
|
|
108
108
|
"Version": "2012-10-17",
|
|
@@ -116,7 +116,7 @@ class _IdentityBuilder:
|
|
|
116
116
|
}
|
|
117
117
|
return self
|
|
118
118
|
|
|
119
|
-
def apply(self) ->
|
|
119
|
+
def apply(self) -> IamBuilder:
|
|
120
120
|
"""Register the identity and return the parent builder for chaining."""
|
|
121
121
|
config: dict[str, Any] = {"inline_policies": self._inline_policies}
|
|
122
122
|
if self._boundary is not None:
|
|
@@ -23,11 +23,11 @@ class MockBuilder:
|
|
|
23
23
|
self._mgmt_port = mgmt_port
|
|
24
24
|
self._rules: list[dict[str, Any]] = []
|
|
25
25
|
|
|
26
|
-
def operation(self, operation_name: str) ->
|
|
26
|
+
def operation(self, operation_name: str) -> _MockRuleBuilder:
|
|
27
27
|
"""Start building a mock rule for *operation_name*."""
|
|
28
28
|
return _MockRuleBuilder(self, operation_name)
|
|
29
29
|
|
|
30
|
-
def _add_rule(self, rule: dict[str, Any]) ->
|
|
30
|
+
def _add_rule(self, rule: dict[str, Any]) -> MockBuilder:
|
|
31
31
|
self._rules.append(rule)
|
|
32
32
|
self._apply()
|
|
33
33
|
return self
|
|
@@ -58,7 +58,7 @@ class _MockRuleBuilder:
|
|
|
58
58
|
self._operation = operation
|
|
59
59
|
self._match_headers: dict[str, str] = {}
|
|
60
60
|
|
|
61
|
-
def with_header(self, name: str, value: str) ->
|
|
61
|
+
def with_header(self, name: str, value: str) -> _MockRuleBuilder:
|
|
62
62
|
"""Add an additional header match constraint."""
|
|
63
63
|
self._match_headers[name] = value
|
|
64
64
|
return self
|
|
@@ -6,7 +6,6 @@ import re
|
|
|
6
6
|
from pathlib import Path
|
|
7
7
|
from typing import Any
|
|
8
8
|
|
|
9
|
-
|
|
10
9
|
# Matches: resource "aws_dynamodb_table" "logical_name" {
|
|
11
10
|
_RESOURCE_HEADER = re.compile(
|
|
12
11
|
r'resource\s+"(?P<type>[^"]+)"\s+"(?P<logical>[^"]+)"\s*\{',
|
|
@@ -14,7 +13,18 @@ _RESOURCE_HEADER = re.compile(
|
|
|
14
13
|
# Matches a simple key = "value" assignment
|
|
15
14
|
_ATTR_STR = re.compile(r'^\s*(?P<key>\w+)\s*=\s*"(?P<value>[^"]*)"')
|
|
16
15
|
# Matches a simple key = value (unquoted booleans / numbers)
|
|
17
|
-
_ATTR_BARE = re.compile(r
|
|
16
|
+
_ATTR_BARE = re.compile(r"^\s*(?P<key>\w+)\s*=\s*(?P<value>\S+)")
|
|
17
|
+
|
|
18
|
+
# Maps Terraform resource types to (collection_key, builder_function)
|
|
19
|
+
_DISPATCH: dict[str, tuple[str, Any]] = {
|
|
20
|
+
"aws_dynamodb_table": ("tables", "_build_table"),
|
|
21
|
+
"aws_sqs_queue": ("queues", "_build_queue"),
|
|
22
|
+
"aws_s3_bucket": ("buckets", "_build_bucket"),
|
|
23
|
+
"aws_sns_topic": ("topics", "_build_topic"),
|
|
24
|
+
"aws_sfn_state_machine": ("state_machines", "_build_state_machine"),
|
|
25
|
+
"aws_ssm_parameter": ("parameters", "_build_parameter"),
|
|
26
|
+
"aws_secretsmanager_secret": ("secrets", "_build_secret"),
|
|
27
|
+
}
|
|
18
28
|
|
|
19
29
|
|
|
20
30
|
def discover(project_dir: Path) -> dict[str, Any]:
|
|
@@ -35,58 +45,43 @@ def discover(project_dir: Path) -> dict[str, Any]:
|
|
|
35
45
|
for tf_file in tf_files:
|
|
36
46
|
resources.extend(_parse_tf_file(tf_file))
|
|
37
47
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
48
|
+
return _classify_resources(resources)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _classify_resources(
|
|
52
|
+
resources: list[tuple[str, str, dict[str, str]]],
|
|
53
|
+
) -> dict[str, list[Any]]:
|
|
54
|
+
"""Route parsed resources into typed collections."""
|
|
55
|
+
result: dict[str, list[Any]] = {
|
|
56
|
+
"tables": [],
|
|
57
|
+
"queues": [],
|
|
58
|
+
"buckets": [],
|
|
59
|
+
"topics": [],
|
|
60
|
+
"state_machines": [],
|
|
61
|
+
"parameters": [],
|
|
62
|
+
"secrets": [],
|
|
63
|
+
}
|
|
64
|
+
handlers = _get_handlers()
|
|
46
65
|
for resource_type, _logical, attrs in resources:
|
|
47
|
-
if resource_type
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
queues.append(queue)
|
|
55
|
-
elif resource_type == "aws_s3_bucket":
|
|
56
|
-
name = attrs.get("bucket") or attrs.get("name")
|
|
57
|
-
if name:
|
|
58
|
-
buckets.append(name)
|
|
59
|
-
elif resource_type == "aws_sns_topic":
|
|
60
|
-
name = attrs.get("name")
|
|
61
|
-
if name:
|
|
62
|
-
topics.append({"name": name})
|
|
63
|
-
elif resource_type == "aws_sfn_state_machine":
|
|
64
|
-
sm = _build_state_machine(attrs)
|
|
65
|
-
if sm:
|
|
66
|
-
state_machines.append(sm)
|
|
67
|
-
elif resource_type == "aws_ssm_parameter":
|
|
68
|
-
param = _build_parameter(attrs)
|
|
69
|
-
if param:
|
|
70
|
-
parameters.append(param)
|
|
71
|
-
elif resource_type == "aws_secretsmanager_secret":
|
|
72
|
-
name = attrs.get("name")
|
|
73
|
-
if name:
|
|
74
|
-
secrets.append(
|
|
75
|
-
{
|
|
76
|
-
"name": name,
|
|
77
|
-
"description": attrs.get("description", ""),
|
|
78
|
-
"secret_string": "",
|
|
79
|
-
}
|
|
80
|
-
)
|
|
66
|
+
if resource_type not in _DISPATCH:
|
|
67
|
+
continue
|
|
68
|
+
key, handler_name = _DISPATCH[resource_type]
|
|
69
|
+
item = handlers[handler_name](attrs)
|
|
70
|
+
if item is not None:
|
|
71
|
+
result[key].append(item)
|
|
72
|
+
return result
|
|
81
73
|
|
|
74
|
+
|
|
75
|
+
def _get_handlers() -> dict[str, Any]:
|
|
76
|
+
"""Return a map of handler name to callable."""
|
|
82
77
|
return {
|
|
83
|
-
"
|
|
84
|
-
"
|
|
85
|
-
"
|
|
86
|
-
"
|
|
87
|
-
"
|
|
88
|
-
"
|
|
89
|
-
"
|
|
78
|
+
"_build_table": _build_table,
|
|
79
|
+
"_build_queue": _build_queue,
|
|
80
|
+
"_build_bucket": _build_bucket,
|
|
81
|
+
"_build_topic": _build_topic,
|
|
82
|
+
"_build_state_machine": _build_state_machine,
|
|
83
|
+
"_build_parameter": _build_parameter,
|
|
84
|
+
"_build_secret": _build_secret,
|
|
90
85
|
}
|
|
91
86
|
|
|
92
87
|
|
|
@@ -115,11 +110,8 @@ def _collect_block(lines: list[str], start: int) -> tuple[dict[str, str], int]:
|
|
|
115
110
|
i = start
|
|
116
111
|
while i < len(lines) and depth > 0:
|
|
117
112
|
line = lines[i]
|
|
118
|
-
|
|
119
|
-
closes = line.count("}")
|
|
120
|
-
depth += opens - closes
|
|
113
|
+
depth += line.count("{") - line.count("}")
|
|
121
114
|
if depth > 1:
|
|
122
|
-
# Nested block — skip its contents
|
|
123
115
|
i += 1
|
|
124
116
|
continue
|
|
125
117
|
if depth == 0:
|
|
@@ -162,6 +154,17 @@ def _build_queue(attrs: dict[str, str]) -> dict[str, Any] | None:
|
|
|
162
154
|
return spec
|
|
163
155
|
|
|
164
156
|
|
|
157
|
+
def _build_bucket(attrs: dict[str, str]) -> str | None:
|
|
158
|
+
"""Build a bucket name from S3 resource attributes."""
|
|
159
|
+
return attrs.get("bucket") or attrs.get("name")
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _build_topic(attrs: dict[str, str]) -> dict[str, str] | None:
|
|
163
|
+
"""Build a topic spec dict from SNS resource attributes."""
|
|
164
|
+
name = attrs.get("name")
|
|
165
|
+
return {"name": name} if name else None
|
|
166
|
+
|
|
167
|
+
|
|
165
168
|
def _build_state_machine(attrs: dict[str, str]) -> dict[str, Any] | None:
|
|
166
169
|
"""Build a state machine spec dict from Step Functions resource attributes."""
|
|
167
170
|
name = attrs.get("name")
|
|
@@ -185,3 +188,15 @@ def _build_parameter(attrs: dict[str, str]) -> dict[str, Any] | None:
|
|
|
185
188
|
"type": attrs.get("type", "String"),
|
|
186
189
|
"description": attrs.get("description", ""),
|
|
187
190
|
}
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _build_secret(attrs: dict[str, str]) -> dict[str, Any] | None:
|
|
194
|
+
"""Build a secret spec dict from Secrets Manager resource attributes."""
|
|
195
|
+
name = attrs.get("name")
|
|
196
|
+
if not name:
|
|
197
|
+
return None
|
|
198
|
+
return {
|
|
199
|
+
"name": name,
|
|
200
|
+
"description": attrs.get("description", ""),
|
|
201
|
+
"secret_string": "",
|
|
202
|
+
}
|
{local_web_services_python_sdk-0.1.0 → local_web_services_python_sdk-0.1.1}/src/lws_testing/_logs.py
RENAMED
|
@@ -33,7 +33,7 @@ class LogCapture:
|
|
|
33
33
|
if self._handler is None:
|
|
34
34
|
return
|
|
35
35
|
full_backlog = self._handler.backlog()
|
|
36
|
-
self._entries = full_backlog[self._snapshot_len:]
|
|
36
|
+
self._entries = full_backlog[self._snapshot_len :]
|
|
37
37
|
|
|
38
38
|
# ── Access ────────────────────────────────────────────────────────────────
|
|
39
39
|
|
|
@@ -59,11 +59,11 @@ class LogCapture:
|
|
|
59
59
|
matching = [
|
|
60
60
|
e
|
|
61
61
|
for e in self._entries
|
|
62
|
-
if e.get("service", "").lower() == expected_service
|
|
63
|
-
and e.get("operation") == operation
|
|
62
|
+
if e.get("service", "").lower() == expected_service and e.get("operation") == operation
|
|
64
63
|
]
|
|
65
64
|
assert matching, (
|
|
66
|
-
f"Expected {service}.{operation} to have been called,
|
|
65
|
+
f"Expected {service}.{operation} to have been called, "
|
|
66
|
+
f"but no matching log entry was found. "
|
|
67
67
|
f"Captured entries: {self._entries}"
|
|
68
68
|
)
|
|
69
69
|
|
|
@@ -73,8 +73,7 @@ class LogCapture:
|
|
|
73
73
|
matching = [
|
|
74
74
|
e
|
|
75
75
|
for e in self._entries
|
|
76
|
-
if e.get("service", "").lower() == expected_service
|
|
77
|
-
and e.get("operation") == operation
|
|
76
|
+
if e.get("service", "").lower() == expected_service and e.get("operation") == operation
|
|
78
77
|
]
|
|
79
78
|
assert not matching, (
|
|
80
79
|
f"Expected {service}.{operation} NOT to have been called, "
|
|
@@ -84,9 +83,7 @@ class LogCapture:
|
|
|
84
83
|
def assert_no_errors(self) -> None:
|
|
85
84
|
"""Assert that no ERROR-level log entries were captured."""
|
|
86
85
|
errors = [e for e in self._entries if e.get("level") == "ERROR"]
|
|
87
|
-
assert not errors, (
|
|
88
|
-
f"Expected no ERROR log entries, but found {len(errors)}: {errors}"
|
|
89
|
-
)
|
|
86
|
+
assert not errors, f"Expected no ERROR log entries, but found {len(errors)}: {errors}"
|
|
90
87
|
|
|
91
88
|
def assert_call_count(self, service: str, operation: str, expected_count: int) -> None:
|
|
92
89
|
"""Assert that *service*.*operation* was called exactly *expected_count* times."""
|
|
@@ -94,8 +91,7 @@ class LogCapture:
|
|
|
94
91
|
actual_entries = [
|
|
95
92
|
e
|
|
96
93
|
for e in self._entries
|
|
97
|
-
if e.get("service", "").lower() == expected_service
|
|
98
|
-
and e.get("operation") == operation
|
|
94
|
+
if e.get("service", "").lower() == expected_service and e.get("operation") == operation
|
|
99
95
|
]
|
|
100
96
|
actual_count = len(actual_entries)
|
|
101
97
|
assert actual_count == expected_count, (
|
|
@@ -54,7 +54,8 @@ class DynamoDBHelper:
|
|
|
54
54
|
"""Assert that an item matching *key* exists; return the item."""
|
|
55
55
|
item = self.get(key)
|
|
56
56
|
assert item is not None, (
|
|
57
|
-
f"Expected item with key {key} in table {self._table_name!r}
|
|
57
|
+
f"Expected item with key {key} in table {self._table_name!r} "
|
|
58
|
+
"to exist, but it was not found."
|
|
58
59
|
)
|
|
59
60
|
return item
|
|
60
61
|
|
|
@@ -62,9 +62,7 @@ class SQSHelper:
|
|
|
62
62
|
QueueUrl=self._queue_url,
|
|
63
63
|
AttributeNames=["ApproximateNumberOfMessages"],
|
|
64
64
|
)
|
|
65
|
-
actual_count = int(
|
|
66
|
-
response["Attributes"].get("ApproximateNumberOfMessages", "0")
|
|
67
|
-
)
|
|
65
|
+
actual_count = int(response["Attributes"].get("ApproximateNumberOfMessages", "0"))
|
|
68
66
|
assert actual_count == expected_count, (
|
|
69
67
|
f"Expected {expected_count} message(s) in queue {self._queue_name!r}, "
|
|
70
68
|
f"but found approximately {actual_count}."
|
|
@@ -112,8 +112,8 @@ def _create_management_app(
|
|
|
112
112
|
) -> Any:
|
|
113
113
|
"""Build a FastAPI management app with reset, mock, and chaos endpoints."""
|
|
114
114
|
from fastapi import FastAPI
|
|
115
|
-
from lws.api.management import _handle_reset, create_management_router
|
|
116
115
|
from fastapi.responses import JSONResponse
|
|
116
|
+
from lws.api.management import _handle_reset, create_management_router
|
|
117
117
|
|
|
118
118
|
orchestrator = _StubOrchestrator(providers)
|
|
119
119
|
app = FastAPI(title="LWS Testing Management")
|
|
@@ -134,139 +134,189 @@ def _create_management_app(
|
|
|
134
134
|
return app
|
|
135
135
|
|
|
136
136
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
data_dir: Path,
|
|
140
|
-
) -> tuple[Any, dict[str, int], int, list[Any]]:
|
|
141
|
-
"""Start all LWS services in-process.
|
|
142
|
-
|
|
143
|
-
Returns:
|
|
144
|
-
Tuple of (log_handler, service_ports, management_port, servers_list).
|
|
145
|
-
``servers_list`` is a list of ``(Server, Task)`` pairs that can be
|
|
146
|
-
passed directly to :func:`stop_services`.
|
|
147
|
-
"""
|
|
137
|
+
def _setup_logging() -> Any:
|
|
138
|
+
"""Initialise WebSocketLogHandler and install it globally."""
|
|
148
139
|
from lws.logging.logger import WebSocketLogHandler, set_ws_handler
|
|
149
|
-
|
|
150
|
-
|
|
140
|
+
|
|
141
|
+
log_handler = WebSocketLogHandler()
|
|
142
|
+
set_ws_handler(log_handler)
|
|
143
|
+
return log_handler
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _convert_spec(spec: dict[str, Any]) -> dict[str, list[Any]]:
|
|
147
|
+
"""Convert raw spec dict to typed provider config lists."""
|
|
148
|
+
return {
|
|
149
|
+
"tables": [_make_table_config(t) for t in spec.get("tables", [])],
|
|
150
|
+
"queues": [_make_queue_config(q) for q in spec.get("queues", [])],
|
|
151
|
+
"buckets": [b if isinstance(b, str) else b["name"] for b in spec.get("buckets", [])],
|
|
152
|
+
"topics": [_make_topic_config(t) for t in spec.get("topics", [])],
|
|
153
|
+
"state_machines": [_make_state_machine_config(sm) for sm in spec.get("state_machines", [])],
|
|
154
|
+
"parameters": [_make_initial_parameter(p) for p in spec.get("parameters", [])],
|
|
155
|
+
"secrets": [_make_initial_secret(s) for s in spec.get("secrets", [])],
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _create_providers(cfg: dict[str, list[Any]], data_dir: Path) -> dict[str, Any]:
|
|
160
|
+
"""Instantiate all service providers."""
|
|
151
161
|
from lws.providers.dynamodb.provider import SqliteDynamoProvider
|
|
152
|
-
from lws.providers.dynamodb.routes import create_dynamodb_app
|
|
153
|
-
from lws.providers.mockserver.provider import start_uvicorn_server
|
|
154
162
|
from lws.providers.s3.provider import S3Provider
|
|
155
|
-
from lws.providers.s3.routes import create_s3_app
|
|
156
|
-
from lws.providers.secretsmanager.routes import create_secretsmanager_app
|
|
157
163
|
from lws.providers.sns.provider import SnsProvider
|
|
158
|
-
from lws.providers.sns.routes import create_sns_app
|
|
159
164
|
from lws.providers.sqs.provider import SqsProvider
|
|
160
|
-
from lws.providers.sqs.routes import create_sqs_app
|
|
161
|
-
from lws.providers.ssm.routes import create_ssm_app
|
|
162
165
|
from lws.providers.stepfunctions.provider import StepFunctionsProvider
|
|
163
|
-
from lws.providers.stepfunctions.routes import create_stepfunctions_app
|
|
164
|
-
|
|
165
|
-
# Set up log handler
|
|
166
|
-
log_handler = WebSocketLogHandler()
|
|
167
|
-
set_ws_handler(log_handler)
|
|
168
|
-
|
|
169
|
-
# Convert spec to provider configs
|
|
170
|
-
tables = [_make_table_config(t) for t in spec.get("tables", [])]
|
|
171
|
-
queues = [_make_queue_config(q) for q in spec.get("queues", [])]
|
|
172
|
-
buckets = [b if isinstance(b, str) else b["name"] for b in spec.get("buckets", [])]
|
|
173
|
-
topics = [_make_topic_config(t) for t in spec.get("topics", [])]
|
|
174
|
-
state_machines = [_make_state_machine_config(sm) for sm in spec.get("state_machines", [])]
|
|
175
|
-
parameters = [_make_initial_parameter(p) for p in spec.get("parameters", [])]
|
|
176
|
-
secrets = [_make_initial_secret(s) for s in spec.get("secrets", [])]
|
|
177
166
|
|
|
178
|
-
# Create providers
|
|
179
167
|
dynamo_data = data_dir / "dynamodb"
|
|
180
168
|
dynamo_data.mkdir(parents=True, exist_ok=True)
|
|
181
169
|
s3_data = data_dir / "s3"
|
|
182
170
|
s3_data.mkdir(parents=True, exist_ok=True)
|
|
183
171
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
172
|
+
return {
|
|
173
|
+
"dynamodb": SqliteDynamoProvider(data_dir=dynamo_data, tables=cfg["tables"]),
|
|
174
|
+
"sqs": SqsProvider(queues=cfg["queues"]),
|
|
175
|
+
"s3": S3Provider(data_dir=s3_data, buckets=cfg["buckets"]),
|
|
176
|
+
"sns": SnsProvider(topics=cfg["topics"]),
|
|
177
|
+
"stepfunctions": StepFunctionsProvider(state_machines=cfg["state_machines"]),
|
|
178
|
+
}
|
|
189
179
|
|
|
190
|
-
# Create mutable chaos and mock configs per service
|
|
191
|
-
service_names = ["dynamodb", "sqs", "s3", "sns", "stepfunctions", "ssm", "secretsmanager"]
|
|
192
|
-
chaos_configs: dict[str, AwsChaosConfig] = {s: AwsChaosConfig() for s in service_names}
|
|
193
|
-
mock_configs: dict[str, AwsMockConfig] = {s: AwsMockConfig(service=s) for s in service_names}
|
|
194
180
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
181
|
+
def _build_service_apps(
|
|
182
|
+
providers: dict[str, Any],
|
|
183
|
+
ports: dict[str, int],
|
|
184
|
+
chaos_configs: dict[str, Any],
|
|
185
|
+
mock_configs: dict[str, Any],
|
|
186
|
+
cfg: dict[str, list[Any]],
|
|
187
|
+
) -> list[tuple[str, Any]]:
|
|
188
|
+
"""Build FastAPI apps for all services; return as (service_name, app) pairs."""
|
|
189
|
+
from lws.providers.dynamodb.routes import create_dynamodb_app
|
|
190
|
+
from lws.providers.s3.routes import create_s3_app
|
|
191
|
+
from lws.providers.secretsmanager.routes import create_secretsmanager_app
|
|
192
|
+
from lws.providers.sns.routes import create_sns_app
|
|
193
|
+
from lws.providers.sqs.routes import create_sqs_app
|
|
194
|
+
from lws.providers.ssm.routes import create_ssm_app
|
|
195
|
+
from lws.providers.stepfunctions.routes import create_stepfunctions_app
|
|
196
|
+
|
|
197
|
+
return [
|
|
198
|
+
(
|
|
199
|
+
"dynamodb",
|
|
200
|
+
create_dynamodb_app(
|
|
201
|
+
providers["dynamodb"],
|
|
202
|
+
chaos=chaos_configs["dynamodb"],
|
|
203
|
+
aws_mock=mock_configs["dynamodb"],
|
|
204
|
+
),
|
|
205
|
+
),
|
|
206
|
+
(
|
|
207
|
+
"sqs",
|
|
208
|
+
create_sqs_app(
|
|
209
|
+
providers["sqs"],
|
|
210
|
+
port=ports["sqs"],
|
|
211
|
+
chaos=chaos_configs["sqs"],
|
|
212
|
+
aws_mock=mock_configs["sqs"],
|
|
213
|
+
),
|
|
214
|
+
),
|
|
215
|
+
(
|
|
216
|
+
"s3",
|
|
217
|
+
create_s3_app(
|
|
218
|
+
providers["s3"],
|
|
219
|
+
chaos=chaos_configs["s3"],
|
|
220
|
+
aws_mock=mock_configs["s3"],
|
|
221
|
+
),
|
|
222
|
+
),
|
|
223
|
+
(
|
|
224
|
+
"sns",
|
|
225
|
+
create_sns_app(
|
|
226
|
+
providers["sns"],
|
|
227
|
+
chaos=chaos_configs["sns"],
|
|
228
|
+
aws_mock=mock_configs["sns"],
|
|
229
|
+
),
|
|
230
|
+
),
|
|
231
|
+
(
|
|
232
|
+
"stepfunctions",
|
|
233
|
+
create_stepfunctions_app(
|
|
234
|
+
providers["stepfunctions"],
|
|
235
|
+
chaos=chaos_configs["stepfunctions"],
|
|
236
|
+
aws_mock=mock_configs["stepfunctions"],
|
|
237
|
+
),
|
|
238
|
+
),
|
|
239
|
+
(
|
|
240
|
+
"ssm",
|
|
241
|
+
create_ssm_app(
|
|
242
|
+
initial_parameters=cfg["parameters"] or None,
|
|
243
|
+
chaos=chaos_configs["ssm"],
|
|
244
|
+
aws_mock=mock_configs["ssm"],
|
|
245
|
+
),
|
|
246
|
+
),
|
|
247
|
+
(
|
|
248
|
+
"secretsmanager",
|
|
249
|
+
create_secretsmanager_app(
|
|
250
|
+
initial_secrets=cfg["secrets"] or None,
|
|
251
|
+
chaos=chaos_configs["secretsmanager"],
|
|
252
|
+
aws_mock=mock_configs["secretsmanager"],
|
|
253
|
+
),
|
|
254
|
+
),
|
|
255
|
+
]
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
async def _start_providers(providers: dict[str, Any]) -> None:
|
|
259
|
+
"""Start the lifecycle of all service providers."""
|
|
260
|
+
for provider in providers.values():
|
|
261
|
+
await provider.start()
|
|
198
262
|
|
|
199
|
-
# Build FastAPI apps
|
|
200
|
-
dynamo_app = create_dynamodb_app(
|
|
201
|
-
dynamo_provider,
|
|
202
|
-
chaos=chaos_configs["dynamodb"],
|
|
203
|
-
aws_mock=mock_configs["dynamodb"],
|
|
204
|
-
)
|
|
205
|
-
sqs_app = create_sqs_app(
|
|
206
|
-
sqs_provider,
|
|
207
|
-
port=ports["sqs"],
|
|
208
|
-
chaos=chaos_configs["sqs"],
|
|
209
|
-
aws_mock=mock_configs["sqs"],
|
|
210
|
-
)
|
|
211
|
-
s3_app = create_s3_app(
|
|
212
|
-
s3_provider,
|
|
213
|
-
chaos=chaos_configs["s3"],
|
|
214
|
-
aws_mock=mock_configs["s3"],
|
|
215
|
-
)
|
|
216
|
-
sns_app = create_sns_app(
|
|
217
|
-
sns_provider,
|
|
218
|
-
chaos=chaos_configs["sns"],
|
|
219
|
-
aws_mock=mock_configs["sns"],
|
|
220
|
-
)
|
|
221
|
-
sf_app = create_stepfunctions_app(
|
|
222
|
-
sf_provider,
|
|
223
|
-
chaos=chaos_configs["stepfunctions"],
|
|
224
|
-
aws_mock=mock_configs["stepfunctions"],
|
|
225
|
-
)
|
|
226
|
-
ssm_app = create_ssm_app(
|
|
227
|
-
initial_parameters=parameters or None,
|
|
228
|
-
chaos=chaos_configs["ssm"],
|
|
229
|
-
aws_mock=mock_configs["ssm"],
|
|
230
|
-
)
|
|
231
|
-
secretsmanager_app = create_secretsmanager_app(
|
|
232
|
-
initial_secrets=secrets or None,
|
|
233
|
-
chaos=chaos_configs["secretsmanager"],
|
|
234
|
-
aws_mock=mock_configs["secretsmanager"],
|
|
235
|
-
)
|
|
236
263
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
264
|
+
async def _start_all_servers(
|
|
265
|
+
service_apps: list[tuple[str, Any]],
|
|
266
|
+
ports: dict[str, int],
|
|
267
|
+
mgmt_app: Any,
|
|
268
|
+
mgmt_port: int,
|
|
269
|
+
) -> list[Any]:
|
|
270
|
+
"""Start uvicorn servers for every service app plus the management app."""
|
|
271
|
+
from lws.providers.mockserver.provider import start_uvicorn_server
|
|
243
272
|
|
|
244
|
-
# Start uvicorn servers for each service
|
|
245
273
|
servers: list[Any] = []
|
|
246
|
-
service_apps = [
|
|
247
|
-
("dynamodb", dynamo_app),
|
|
248
|
-
("sqs", sqs_app),
|
|
249
|
-
("s3", s3_app),
|
|
250
|
-
("sns", sns_app),
|
|
251
|
-
("stepfunctions", sf_app),
|
|
252
|
-
("ssm", ssm_app),
|
|
253
|
-
("secretsmanager", secretsmanager_app),
|
|
254
|
-
]
|
|
255
274
|
for svc, app in service_apps:
|
|
256
275
|
server, task = await start_uvicorn_server(app, ports[svc], host="127.0.0.1")
|
|
257
276
|
servers.append((server, task))
|
|
258
|
-
|
|
259
|
-
# Start management app
|
|
260
|
-
all_providers: dict[str, Any] = {
|
|
261
|
-
"dynamodb": dynamo_provider,
|
|
262
|
-
"sqs": sqs_provider,
|
|
263
|
-
"s3": s3_provider,
|
|
264
|
-
"sns": sns_provider,
|
|
265
|
-
"stepfunctions": sf_provider,
|
|
266
|
-
}
|
|
267
|
-
mgmt_app = _create_management_app(all_providers, chaos_configs, mock_configs)
|
|
268
277
|
mgmt_server, mgmt_task = await start_uvicorn_server(mgmt_app, mgmt_port, host="127.0.0.1")
|
|
269
278
|
servers.append((mgmt_server, mgmt_task))
|
|
279
|
+
return servers
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
_SERVICE_NAMES = [
|
|
283
|
+
"dynamodb",
|
|
284
|
+
"sqs",
|
|
285
|
+
"s3",
|
|
286
|
+
"sns",
|
|
287
|
+
"stepfunctions",
|
|
288
|
+
"ssm",
|
|
289
|
+
"secretsmanager",
|
|
290
|
+
]
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
async def start_services(
|
|
294
|
+
spec: dict[str, Any],
|
|
295
|
+
data_dir: Path,
|
|
296
|
+
) -> tuple[Any, dict[str, int], int, list[Any]]:
|
|
297
|
+
"""Start all LWS services in-process.
|
|
298
|
+
|
|
299
|
+
Returns:
|
|
300
|
+
Tuple of (log_handler, service_ports, management_port, servers_list).
|
|
301
|
+
``servers_list`` is a list of ``(Server, Task)`` pairs that can be
|
|
302
|
+
passed directly to :func:`stop_services`.
|
|
303
|
+
"""
|
|
304
|
+
from lws.providers._shared.aws_chaos import AwsChaosConfig
|
|
305
|
+
from lws.providers._shared.aws_operation_mock import AwsMockConfig
|
|
306
|
+
|
|
307
|
+
log_handler = _setup_logging()
|
|
308
|
+
cfg = _convert_spec(spec)
|
|
309
|
+
providers = _create_providers(cfg, data_dir)
|
|
310
|
+
|
|
311
|
+
chaos_configs: dict[str, Any] = {s: AwsChaosConfig() for s in _SERVICE_NAMES}
|
|
312
|
+
mock_configs: dict[str, Any] = {s: AwsMockConfig(service=s) for s in _SERVICE_NAMES}
|
|
313
|
+
ports: dict[str, int] = {s: _free_port() for s in _SERVICE_NAMES}
|
|
314
|
+
mgmt_port = _free_port()
|
|
315
|
+
|
|
316
|
+
service_apps = _build_service_apps(providers, ports, chaos_configs, mock_configs, cfg)
|
|
317
|
+
await _start_providers(providers)
|
|
318
|
+
mgmt_app = _create_management_app(providers, chaos_configs, mock_configs)
|
|
319
|
+
servers = await _start_all_servers(service_apps, ports, mgmt_app, mgmt_port)
|
|
270
320
|
|
|
271
321
|
return log_handler, ports, mgmt_port, servers
|
|
272
322
|
|
|
@@ -7,11 +7,10 @@ import shutil
|
|
|
7
7
|
import socket
|
|
8
8
|
import tempfile
|
|
9
9
|
import threading
|
|
10
|
+
from collections.abc import Generator
|
|
10
11
|
from contextlib import contextmanager
|
|
11
12
|
from pathlib import Path
|
|
12
|
-
from typing import Any
|
|
13
|
-
|
|
14
|
-
import httpx
|
|
13
|
+
from typing import Any
|
|
15
14
|
|
|
16
15
|
|
|
17
16
|
def _free_port() -> int:
|
|
@@ -75,7 +74,7 @@ class LwsSession:
|
|
|
75
74
|
# ── Constructors ──────────────────────────────────────────────────────────
|
|
76
75
|
|
|
77
76
|
@classmethod
|
|
78
|
-
def from_cdk(cls, project_dir: str = ".") ->
|
|
77
|
+
def from_cdk(cls, project_dir: str = ".") -> LwsSession:
|
|
79
78
|
"""Create a session by discovering resources from a CDK project.
|
|
80
79
|
|
|
81
80
|
Reads the synthesised cloud assembly in ``{project_dir}/cdk.out/``.
|
|
@@ -91,7 +90,7 @@ class LwsSession:
|
|
|
91
90
|
return cls(**spec)
|
|
92
91
|
|
|
93
92
|
@classmethod
|
|
94
|
-
def from_hcl(cls, project_dir: str = ".") ->
|
|
93
|
+
def from_hcl(cls, project_dir: str = ".") -> LwsSession:
|
|
95
94
|
"""Create a session by discovering resources from an HCL project.
|
|
96
95
|
|
|
97
96
|
Reads ``.tf`` files in ``project_dir`` to discover tables, queues,
|
|
@@ -107,7 +106,7 @@ class LwsSession:
|
|
|
107
106
|
|
|
108
107
|
# ── Context manager ───────────────────────────────────────────────────────
|
|
109
108
|
|
|
110
|
-
def __enter__(self) ->
|
|
109
|
+
def __enter__(self) -> LwsSession:
|
|
111
110
|
self._start()
|
|
112
111
|
return self
|
|
113
112
|
|
|
@@ -137,14 +136,15 @@ class LwsSession:
|
|
|
137
136
|
ready.wait(timeout=30)
|
|
138
137
|
|
|
139
138
|
if error_holder:
|
|
140
|
-
|
|
139
|
+
exc = error_holder[0]
|
|
140
|
+
raise RuntimeError(f"LwsSession failed to start: {exc}") from exc
|
|
141
141
|
|
|
142
142
|
async def _async_start(self, ready: threading.Event) -> None:
|
|
143
143
|
"""Create providers, build apps, start servers."""
|
|
144
144
|
from lws_testing._transport.inprocess import start_services
|
|
145
145
|
|
|
146
|
-
self._log_handler, self._ports, self._mgmt_port, self._servers = (
|
|
147
|
-
|
|
146
|
+
self._log_handler, self._ports, self._mgmt_port, self._servers = await start_services(
|
|
147
|
+
self._spec, self._data_dir
|
|
148
148
|
)
|
|
149
149
|
ready.set()
|
|
150
150
|
|
|
@@ -1,17 +0,0 @@
|
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{local_web_services_python_sdk-0.1.0 → local_web_services_python_sdk-0.1.1}/tests/__init__.py
RENAMED
|
File without changes
|
{local_web_services_python_sdk-0.1.0 → local_web_services_python_sdk-0.1.1}/tests/unit/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|