lambda-preflight 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 (24) hide show
  1. lambda_preflight-0.1.0/LICENSE +21 -0
  2. lambda_preflight-0.1.0/PKG-INFO +143 -0
  3. lambda_preflight-0.1.0/README.md +112 -0
  4. lambda_preflight-0.1.0/pyproject.toml +55 -0
  5. lambda_preflight-0.1.0/src/lambda_preflight/__init__.py +58 -0
  6. lambda_preflight-0.1.0/src/lambda_preflight/events/__init__.py +21 -0
  7. lambda_preflight-0.1.0/src/lambda_preflight/events/builders.py +287 -0
  8. lambda_preflight-0.1.0/src/lambda_preflight/iam/__init__.py +32 -0
  9. lambda_preflight-0.1.0/src/lambda_preflight/iam/conditions.py +131 -0
  10. lambda_preflight-0.1.0/src/lambda_preflight/iam/evaluator.py +177 -0
  11. lambda_preflight-0.1.0/src/lambda_preflight/iam/intercept.py +101 -0
  12. lambda_preflight-0.1.0/src/lambda_preflight/iam/model.py +90 -0
  13. lambda_preflight-0.1.0/src/lambda_preflight/iam/puller.py +190 -0
  14. lambda_preflight-0.1.0/src/lambda_preflight/runtime/__init__.py +13 -0
  15. lambda_preflight-0.1.0/src/lambda_preflight/runtime/context.py +75 -0
  16. lambda_preflight-0.1.0/src/lambda_preflight/runtime/errors.py +34 -0
  17. lambda_preflight-0.1.0/src/lambda_preflight/runtime/harness.py +167 -0
  18. lambda_preflight-0.1.0/src/lambda_preflight/runtime/loader.py +94 -0
  19. lambda_preflight-0.1.0/src/lambda_preflight/testing/__init__.py +3 -0
  20. lambda_preflight-0.1.0/src/lambda_preflight/testing/fixtures.py +43 -0
  21. lambda_preflight-0.1.0/tests/sample_handlers.py +34 -0
  22. lambda_preflight-0.1.0/tests/test_events.py +85 -0
  23. lambda_preflight-0.1.0/tests/test_iam.py +138 -0
  24. lambda_preflight-0.1.0/tests/test_runtime.py +68 -0
@@ -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.
@@ -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,112 @@
1
+ # lambda-preflight
2
+
3
+ **Catch AWS Lambda IAM permission errors and event-shape bugs before you deploy.**
4
+
5
+ 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.
6
+
7
+ No Docker. No SAM CLI. No cloud invocations, so no invocation charges.
8
+
9
+ ```bash
10
+ pip install "lambda-preflight[aws]"
11
+ ```
12
+
13
+ ---
14
+
15
+ ## The headline: local IAM linting
16
+
17
+ 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.
18
+
19
+ ```python
20
+ from lambda_preflight import pull_role, PolicyEvaluator, intercept_boto3
21
+ from lambda_preflight import LambdaHarness, events
22
+
23
+ # Pull the execution role's real policies (managed + inline + boundary), cached after first call.
24
+ policies = pull_role("my-function-exec-role")
25
+ evaluator = PolicyEvaluator(policies, strict=True)
26
+
27
+ harness = LambdaHarness("app.handler", timeout=30)
28
+
29
+ # boto3 calls inside the handler are authorized against the real policy — locally.
30
+ with intercept_boto3(evaluator):
31
+ result = harness.invoke(events.sqs(body={"orderId": 123}))
32
+ # If app.handler calls s3:PutObject and the role can't → AccessDenied raised here,
33
+ # with the same reason AWS would give, before you ever deploy.
34
+ ```
35
+
36
+ Three ways to load policies, so it works on a plane or in locked-down accounts:
37
+
38
+ ```python
39
+ from lambda_preflight import pull_role, load_role_policies, PolicySet
40
+
41
+ pull_role("my-exec-role") # 1. live pull — needs creds + IAM read, caches result
42
+ load_role_policies("my-exec-role") # 2. offline — reuse cache, no AWS call, no charge
43
+ PolicySet.from_policy_documents([{"Statement": [...]}]) # 3. BYO JSON — when you can't pull
44
+ ```
45
+
46
+ 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.
47
+
48
+ ---
49
+
50
+ ## The foundation: an AWS-faithful local runtime
51
+
52
+ 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.
53
+
54
+ ### Schema-accurate event fixtures
55
+
56
+ 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:
57
+
58
+ ```python
59
+ events.api_gateway_v1(method="GET", path="/health")
60
+ events.api_gateway_v2(method="POST", path="/orders", body={"id": 1})
61
+ events.sqs(records=[{"a": 1}, {"b": 2}]) # real batch shape, messageAttributes, md5
62
+ events.sns(message={"x": 1}, subject="hi")
63
+ events.s3(bucket="b", key="k.txt", event_name="ObjectCreated:Put")
64
+ events.eventbridge(detail={"k": "v"}, detail_type="my.type", source="svc")
65
+ events.dynamodb_stream(keys={"id": {"S": "1"}}, event_name="INSERT")
66
+ ```
67
+
68
+ ### Faithful invocation semantics
69
+
70
+ ```python
71
+ result = harness.invoke(event)
72
+ result.ok # success vs. error
73
+ result.payload # what the handler returned
74
+ result.error # {errorType, errorMessage, stackTrace} — AWS's exact envelope
75
+ result.cold_start # models warm-container reuse across invokes
76
+ ```
77
+
78
+ 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.
79
+
80
+ ### pytest, because CI is the point
81
+
82
+ Fixtures register automatically — fast, cloud-free, zero-charge tests:
83
+
84
+ ```python
85
+ def test_orders(lambda_harness, lambda_events):
86
+ harness = lambda_harness("app.handler", timeout=5)
87
+ result = harness.invoke(lambda_events.sqs(body={"order": 42}))
88
+ assert result.ok
89
+ ```
90
+
91
+ ---
92
+
93
+ ## How this differs from other local-lambda tools
94
+
95
+ 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:
96
+
97
+ | | Typical local-lambda tools | lambda-preflight |
98
+ |---|---|---|
99
+ | Run a handler locally | ✅ | ✅ |
100
+ | Schema-accurate event fixtures | ✋ you write the dict | ✅ built-in, v1 ≠ v2 |
101
+ | Faithful context / timeout / cold-start / AWS-shape errors | partial | ✅ |
102
+ | **IAM permission linting against your real role** | ❌ | ✅ **unique** |
103
+
104
+ ---
105
+
106
+ ## Honest scope
107
+
108
+ 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.
109
+
110
+ ## License
111
+
112
+ MIT
@@ -0,0 +1,55 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "lambda-preflight"
7
+ version = "0.1.0"
8
+ description = "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."
9
+ readme = "README.md"
10
+ requires-python = ">=3.8"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "Swar Borgaonkar" }]
13
+ keywords = [
14
+ "iam",
15
+ "linter",
16
+ "preflight",
17
+ "aws",
18
+ "lambda",
19
+ "serverless",
20
+ "local",
21
+ "testing",
22
+ "mock",
23
+ "pytest",
24
+ ]
25
+ classifiers = [
26
+ "Development Status :: 4 - Beta",
27
+ "Intended Audience :: Developers",
28
+ "License :: OSI Approved :: MIT License",
29
+ "Programming Language :: Python :: 3",
30
+ "Programming Language :: Python :: 3.8",
31
+ "Programming Language :: Python :: 3.9",
32
+ "Programming Language :: Python :: 3.10",
33
+ "Programming Language :: Python :: 3.11",
34
+ "Programming Language :: Python :: 3.12",
35
+ "Topic :: Software Development :: Testing",
36
+ "Topic :: Software Development :: Libraries :: Python Modules",
37
+ ]
38
+ dependencies = []
39
+
40
+ [project.optional-dependencies]
41
+ aws = ["boto3>=1.26"]
42
+ dev = ["pytest>=7.0", "boto3>=1.26", "build", "twine"]
43
+
44
+ [project.urls]
45
+ Homepage = "https://github.com/swaranshu-borgaonkar/lambda-preflight"
46
+ Issues = "https://github.com/swaranshu-borgaonkar/lambda-preflight/issues"
47
+
48
+ [project.entry-points.pytest11]
49
+ lambda_preflight = "lambda_preflight.testing.fixtures"
50
+
51
+ [tool.hatch.build.targets.wheel]
52
+ packages = ["src/lambda_preflight"]
53
+
54
+ [tool.pytest.ini_options]
55
+ testpaths = ["tests"]
@@ -0,0 +1,58 @@
1
+ """lambda_preflight — catch AWS Lambda IAM and event-shape bugs before you deploy.
2
+
3
+ The headline feature is a local **IAM linter**: it evaluates the AWS calls your
4
+ handler makes against your execution role's *real, pulled* policies and raises
5
+ AccessDenied locally — catching the permission mistakes you'd otherwise only
6
+ discover after deploying. Nothing else in the ecosystem does this.
7
+
8
+ Around it sits an AWS-faithful local runtime so the linter has something real to
9
+ run against: schema-accurate event fixtures, a faithful context object, timeout
10
+ enforcement, cold/warm semantics, and AWS-shape error envelopes. No Docker, no
11
+ SAM CLI, no cloud invocations (no invocation charges).
12
+
13
+ Honest scope: this eliminates the shape/contract/runtime-semantics class of
14
+ "works locally, breaks in AWS" failures. It does not and cannot reproduce
15
+ cold-start latency, live concurrency, SCPs, resource-based policies, or
16
+ account quotas — those exist only in the cloud. The IAM layer is a fast
17
+ pre-flight linter, not a parity guarantee, and fails loud (strict mode) on
18
+ anything it cannot faithfully evaluate.
19
+ """
20
+
21
+ from . import events
22
+ from .iam import (
23
+ AccessDenied,
24
+ CannotEvaluate,
25
+ PolicyEvaluator,
26
+ PolicySet,
27
+ PullError,
28
+ intercept_boto3,
29
+ load_role_policies,
30
+ pull_role,
31
+ )
32
+ from .runtime import (
33
+ HandlerLoadError,
34
+ InvocationResult,
35
+ LambdaContext,
36
+ LambdaHarness,
37
+ LambdaTimeout,
38
+ )
39
+
40
+ __version__ = "0.1.0"
41
+
42
+ __all__ = [
43
+ "LambdaHarness",
44
+ "InvocationResult",
45
+ "LambdaContext",
46
+ "LambdaTimeout",
47
+ "HandlerLoadError",
48
+ "events",
49
+ "PolicyEvaluator",
50
+ "PolicySet",
51
+ "pull_role",
52
+ "load_role_policies",
53
+ "intercept_boto3",
54
+ "AccessDenied",
55
+ "CannotEvaluate",
56
+ "PullError",
57
+ "__version__",
58
+ ]
@@ -0,0 +1,21 @@
1
+ """Schema-accurate event builders for common Lambda triggers."""
2
+
3
+ from .builders import (
4
+ api_gateway_v1,
5
+ api_gateway_v2,
6
+ dynamodb_stream,
7
+ eventbridge,
8
+ s3,
9
+ sns,
10
+ sqs,
11
+ )
12
+
13
+ __all__ = [
14
+ "api_gateway_v1",
15
+ "api_gateway_v2",
16
+ "sqs",
17
+ "sns",
18
+ "s3",
19
+ "eventbridge",
20
+ "dynamodb_stream",
21
+ ]