modmex-lambda 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.
Files changed (48) hide show
  1. modmex_lambda/__init__.py +62 -0
  2. modmex_lambda/data_classes/__init__.py +49 -0
  3. modmex_lambda/data_classes/api_gateway_authorizer_event.py +38 -0
  4. modmex_lambda/data_classes/api_gateway_proxy_event.py +328 -0
  5. modmex_lambda/data_classes/api_gateway_websocket_event.py +40 -0
  6. modmex_lambda/data_classes/cognito_user_pool_event.py +599 -0
  7. modmex_lambda/data_classes/common.py +441 -0
  8. modmex_lambda/event_handler/__init__.py +45 -0
  9. modmex_lambda/event_handler/api_gateway.py +331 -0
  10. modmex_lambda/event_handler/constants.py +3 -0
  11. modmex_lambda/event_handler/content_types.py +13 -0
  12. modmex_lambda/event_handler/cors.py +97 -0
  13. modmex_lambda/event_handler/dependencies/__init__.py +0 -0
  14. modmex_lambda/event_handler/dependencies/compat.py +231 -0
  15. modmex_lambda/event_handler/dependencies/dependant.py +279 -0
  16. modmex_lambda/event_handler/dependencies/dependency_middleware.py +423 -0
  17. modmex_lambda/event_handler/dependencies/depends.py +184 -0
  18. modmex_lambda/event_handler/dependencies/params.py +317 -0
  19. modmex_lambda/event_handler/dependencies/types.py +14 -0
  20. modmex_lambda/event_handler/exception_handler.py +70 -0
  21. modmex_lambda/event_handler/exceptions.py +72 -0
  22. modmex_lambda/event_handler/gateway_response.py +96 -0
  23. modmex_lambda/event_handler/middlewares.py +33 -0
  24. modmex_lambda/event_handler/params.py +44 -0
  25. modmex_lambda/event_handler/request.py +70 -0
  26. modmex_lambda/event_handler/response.py +60 -0
  27. modmex_lambda/event_handler/routing.py +507 -0
  28. modmex_lambda/event_handler/routing_fallbacks.py +92 -0
  29. modmex_lambda/event_handler/types.py +31 -0
  30. modmex_lambda/event_sources.py +53 -0
  31. modmex_lambda/exceptions.py +3 -0
  32. modmex_lambda/logging.py +99 -0
  33. modmex_lambda/params.py +3 -0
  34. modmex_lambda/parser.py +47 -0
  35. modmex_lambda/request.py +3 -0
  36. modmex_lambda/resolver.py +3 -0
  37. modmex_lambda/response.py +3 -0
  38. modmex_lambda/routing.py +3 -0
  39. modmex_lambda/shared/__init__.py +0 -0
  40. modmex_lambda/shared/cookies.py +84 -0
  41. modmex_lambda/shared/headers_serializer.py +65 -0
  42. modmex_lambda/shared/json_encoder.py +53 -0
  43. modmex_lambda/shared/types.py +4 -0
  44. modmex_lambda/validation.py +178 -0
  45. modmex_lambda-0.1.0.dist-info/METADATA +375 -0
  46. modmex_lambda-0.1.0.dist-info/RECORD +48 -0
  47. modmex_lambda-0.1.0.dist-info/WHEEL +4 -0
  48. modmex_lambda-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,375 @@
1
+ Metadata-Version: 2.4
2
+ Name: modmex-lambda
3
+ Version: 0.1.0
4
+ Summary: Ultra-lightweight AWS Lambda utilities for API Gateway routing and event handling.
5
+ Author: Modmex
6
+ License: MIT
7
+ License-File: LICENSE
8
+ Requires-Python: >=3.10
9
+ Requires-Dist: modmex<2.0.0,>=1.1.10
10
+ Provides-Extra: dev
11
+ Requires-Dist: pytest>=8.0; extra == 'dev'
12
+ Description-Content-Type: text/markdown
13
+
14
+ # modmex-lambda
15
+
16
+ Ultra-lightweight AWS Lambda utilities for API Gateway-first workloads.
17
+
18
+ `modmex-lambda` is a Lambda utility layer, not an ASGI framework. It focuses on
19
+ API Gateway proxy events, fast routing, request binding, response serialization,
20
+ middleware, dependency injection, event source wrappers, parsing, and structured
21
+ logging with a small dependency footprint.
22
+
23
+ ## Install
24
+
25
+ ```bash
26
+ pip install modmex-lambda
27
+ ```
28
+
29
+ For local development:
30
+
31
+ ```bash
32
+ poetry install --extras dev
33
+ poetry run pytest -q
34
+ ```
35
+
36
+ ## API Gateway Resolvers
37
+
38
+ Choose the resolver that matches the API Gateway payload version used by your
39
+ Lambda integration:
40
+
41
+ - `ApiGatewayRestResolver` for REST API payload v1.
42
+ - `ApiGatewayHttpResolver` for HTTP API payload v2 and Lambda Function URLs.
43
+
44
+ ```python
45
+ from modmex_lambda import ApiGatewayHttpResolver
46
+
47
+ app = ApiGatewayHttpResolver()
48
+
49
+
50
+ @app.get("/ping")
51
+ def ping():
52
+ return {"message": "pong"}
53
+
54
+
55
+ handler = app.handler
56
+ ```
57
+
58
+ The internal base resolver is intentionally not exported from the package root;
59
+ application code should select REST or HTTP explicitly.
60
+
61
+ ## Routing
62
+
63
+ Routes are declared with decorators:
64
+
65
+ ```python
66
+ @app.get("/users/<user_id>")
67
+ def get_user(user_id: int):
68
+ return {"user_id": user_id}
69
+
70
+
71
+ @app.post("/users", status_code=201)
72
+ def create_user():
73
+ return {"id": 42}
74
+ ```
75
+
76
+ Supported route decorators include `get`, `post`, `put`, `patch`, `delete`,
77
+ `options`, and `any`.
78
+
79
+ Routers can also strip deployment prefixes:
80
+
81
+ ```python
82
+ app = ApiGatewayHttpResolver(strip_prefixes=["/prod"])
83
+ ```
84
+
85
+ ## Request Binding
86
+
87
+ Use `typing.Annotated` with the public parameter markers:
88
+
89
+ - `Path()`
90
+ - `Query()`
91
+ - `Header()`
92
+ - `Cookie()`
93
+ - `Body()`
94
+
95
+ ```python
96
+ from typing import Annotated
97
+
98
+ from modmex import BaseModel
99
+ from modmex_lambda import ApiGatewayHttpResolver, Request
100
+ from modmex_lambda.event_handler.params import Body, Header, Path, Query
101
+
102
+ app = ApiGatewayHttpResolver()
103
+
104
+
105
+ class CreateUserRequest(BaseModel):
106
+ name: str
107
+ age: int | None = None
108
+
109
+
110
+ @app.post("/users", status_code=201)
111
+ def create_user(
112
+ payload: Annotated[CreateUserRequest, Body()],
113
+ tenant_id: Annotated[str, Header(name="x-tenant-id")],
114
+ request: Request,
115
+ ):
116
+ return {
117
+ "id": 42,
118
+ "tenant_id": tenant_id,
119
+ "route": request.route,
120
+ "payload": payload.model_dump(),
121
+ }
122
+
123
+
124
+ @app.get("/users/<user_id>")
125
+ def get_user(
126
+ user_id: Annotated[int, Path()],
127
+ include_orders: Annotated[bool, Query()] = False,
128
+ ):
129
+ return {"user_id": user_id, "include_orders": include_orders}
130
+ ```
131
+
132
+ For headers, simple scalar parameters can use `Header(name="x-header-name")`.
133
+ Header models are also supported; field names are exposed as dash-case aliases.
134
+
135
+ ```python
136
+ class HeaderModel(BaseModel):
137
+ x_tenant_id: str
138
+
139
+
140
+ @app.get("/me")
141
+ def me(headers: Annotated[HeaderModel, Header()]):
142
+ return {"tenant": headers.x_tenant_id}
143
+ ```
144
+
145
+ ## Responses
146
+
147
+ Route return values are converted to API Gateway proxy responses:
148
+
149
+ - `dict` and `list` become JSON responses.
150
+ - `str` becomes a text response.
151
+ - `bytes` are base64 encoded.
152
+ - `None` returns an empty response.
153
+ - `(body, status_code)` sets the response status.
154
+ - `Response` gives full control over status, headers, cookies, and content type.
155
+
156
+ ```python
157
+ from modmex_lambda import Response
158
+ from modmex_lambda.shared.cookies import Cookie
159
+
160
+
161
+ @app.get("/session")
162
+ def session():
163
+ return Response(
164
+ status_code=200,
165
+ body={"ok": True},
166
+ cookies=[Cookie("seen", "true", secure=True)],
167
+ )
168
+ ```
169
+
170
+ REST API responses use `multiValueHeaders`; HTTP API responses use the v2
171
+ `headers` and `cookies` shape.
172
+
173
+ ## Middleware
174
+
175
+ Middleware receives the resolver instance and a `next_middleware` callable.
176
+ Global middleware can be registered with `use` or `@app.middleware`; route
177
+ middleware can be attached per route.
178
+
179
+ ```python
180
+ from modmex_lambda import Response
181
+ from modmex_lambda.event_handler.middlewares import NextMiddleware
182
+
183
+
184
+ @app.middleware
185
+ def require_auth(app: ApiGatewayHttpResolver, next_middleware: NextMiddleware) -> Response:
186
+ if app.current_event.headers.get("x-auth") != "ok":
187
+ return Response(status_code=401, body={"message": "Unauthorized"})
188
+ return next_middleware(app)
189
+ ```
190
+
191
+ Middleware also wraps routing fallbacks, so `404` and `405` responses still flow
192
+ through the middleware chain.
193
+
194
+ ## Dependency Injection
195
+
196
+ `Depends` supports nested dependency trees, request-aware dependencies,
197
+ per-invocation caching, and overrides for tests.
198
+
199
+ ```python
200
+ from typing import Annotated
201
+
202
+ from modmex_lambda import Depends, Request
203
+ from modmex_lambda.event_handler.params import Path
204
+
205
+
206
+ class UserRepository:
207
+ def __init__(self, *, tenant_id: str, token: str):
208
+ self.tenant_id = tenant_id
209
+ self.token = token
210
+
211
+ def get_user(self, user_id: int) -> dict:
212
+ # Replace this with a database or service call.
213
+ return {"id": user_id, "tenant_id": self.tenant_id}
214
+
215
+
216
+ def get_token() -> str:
217
+ return "token"
218
+
219
+
220
+ def get_tenant_id(request: Request) -> str:
221
+ return request.headers["x-tenant-id"]
222
+
223
+
224
+ def get_user_repository(
225
+ tenant_id: Annotated[str, Depends(get_tenant_id)],
226
+ token: Annotated[str, Depends(get_token)],
227
+ ) -> UserRepository:
228
+ return UserRepository(tenant_id=tenant_id, token=token)
229
+
230
+
231
+ @app.get("/users/<user_id>")
232
+ def get_user(
233
+ user_id: Annotated[int, Path()],
234
+ repository: Annotated[UserRepository, Depends(get_user_repository)],
235
+ ):
236
+ return repository.get_user(user_id)
237
+ ```
238
+
239
+ Disable dependency caching when a dependency must run every time:
240
+
241
+ ```python
242
+ def next_counter() -> int:
243
+ ...
244
+
245
+
246
+ @app.get("/counter")
247
+ def counter(value: Annotated[int, Depends(next_counter, use_cache=False)]):
248
+ return {"value": value}
249
+ ```
250
+
251
+ For tests, set `app.dependency_overrides`:
252
+
253
+ ```python
254
+ app.dependency_overrides[get_token] = lambda: "test-token"
255
+ ```
256
+
257
+ ## Exception Handling
258
+
259
+ Built-in error responses:
260
+
261
+ - request validation errors return `400`.
262
+ - `NotFoundError` returns `404`.
263
+ - `MethodNotAllowedError` returns `405`.
264
+ - `UnauthorizedError` returns `401`.
265
+ - `ForbiddenError` returns `403`.
266
+
267
+ Custom handlers can be registered per exception type. The most specific handler
268
+ wins.
269
+
270
+ ```python
271
+ from modmex_lambda import Response
272
+
273
+
274
+ class DomainError(Exception):
275
+ pass
276
+
277
+
278
+ @app.exception_handler(DomainError)
279
+ def on_domain_error(exc: DomainError):
280
+ return Response(status_code=409, body={"message": str(exc)})
281
+ ```
282
+
283
+ If a custom exception handler raises, the resolver falls back to the default
284
+ error response when one exists.
285
+
286
+ ## CORS
287
+
288
+ Pass `CORSConfig` to the resolver to add CORS headers and automatic preflight
289
+ behavior.
290
+
291
+ ```python
292
+ from modmex_lambda import ApiGatewayHttpResolver
293
+ from modmex_lambda.event_handler.cors import CORSConfig
294
+
295
+ app = ApiGatewayHttpResolver(
296
+ cors=CORSConfig(
297
+ allow_origin="https://app.example",
298
+ allow_headers=["X-Tenant-Id"],
299
+ allow_credentials=True,
300
+ ),
301
+ )
302
+ ```
303
+
304
+ ## Parser
305
+
306
+ ```python
307
+ from modmex_lambda.parser import event_parser, parse
308
+
309
+ parsed = parse(event={"name": "Ada"}, model=MyModel)
310
+
311
+
312
+ @event_parser(model=MyModel)
313
+ def lambda_handler(event: MyModel, context):
314
+ ...
315
+ ```
316
+
317
+ ## Event Source Data Classes
318
+
319
+ Current scoped data classes include:
320
+
321
+ - `APIGatewayProxyEvent` and `APIGatewayProxyEventV2`
322
+ - `APIGatewayRestEvent` and `APIGatewayHttpEvent` aliases
323
+ - `APIGatewayAuthorizerEvent`
324
+ - `APIGatewayWebSocketEvent`
325
+ - Cognito User Pool trigger wrappers
326
+
327
+ ```python
328
+ from modmex_lambda.data_classes import APIGatewayHttpEvent
329
+ from modmex_lambda.event_sources import event_source
330
+
331
+
332
+ @event_source(data_class=APIGatewayHttpEvent)
333
+ def lambda_handler(event: APIGatewayHttpEvent, context):
334
+ return {"path": event.path}
335
+ ```
336
+
337
+ ## Validation
338
+
339
+ Modmex is the default validation and coercion engine. Pydantic is not required
340
+ for normal operation.
341
+
342
+ ## Logging
343
+
344
+ ```python
345
+ from modmex_lambda import Logger
346
+
347
+ logger = Logger(service="users")
348
+
349
+
350
+ def lambda_handler(event, context):
351
+ logger.append_keys(tenant_id="mx")
352
+ logger.info("request received")
353
+ ```
354
+
355
+ The logger emits structured JSON and can extract Lambda request IDs and API
356
+ Gateway correlation IDs.
357
+
358
+ ## Benchmarks
359
+
360
+ The benchmark suite lives in `.benchmark/api_gateway_benchmark.py`.
361
+
362
+ It covers cold imports, app setup, route registration, API Gateway v1/v2
363
+ invocation, `event_parser`, `event_source`, and logger hot paths.
364
+
365
+ ```bash
366
+ poetry run python .benchmark/api_gateway_benchmark.py
367
+ ```
368
+
369
+ More details are in `.benchmark/README.md`.
370
+
371
+ ## Limitations
372
+
373
+ - Event source scope is intentionally focused on API Gateway and Cognito.
374
+ - OpenAPI/Swagger generation is not implemented.
375
+ - Async resolver pipelines are not implemented yet.
@@ -0,0 +1,48 @@
1
+ modmex_lambda/__init__.py,sha256=3gWLZwuPGrvvjTsycbsvGd4Noh4bEj87MwP32IyNrjU,1966
2
+ modmex_lambda/event_sources.py,sha256=eLTEN1g9JF2Nk09fMcJn43dBz8BKwCOrtxmZkzr99sM,1714
3
+ modmex_lambda/exceptions.py,sha256=GL6ZZKhXr0BqfCsGwzookBrUJ1M7JIU2Ju3vWdyna_s,128
4
+ modmex_lambda/logging.py,sha256=FFINAvMu25d0Ieow-Wa8fEjr9xvtRtPgtLixsDGFPHc,3335
5
+ modmex_lambda/params.py,sha256=s7cnrW2NSCwWyQ_odn0b_uQJJn_aDR0JO6XUAFKI3PY,131
6
+ modmex_lambda/parser.py,sha256=BO2PG1YHZsRbHxqcd4rKehJDkBoipAQjAgXi-E7mysM,1544
7
+ modmex_lambda/request.py,sha256=2LgG5bKFhFL8xNGaGJTK_djX0nSQOwCmjuPN54ORdL8,129
8
+ modmex_lambda/resolver.py,sha256=6EvyxbcrTqNy-xb8l-Pz8z3gXeuYWyQErZVyY5w--8o,126
9
+ modmex_lambda/response.py,sha256=n7ZjcV13IR_ohtxEs8O6T6xVhNFs0DiLlUej8PpO0no,132
10
+ modmex_lambda/routing.py,sha256=nCIo7UoOn9W6oZXYjjw5GxMp80jZ6nbuh4WEN6f5dSg,132
11
+ modmex_lambda/validation.py,sha256=64uWzSnkxzN1t7DAqC_0TYzhupQUtAWnrLATEFAEfzs,7130
12
+ modmex_lambda/data_classes/__init__.py,sha256=puf_FWi1HaqzoPw2mhA-gUUJB_8v6x1ndp0dgoGRDp0,1711
13
+ modmex_lambda/data_classes/api_gateway_authorizer_event.py,sha256=Qk7RLWm_L67VwWbu8VRmJTUlDcAJDlFvVp-cCKm2QhA,1037
14
+ modmex_lambda/data_classes/api_gateway_proxy_event.py,sha256=fetGgKG4QFpPXu0lImtei29G1bgNF5hNZOdFdW8zP90,10957
15
+ modmex_lambda/data_classes/api_gateway_websocket_event.py,sha256=EGFqQ4SrC-ssb509UzfNLWpPm7gdtleoYz5Pvb2fIwU,1031
16
+ modmex_lambda/data_classes/cognito_user_pool_event.py,sha256=DwHa9xtqpFYnFKdUi1Y9RRgxvkVGJQXhFkRh7_4iAf4,18582
17
+ modmex_lambda/data_classes/common.py,sha256=8saOnaCxH25G-jQumUJMg6CuOhuJ6ugw4vAWH-Fn4Ls,13656
18
+ modmex_lambda/event_handler/__init__.py,sha256=4uB7HhYgShpgXJECQhvxJId8Y1Cbkz69M0rpuSxN0I8,1327
19
+ modmex_lambda/event_handler/api_gateway.py,sha256=YdgeyObhdPsBApQxnSQd-JykqJ6WKsrIqgKw5r9X8d0,11795
20
+ modmex_lambda/event_handler/constants.py,sha256=x53p_BZw2lmISpkO5v6d1nfuP-De9uUsUtwgJLaBHKw,28
21
+ modmex_lambda/event_handler/content_types.py,sha256=FcnCaAVhEOewQXAbkStMBNC63NFMxqwmEFnXcGholug,282
22
+ modmex_lambda/event_handler/cors.py,sha256=j8H81NdPxqRl5ikW8Wdf7o6pEIx7dGkAsCtSysYI1nc,3516
23
+ modmex_lambda/event_handler/exception_handler.py,sha256=zJmM_kUwq3vnzx3ETlgPs2JokQbauweuUJrCDYsPZwg,2491
24
+ modmex_lambda/event_handler/exceptions.py,sha256=wPova4geWuyuFsP43xyy0MIxOvDwVx-CfgwnF0QcZbk,2025
25
+ modmex_lambda/event_handler/gateway_response.py,sha256=9V5d8Ar7JfrsFRWk02GKzJN8maSIHdl4fLlAlgmzJSQ,3728
26
+ modmex_lambda/event_handler/middlewares.py,sha256=oOsQ0nOl7lc_N-kt8wTx7qOXNE5VtRjDEVdcsnYb-Sk,925
27
+ modmex_lambda/event_handler/params.py,sha256=nNhLKduyeQ6TI9Zli32_XtRM93e973igOOZSY0vcpy4,544
28
+ modmex_lambda/event_handler/request.py,sha256=f5TMv6muYfYOsLbZMPdd6kRMza8U2-UiyNmvuB5grH4,1705
29
+ modmex_lambda/event_handler/response.py,sha256=izW9v_0E91UsQKX6-W26wnuAy-YP2_kTPbA5OW9L3tM,1988
30
+ modmex_lambda/event_handler/routing.py,sha256=ofRSEfWxxAayThjgsBzZ6ZQaSFwtFxnpp1LDp-za2vY,16077
31
+ modmex_lambda/event_handler/routing_fallbacks.py,sha256=vx23xyg4vEKCqGg1bxBP0OxRsN8NbMkAdehfcNiqwjw,3669
32
+ modmex_lambda/event_handler/types.py,sha256=VKl50yStngGK1vBsPVDLQnO8zffgqL9T1StPIVY82qo,1060
33
+ modmex_lambda/event_handler/dependencies/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
34
+ modmex_lambda/event_handler/dependencies/compat.py,sha256=MPEJ9EKNja_ys4XLGIm8vTgN7Z96nmuqYHTvixHFR9A,7599
35
+ modmex_lambda/event_handler/dependencies/dependant.py,sha256=LWo2r6BZkOKo26tXr5g9vJ_C9Yrixf6iiFsAfM66JXw,9306
36
+ modmex_lambda/event_handler/dependencies/dependency_middleware.py,sha256=c_N7UgtbAVYC6xbfn_MaW7zYQxi5pSSdTeIRaz703Ak,14999
37
+ modmex_lambda/event_handler/dependencies/depends.py,sha256=X174dJZ_m6kYvcUvy2IJ7UuLRDV8lH2Jgiht5PQRt8w,5932
38
+ modmex_lambda/event_handler/dependencies/params.py,sha256=bc208GHiXCfm1V-GLeIMm8-ypZ8P8Q9qUKQkK2Vh1Fg,10085
39
+ modmex_lambda/event_handler/dependencies/types.py,sha256=5VA8zl-EjQDNKXOrNT7sRv_vNs40Lz8aMP-sf-lf8sk,376
40
+ modmex_lambda/shared/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
41
+ modmex_lambda/shared/cookies.py,sha256=E2hTuht5_Xljw9zOQLW6vjsOWEpQSIqD2D3Tl1Co_s0,2076
42
+ modmex_lambda/shared/headers_serializer.py,sha256=Ao-wpx0cmy9E2ACrh7R7RIDUF0h24gax1AMtY_MjIWQ,2505
43
+ modmex_lambda/shared/json_encoder.py,sha256=Mo4tlrhWvlBhhRYoPt2PAP6b_KsEKbk1pK6e5q0gbuk,1297
44
+ modmex_lambda/shared/types.py,sha256=lH3i9MF7EQa1et8dD_qagEjlNzqznNHc0YZawp3toMc,134
45
+ modmex_lambda-0.1.0.dist-info/METADATA,sha256=HOUUd4hbMrZTGBW045M_ltsC9-myVmZiUV9Jt51JHRo,8936
46
+ modmex_lambda-0.1.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
47
+ modmex_lambda-0.1.0.dist-info/licenses/LICENSE,sha256=_C2TDTOsYeJvE4vn9VB51laKvleBKbdNnn96wJVtXhQ,1063
48
+ modmex_lambda-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.30.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 modmex
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.