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.
@@ -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,3 @@
1
+ """Testing helpers and pytest fixtures."""
2
+
3
+ from .fixtures import * # noqa: F401,F403 (registers pytest fixtures)
@@ -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,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.27.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [pytest11]
2
+ lambda_preflight = lambda_preflight.testing.fixtures
@@ -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.