lambda-preflight 0.1.0__py3-none-any.whl
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.
- lambda_preflight/__init__.py +58 -0
- lambda_preflight/events/__init__.py +21 -0
- lambda_preflight/events/builders.py +287 -0
- lambda_preflight/iam/__init__.py +32 -0
- lambda_preflight/iam/conditions.py +131 -0
- lambda_preflight/iam/evaluator.py +177 -0
- lambda_preflight/iam/intercept.py +101 -0
- lambda_preflight/iam/model.py +90 -0
- lambda_preflight/iam/puller.py +190 -0
- lambda_preflight/runtime/__init__.py +13 -0
- lambda_preflight/runtime/context.py +75 -0
- lambda_preflight/runtime/errors.py +34 -0
- lambda_preflight/runtime/harness.py +167 -0
- lambda_preflight/runtime/loader.py +94 -0
- lambda_preflight/testing/__init__.py +3 -0
- lambda_preflight/testing/fixtures.py +43 -0
- lambda_preflight-0.1.0.dist-info/METADATA +143 -0
- lambda_preflight-0.1.0.dist-info/RECORD +21 -0
- lambda_preflight-0.1.0.dist-info/WHEEL +4 -0
- lambda_preflight-0.1.0.dist-info/entry_points.txt +2 -0
- lambda_preflight-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"""The in-process Lambda runtime shim.
|
|
2
|
+
|
|
3
|
+
``LambdaHarness`` reconstructs the Lambda invocation contract without Docker,
|
|
4
|
+
SAM, or a cloud call: it resolves the handler once (cold start), then on each
|
|
5
|
+
invoke builds a fresh context, applies the Lambda environment, enforces the
|
|
6
|
+
timeout, and serializes errors the way AWS does.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import threading
|
|
12
|
+
from typing import Any, Callable, Dict, Optional
|
|
13
|
+
|
|
14
|
+
from .context import LambdaContext
|
|
15
|
+
from .errors import LambdaTimeout, format_exception
|
|
16
|
+
from .loader import build_lambda_environ, lambda_environment, resolve_handler
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class InvocationResult:
|
|
20
|
+
"""Outcome of a single invocation.
|
|
21
|
+
|
|
22
|
+
``ok`` distinguishes a normal return from an error envelope. On success
|
|
23
|
+
``payload`` is the handler's return value; on failure ``error`` holds the
|
|
24
|
+
AWS-shape envelope (``errorType`` / ``errorMessage`` / ``stackTrace``).
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(
|
|
28
|
+
self,
|
|
29
|
+
ok: bool,
|
|
30
|
+
payload: Any = None,
|
|
31
|
+
error: Optional[Dict[str, Any]] = None,
|
|
32
|
+
request_id: str = "",
|
|
33
|
+
cold_start: bool = False,
|
|
34
|
+
) -> None:
|
|
35
|
+
self.ok = ok
|
|
36
|
+
self.payload = payload
|
|
37
|
+
self.error = error
|
|
38
|
+
self.request_id = request_id
|
|
39
|
+
self.cold_start = cold_start
|
|
40
|
+
|
|
41
|
+
def __repr__(self) -> str: # pragma: no cover - debug convenience
|
|
42
|
+
state = "ok" if self.ok else "error"
|
|
43
|
+
return f"<InvocationResult {state} request_id={self.request_id!r}>"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _run_with_timeout(
|
|
47
|
+
func: Callable, args: tuple, timeout_seconds: float
|
|
48
|
+
) -> Any:
|
|
49
|
+
"""Run ``func`` in a worker thread, raising LambdaTimeout on overrun.
|
|
50
|
+
|
|
51
|
+
A thread is used (not a signal) so the harness works off the main thread
|
|
52
|
+
— inside pytest, web servers, notebooks — where signal-based timeouts
|
|
53
|
+
cannot be installed. The worker is a daemon: if it overruns we stop
|
|
54
|
+
waiting and surface the timeout, mirroring how AWS abandons the invoke.
|
|
55
|
+
"""
|
|
56
|
+
result: Dict[str, Any] = {}
|
|
57
|
+
|
|
58
|
+
def target() -> None:
|
|
59
|
+
try:
|
|
60
|
+
result["value"] = func(*args)
|
|
61
|
+
except BaseException as exc: # noqa: BLE001 - captured for re-raise
|
|
62
|
+
result["error"] = exc
|
|
63
|
+
|
|
64
|
+
worker = threading.Thread(target=target, daemon=True)
|
|
65
|
+
worker.start()
|
|
66
|
+
worker.join(timeout_seconds)
|
|
67
|
+
|
|
68
|
+
if worker.is_alive():
|
|
69
|
+
raise LambdaTimeout(
|
|
70
|
+
f"Task timed out after {timeout_seconds:.2f} seconds"
|
|
71
|
+
)
|
|
72
|
+
if "error" in result:
|
|
73
|
+
raise result["error"]
|
|
74
|
+
return result.get("value")
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class LambdaHarness:
|
|
78
|
+
"""Invoke a Lambda handler in-process with AWS-faithful semantics.
|
|
79
|
+
|
|
80
|
+
The handler is resolved lazily on first invoke to reproduce cold-start
|
|
81
|
+
timing, and the imported module is reused across invokes so module-global
|
|
82
|
+
state (connection pools, caches) behaves as it does in a warm container.
|
|
83
|
+
|
|
84
|
+
An optional ``iam`` evaluator, when provided, is exposed to the boto3
|
|
85
|
+
intercept hook so calls made inside the handler can be authorized against
|
|
86
|
+
local policy. The harness itself stays runtime-focused; IAM is delegated.
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
def __init__(
|
|
90
|
+
self,
|
|
91
|
+
handler_path: str,
|
|
92
|
+
function_name: str = "local-function",
|
|
93
|
+
function_version: str = "$LATEST",
|
|
94
|
+
memory_mb: int = 128,
|
|
95
|
+
timeout: float = 3.0,
|
|
96
|
+
region: str = "us-east-1",
|
|
97
|
+
account_id: str = "000000000000",
|
|
98
|
+
environment: Optional[Dict[str, str]] = None,
|
|
99
|
+
iam: Any = None,
|
|
100
|
+
) -> None:
|
|
101
|
+
self.handler_path = handler_path
|
|
102
|
+
self.function_name = function_name
|
|
103
|
+
self.function_version = function_version
|
|
104
|
+
self.memory_mb = memory_mb
|
|
105
|
+
self.timeout = timeout
|
|
106
|
+
self.region = region
|
|
107
|
+
self.account_id = account_id
|
|
108
|
+
self.user_environment = dict(environment or {})
|
|
109
|
+
self.iam = iam
|
|
110
|
+
|
|
111
|
+
self._handler: Optional[Callable] = None
|
|
112
|
+
self._has_run = False # tracks cold vs warm
|
|
113
|
+
|
|
114
|
+
def _ensure_handler(self) -> Callable:
|
|
115
|
+
"""Resolve the handler on first use (cold start)."""
|
|
116
|
+
if self._handler is None:
|
|
117
|
+
self._handler = resolve_handler(self.handler_path)
|
|
118
|
+
return self._handler
|
|
119
|
+
|
|
120
|
+
def invoke(
|
|
121
|
+
self, event: Any, client_context: Any = None
|
|
122
|
+
) -> InvocationResult:
|
|
123
|
+
"""Invoke the handler with ``event`` and an AWS-faithful context."""
|
|
124
|
+
is_cold = not self._has_run
|
|
125
|
+
handler = self._ensure_handler()
|
|
126
|
+
self._has_run = True
|
|
127
|
+
|
|
128
|
+
context = LambdaContext(
|
|
129
|
+
function_name=self.function_name,
|
|
130
|
+
function_version=self.function_version,
|
|
131
|
+
memory_limit_in_mb=self.memory_mb,
|
|
132
|
+
timeout_seconds=self.timeout,
|
|
133
|
+
region=self.region,
|
|
134
|
+
account_id=self.account_id,
|
|
135
|
+
)
|
|
136
|
+
if client_context is not None:
|
|
137
|
+
context.client_context = client_context
|
|
138
|
+
|
|
139
|
+
lambda_env = build_lambda_environ(
|
|
140
|
+
function_name=self.function_name,
|
|
141
|
+
function_version=self.function_version,
|
|
142
|
+
memory_limit_in_mb=self.memory_mb,
|
|
143
|
+
region=self.region,
|
|
144
|
+
handler_path=self.handler_path,
|
|
145
|
+
log_group_name=context.log_group_name,
|
|
146
|
+
log_stream_name=context.log_stream_name,
|
|
147
|
+
)
|
|
148
|
+
lambda_env.update(self.user_environment)
|
|
149
|
+
|
|
150
|
+
with lambda_environment(lambda_env):
|
|
151
|
+
try:
|
|
152
|
+
payload = _run_with_timeout(
|
|
153
|
+
handler, (event, context), self.timeout
|
|
154
|
+
)
|
|
155
|
+
return InvocationResult(
|
|
156
|
+
ok=True,
|
|
157
|
+
payload=payload,
|
|
158
|
+
request_id=context.aws_request_id,
|
|
159
|
+
cold_start=is_cold,
|
|
160
|
+
)
|
|
161
|
+
except BaseException as exc: # noqa: BLE001 - serialized like AWS
|
|
162
|
+
return InvocationResult(
|
|
163
|
+
ok=False,
|
|
164
|
+
error=format_exception(exc),
|
|
165
|
+
request_id=context.aws_request_id,
|
|
166
|
+
cold_start=is_cold,
|
|
167
|
+
)
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""Handler resolution and Lambda environment reproduction.
|
|
2
|
+
|
|
3
|
+
Two responsibilities:
|
|
4
|
+
|
|
5
|
+
1. Resolve a ``module.function`` handler string the way the AWS runtime does,
|
|
6
|
+
importing the module (which runs init / cold-start code) and returning the
|
|
7
|
+
callable.
|
|
8
|
+
2. Inject the environment variables the Lambda runtime sets, so handler code
|
|
9
|
+
that reads ``os.environ`` sees the same keys it would in AWS.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import importlib
|
|
15
|
+
import os
|
|
16
|
+
from contextlib import contextmanager
|
|
17
|
+
from typing import Callable, Dict, Iterator
|
|
18
|
+
|
|
19
|
+
from .errors import HandlerLoadError
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def resolve_handler(handler_path: str) -> Callable:
|
|
23
|
+
"""Resolve a ``module.function`` string to a callable.
|
|
24
|
+
|
|
25
|
+
``app.handler`` imports module ``app`` and returns its ``handler``
|
|
26
|
+
attribute. Dotted modules (``pkg.mod.handler``) are supported: the final
|
|
27
|
+
segment is the function, everything before it is the module path.
|
|
28
|
+
"""
|
|
29
|
+
if "." not in handler_path:
|
|
30
|
+
raise HandlerLoadError(
|
|
31
|
+
f"Handler '{handler_path}' must be of the form 'module.function'"
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
module_name, _, func_name = handler_path.rpartition(".")
|
|
35
|
+
try:
|
|
36
|
+
module = importlib.import_module(module_name) # runs cold-start init
|
|
37
|
+
except ImportError as exc:
|
|
38
|
+
raise HandlerLoadError(
|
|
39
|
+
f"Unable to import module '{module_name}': {exc}"
|
|
40
|
+
) from exc
|
|
41
|
+
|
|
42
|
+
try:
|
|
43
|
+
handler = getattr(module, func_name)
|
|
44
|
+
except AttributeError as exc:
|
|
45
|
+
raise HandlerLoadError(
|
|
46
|
+
f"Module '{module_name}' has no attribute '{func_name}'"
|
|
47
|
+
) from exc
|
|
48
|
+
|
|
49
|
+
if not callable(handler):
|
|
50
|
+
raise HandlerLoadError(f"Handler '{handler_path}' is not callable")
|
|
51
|
+
|
|
52
|
+
return handler
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def build_lambda_environ(
|
|
56
|
+
function_name: str,
|
|
57
|
+
function_version: str,
|
|
58
|
+
memory_limit_in_mb: int,
|
|
59
|
+
region: str,
|
|
60
|
+
handler_path: str,
|
|
61
|
+
log_group_name: str,
|
|
62
|
+
log_stream_name: str,
|
|
63
|
+
) -> Dict[str, str]:
|
|
64
|
+
"""Return the environment variables the Lambda runtime injects."""
|
|
65
|
+
return {
|
|
66
|
+
"AWS_LAMBDA_FUNCTION_NAME": function_name,
|
|
67
|
+
"AWS_LAMBDA_FUNCTION_VERSION": function_version,
|
|
68
|
+
"AWS_LAMBDA_FUNCTION_MEMORY_SIZE": str(memory_limit_in_mb),
|
|
69
|
+
"AWS_LAMBDA_LOG_GROUP_NAME": log_group_name,
|
|
70
|
+
"AWS_LAMBDA_LOG_STREAM_NAME": log_stream_name,
|
|
71
|
+
"AWS_REGION": region,
|
|
72
|
+
"AWS_DEFAULT_REGION": region,
|
|
73
|
+
"AWS_EXECUTION_ENV": "AWS_Lambda_python",
|
|
74
|
+
"_HANDLER": handler_path,
|
|
75
|
+
"TZ": "UTC",
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@contextmanager
|
|
80
|
+
def lambda_environment(env: Dict[str, str]) -> Iterator[None]:
|
|
81
|
+
"""Temporarily apply Lambda env vars, restoring prior values on exit."""
|
|
82
|
+
previous: Dict[str, str] = {}
|
|
83
|
+
missing = object()
|
|
84
|
+
try:
|
|
85
|
+
for key, value in env.items():
|
|
86
|
+
previous[key] = os.environ.get(key, missing) # type: ignore[assignment]
|
|
87
|
+
os.environ[key] = value
|
|
88
|
+
yield
|
|
89
|
+
finally:
|
|
90
|
+
for key, prior in previous.items():
|
|
91
|
+
if prior is missing:
|
|
92
|
+
os.environ.pop(key, None)
|
|
93
|
+
else:
|
|
94
|
+
os.environ[key] = prior # type: ignore[arg-type]
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""pytest fixtures for cloud-free Lambda testing.
|
|
2
|
+
|
|
3
|
+
Importing this module in a ``conftest.py`` (or via the pytest plugin entry
|
|
4
|
+
point) exposes fixtures that make the biggest use case — fast, charge-free
|
|
5
|
+
tests in CI — a one-liner.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Callable
|
|
11
|
+
|
|
12
|
+
try:
|
|
13
|
+
import pytest
|
|
14
|
+
except ImportError: # pragma: no cover - pytest optional at runtime
|
|
15
|
+
pytest = None # type: ignore[assignment]
|
|
16
|
+
|
|
17
|
+
from .. import events as _events
|
|
18
|
+
from ..runtime import LambdaHarness
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
if pytest is not None:
|
|
22
|
+
|
|
23
|
+
@pytest.fixture
|
|
24
|
+
def lambda_harness() -> Callable[..., LambdaHarness]:
|
|
25
|
+
"""Factory fixture: build a harness for a given handler path.
|
|
26
|
+
|
|
27
|
+
Example::
|
|
28
|
+
|
|
29
|
+
def test_orders(lambda_harness):
|
|
30
|
+
harness = lambda_harness("app.handler", timeout=5)
|
|
31
|
+
result = harness.invoke({"hello": "world"})
|
|
32
|
+
assert result.ok
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
def _factory(handler_path: str, **kwargs: object) -> LambdaHarness:
|
|
36
|
+
return LambdaHarness(handler_path, **kwargs) # type: ignore[arg-type]
|
|
37
|
+
|
|
38
|
+
return _factory
|
|
39
|
+
|
|
40
|
+
@pytest.fixture
|
|
41
|
+
def lambda_events():
|
|
42
|
+
"""Expose the event builders as a fixture for convenience."""
|
|
43
|
+
return _events
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: lambda-preflight
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Catch AWS Lambda IAM permission errors and event-shape bugs before you deploy — a local pre-flight linter and AWS-faithful runtime. No Docker, no SAM, no invocation charges.
|
|
5
|
+
Project-URL: Homepage, https://github.com/swaranshu-borgaonkar/lambda-preflight
|
|
6
|
+
Project-URL: Issues, https://github.com/swaranshu-borgaonkar/lambda-preflight/issues
|
|
7
|
+
Author: Swar Borgaonkar
|
|
8
|
+
License: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: aws,iam,lambda,linter,local,mock,preflight,pytest,serverless,testing
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
21
|
+
Classifier: Topic :: Software Development :: Testing
|
|
22
|
+
Requires-Python: >=3.8
|
|
23
|
+
Provides-Extra: aws
|
|
24
|
+
Requires-Dist: boto3>=1.26; extra == 'aws'
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: boto3>=1.26; extra == 'dev'
|
|
27
|
+
Requires-Dist: build; extra == 'dev'
|
|
28
|
+
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
29
|
+
Requires-Dist: twine; extra == 'dev'
|
|
30
|
+
Description-Content-Type: text/markdown
|
|
31
|
+
|
|
32
|
+
# lambda-preflight
|
|
33
|
+
|
|
34
|
+
**Catch AWS Lambda IAM permission errors and event-shape bugs before you deploy.**
|
|
35
|
+
|
|
36
|
+
Every "local lambda" tool runs your handler. `lambda-preflight` does something none of them do: it **checks your handler's AWS calls against your execution role's real IAM policies** and fails locally — so you find the missing `s3:PutObject` on your laptop, not in a CloudWatch log after a failed deploy.
|
|
37
|
+
|
|
38
|
+
No Docker. No SAM CLI. No cloud invocations, so no invocation charges.
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
pip install "lambda-preflight[aws]"
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## The headline: local IAM linting
|
|
47
|
+
|
|
48
|
+
Your Lambda runs as an execution *role*. When that role is missing a permission, you normally find out the slow way — deploy, invoke, read the `AccessDenied` in the logs, redeploy. `lambda-preflight` pulls your role's **real** policies once, caches them, and checks every AWS call your handler makes *before* anything leaves your machine.
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
from lambda_preflight import pull_role, PolicyEvaluator, intercept_boto3
|
|
52
|
+
from lambda_preflight import LambdaHarness, events
|
|
53
|
+
|
|
54
|
+
# Pull the execution role's real policies (managed + inline + boundary), cached after first call.
|
|
55
|
+
policies = pull_role("my-function-exec-role")
|
|
56
|
+
evaluator = PolicyEvaluator(policies, strict=True)
|
|
57
|
+
|
|
58
|
+
harness = LambdaHarness("app.handler", timeout=30)
|
|
59
|
+
|
|
60
|
+
# boto3 calls inside the handler are authorized against the real policy — locally.
|
|
61
|
+
with intercept_boto3(evaluator):
|
|
62
|
+
result = harness.invoke(events.sqs(body={"orderId": 123}))
|
|
63
|
+
# If app.handler calls s3:PutObject and the role can't → AccessDenied raised here,
|
|
64
|
+
# with the same reason AWS would give, before you ever deploy.
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Three ways to load policies, so it works on a plane or in locked-down accounts:
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
from lambda_preflight import pull_role, load_role_policies, PolicySet
|
|
71
|
+
|
|
72
|
+
pull_role("my-exec-role") # 1. live pull — needs creds + IAM read, caches result
|
|
73
|
+
load_role_policies("my-exec-role") # 2. offline — reuse cache, no AWS call, no charge
|
|
74
|
+
PolicySet.from_policy_documents([{"Statement": [...]}]) # 3. BYO JSON — when you can't pull
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
It models the parts that actually decide real permissions: explicit-deny-wins, implicit deny, wildcard actions, resource matching, permission-boundary intersection, and common condition operators. What it **can't** faithfully evaluate (uncommon condition keys, SCPs, resource-based policies) it **flags** — in `strict=True` mode it raises `CannotEvaluate` rather than returning a false green. It tells you the edge of its own knowledge instead of guessing.
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## The foundation: an AWS-faithful local runtime
|
|
82
|
+
|
|
83
|
+
The linter needs something real to run against, so the runtime half is built to match AWS's actual invocation contract — not a stubbed approximation.
|
|
84
|
+
|
|
85
|
+
### Schema-accurate event fixtures
|
|
86
|
+
|
|
87
|
+
The #1 cause of "worked locally, broke in AWS" is a test event whose *shape* is wrong. These builders emit payloads matching AWS's real schemas, so what you test is what you'll get. REST (v1) and HTTP (v2) API Gateway differ meaningfully — they're separate on purpose:
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
events.api_gateway_v1(method="GET", path="/health")
|
|
91
|
+
events.api_gateway_v2(method="POST", path="/orders", body={"id": 1})
|
|
92
|
+
events.sqs(records=[{"a": 1}, {"b": 2}]) # real batch shape, messageAttributes, md5
|
|
93
|
+
events.sns(message={"x": 1}, subject="hi")
|
|
94
|
+
events.s3(bucket="b", key="k.txt", event_name="ObjectCreated:Put")
|
|
95
|
+
events.eventbridge(detail={"k": "v"}, detail_type="my.type", source="svc")
|
|
96
|
+
events.dynamodb_stream(keys={"id": {"S": "1"}}, event_name="INSERT")
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Faithful invocation semantics
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
result = harness.invoke(event)
|
|
103
|
+
result.ok # success vs. error
|
|
104
|
+
result.payload # what the handler returned
|
|
105
|
+
result.error # {errorType, errorMessage, stackTrace} — AWS's exact envelope
|
|
106
|
+
result.cold_start # models warm-container reuse across invokes
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
The `context` reproduces real fields (`aws_request_id`, `invoked_function_arn`, log names) and a live `get_remaining_time_in_millis()` countdown. Timeouts are enforced the way AWS enforces them. Lambda's environment variables are injected so handler code that reads them behaves the same.
|
|
110
|
+
|
|
111
|
+
### pytest, because CI is the point
|
|
112
|
+
|
|
113
|
+
Fixtures register automatically — fast, cloud-free, zero-charge tests:
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
def test_orders(lambda_harness, lambda_events):
|
|
117
|
+
harness = lambda_harness("app.handler", timeout=5)
|
|
118
|
+
result = harness.invoke(lambda_events.sqs(body={"order": 42}))
|
|
119
|
+
assert result.ok
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
## How this differs from other local-lambda tools
|
|
125
|
+
|
|
126
|
+
Tools like `python-lambda-local` and `local-aws-lambda` are **runners** — they execute your handler against an event dict you supply. That's the commodity half, and it's solved. `lambda-preflight` includes a competent runner too, but its reason to exist is what sits on top:
|
|
127
|
+
|
|
128
|
+
| | Typical local-lambda tools | lambda-preflight |
|
|
129
|
+
|---|---|---|
|
|
130
|
+
| Run a handler locally | ✅ | ✅ |
|
|
131
|
+
| Schema-accurate event fixtures | ✋ you write the dict | ✅ built-in, v1 ≠ v2 |
|
|
132
|
+
| Faithful context / timeout / cold-start / AWS-shape errors | partial | ✅ |
|
|
133
|
+
| **IAM permission linting against your real role** | ❌ | ✅ **unique** |
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
## Honest scope
|
|
138
|
+
|
|
139
|
+
This kills the **shape / contract / runtime-semantics / identity-permission** class of "works locally, breaks in AWS" failures — which is most of the day-to-day pain. It does **not**, and a local tool cannot, reproduce cloud-only realities: cold-start latency, live concurrency, account quotas, **SCPs**, and **resource-based policies** (the last two are recorded in `PolicySet.unscoped`, never silently ignored). The IAM layer is a **pre-flight linter, not a parity guarantee**. For final confidence, run against real AWS credentials.
|
|
140
|
+
|
|
141
|
+
## License
|
|
142
|
+
|
|
143
|
+
MIT
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
lambda_preflight/__init__.py,sha256=FbDt6uVCDxh4MMGSUxXUncu50jSMygcptFopZX35OjQ,1768
|
|
2
|
+
lambda_preflight/events/__init__.py,sha256=vorVEw2hz6exYOni_j92xNvae0kGXng6Vc4KePc6ohM,329
|
|
3
|
+
lambda_preflight/events/builders.py,sha256=mVq0FD9Gi6foLj51dreQGY9BZlkdax81X874piwnonE,9254
|
|
4
|
+
lambda_preflight/iam/__init__.py,sha256=0Gosb7vDCOzyQ26jp74SKcG8DSNEaH9bzfL95YQSOwA,904
|
|
5
|
+
lambda_preflight/iam/conditions.py,sha256=sKM8JR_xbh6AMv_AYf1bEEXWdJPz4hNqVuUA1Rwuge0,4293
|
|
6
|
+
lambda_preflight/iam/evaluator.py,sha256=XVCzT_IMKv29eGB3Yyzw6w9ra5AcEKl36F51meT1RL4,5674
|
|
7
|
+
lambda_preflight/iam/intercept.py,sha256=8JH9F3yqVZqSG_VlWWej8LKuXH1tPPaj1poGZmPvAlo,3309
|
|
8
|
+
lambda_preflight/iam/model.py,sha256=mD6Q-fXRR5n2sQ_3nsrWBTD7IZxDo_VDmNbRJPBUKio,2775
|
|
9
|
+
lambda_preflight/iam/puller.py,sha256=aIBum1h7JmX5fuczvQZnDF5hnVmvP4UlxswKpB78JP0,6436
|
|
10
|
+
lambda_preflight/runtime/__init__.py,sha256=VjnipgnmZdJtDAKWgryY4EQwAAZw0fmCx8ccGcjZ1hY,305
|
|
11
|
+
lambda_preflight/runtime/context.py,sha256=v4YFekSzfmwOC4WtkWROdEnfG5ZO-3pJbee_niAGXlU,2649
|
|
12
|
+
lambda_preflight/runtime/errors.py,sha256=1HnAImw3deXkiWR2ubUX2AZlcrlmXHSaXjOGoLUFD_c,1094
|
|
13
|
+
lambda_preflight/runtime/harness.py,sha256=GDxIbXcoJwpiuyx5UBCCyL-bslJW8xZMznWMqooRcQY,5926
|
|
14
|
+
lambda_preflight/runtime/loader.py,sha256=wj7YSs6OHU76ahScBB_EjTEGF_7PN2tbPFdnfqPISHA,3084
|
|
15
|
+
lambda_preflight/testing/__init__.py,sha256=u54M4Sd3AhNvBWyBez2j9eGolYRT1MDPm0w2kwoXBEk,116
|
|
16
|
+
lambda_preflight/testing/fixtures.py,sha256=OJa40rxmQa7IEZAwLmO7wciGtJeDnRhwFQks8kbRzg0,1245
|
|
17
|
+
lambda_preflight-0.1.0.dist-info/METADATA,sha256=SdaAI9BqE4gItoSQWvW3QGm8o_sS7DBLkhFSHXuV-YQ,7171
|
|
18
|
+
lambda_preflight-0.1.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
19
|
+
lambda_preflight-0.1.0.dist-info/entry_points.txt,sha256=Wn-5svchMAOdRq1EvqjXbJe9qRhfCOAhG9WUKRcZYag,64
|
|
20
|
+
lambda_preflight-0.1.0.dist-info/licenses/LICENSE,sha256=twrIGE-FkR0x4T1XsB1cD4_vsd_J9vy7uLWiB8XdMsA,1072
|
|
21
|
+
lambda_preflight-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Swar Borgaonkar
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|