wellapi 0.2.1__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.
- wellapi/__init__.py +5 -0
- wellapi/__main__.py +3 -0
- wellapi/applications.py +389 -0
- wellapi/awsmodel.py +17 -0
- wellapi/build/__init__.py +0 -0
- wellapi/build/cdk.py +141 -0
- wellapi/build/packager.py +82 -0
- wellapi/build/sam_openapi.py +10 -0
- wellapi/cli/__init__.py +0 -0
- wellapi/cli/main.py +67 -0
- wellapi/convertors.py +89 -0
- wellapi/datastructures.py +383 -0
- wellapi/dependencies/__init__.py +0 -0
- wellapi/dependencies/models.py +138 -0
- wellapi/dependencies/utils.py +923 -0
- wellapi/exceptions.py +53 -0
- wellapi/local/__init__.py +0 -0
- wellapi/local/reloader.py +94 -0
- wellapi/local/router.py +116 -0
- wellapi/local/server.py +154 -0
- wellapi/middleware/__init__.py +0 -0
- wellapi/middleware/base.py +18 -0
- wellapi/middleware/error.py +239 -0
- wellapi/middleware/exceptions.py +74 -0
- wellapi/middleware/main.py +26 -0
- wellapi/models.py +150 -0
- wellapi/openapi/__init__.py +0 -0
- wellapi/openapi/docs.py +344 -0
- wellapi/openapi/models.py +404 -0
- wellapi/openapi/utils.py +535 -0
- wellapi/params.py +481 -0
- wellapi/routing.py +248 -0
- wellapi/security.py +82 -0
- wellapi/utils.py +37 -0
- wellapi-0.2.1.dist-info/METADATA +32 -0
- wellapi-0.2.1.dist-info/RECORD +38 -0
- wellapi-0.2.1.dist-info/WHEEL +4 -0
- wellapi-0.2.1.dist-info/entry_points.txt +2 -0
wellapi/__init__.py
ADDED
wellapi/__main__.py
ADDED
wellapi/applications.py
ADDED
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
from collections.abc import Callable, Sequence
|
|
3
|
+
from enum import Enum
|
|
4
|
+
from typing import Any, Literal
|
|
5
|
+
|
|
6
|
+
from pydantic._internal._utils import lenient_issubclass
|
|
7
|
+
from pydantic.main import IncEx
|
|
8
|
+
|
|
9
|
+
from wellapi import params
|
|
10
|
+
from wellapi.datastructures import Default, DefaultPlaceholder
|
|
11
|
+
from wellapi.dependencies.models import ModelField
|
|
12
|
+
from wellapi.dependencies.utils import (
|
|
13
|
+
_should_embed_body_fields,
|
|
14
|
+
create_model_field,
|
|
15
|
+
get_body_field,
|
|
16
|
+
get_dependant,
|
|
17
|
+
get_flat_dependant,
|
|
18
|
+
get_parameterless_sub_dependant,
|
|
19
|
+
get_typed_return_annotation,
|
|
20
|
+
)
|
|
21
|
+
from wellapi.middleware.base import BaseMiddleware
|
|
22
|
+
from wellapi.middleware.error import ServerErrorMiddleware
|
|
23
|
+
from wellapi.middleware.exceptions import ExceptionMiddleware
|
|
24
|
+
from wellapi.middleware.main import Middleware
|
|
25
|
+
from wellapi.models import RequestAPIGateway, ResponseAPIGateway
|
|
26
|
+
from wellapi.routing import (
|
|
27
|
+
compile_path,
|
|
28
|
+
get_request_handler,
|
|
29
|
+
is_body_allowed_for_status_code,
|
|
30
|
+
request_response,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def to_camel_case(snake_str):
|
|
35
|
+
return "".join(x.capitalize() for x in snake_str.lower().split("_"))
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def get_arn(endpoint: Callable[..., Any]) -> str:
|
|
39
|
+
name = f"{endpoint.__module__}.{endpoint.__name__}"
|
|
40
|
+
name = name.replace(".", "_")
|
|
41
|
+
return to_camel_case(name)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class Lambda:
|
|
45
|
+
def __init__(
|
|
46
|
+
self,
|
|
47
|
+
path: str,
|
|
48
|
+
endpoint: Callable[..., Any],
|
|
49
|
+
*,
|
|
50
|
+
method: str = None,
|
|
51
|
+
type_: Literal["endpoint", "queue", "job"] = "endpoint",
|
|
52
|
+
name: str | None = None,
|
|
53
|
+
memory_size: int = 128,
|
|
54
|
+
timeout: int = 30,
|
|
55
|
+
response_model: Any = Default(None),
|
|
56
|
+
status_code: int | None = None,
|
|
57
|
+
description: str | None = None,
|
|
58
|
+
response_description: str = "Successful Response",
|
|
59
|
+
responses: dict[int | str, dict[str, Any]] | None = None,
|
|
60
|
+
response_class: type[ResponseAPIGateway] | DefaultPlaceholder = Default(
|
|
61
|
+
ResponseAPIGateway
|
|
62
|
+
),
|
|
63
|
+
response_model_include: IncEx | None = None,
|
|
64
|
+
response_model_exclude: IncEx | None = None,
|
|
65
|
+
response_model_by_alias: bool = True,
|
|
66
|
+
response_model_exclude_unset: bool = False,
|
|
67
|
+
response_model_exclude_defaults: bool = False,
|
|
68
|
+
response_model_exclude_none: bool = False,
|
|
69
|
+
dependencies: Sequence[params.Depends] | None = None,
|
|
70
|
+
tags: list[str | Enum] | None = None,
|
|
71
|
+
deprecated: bool | None = None,
|
|
72
|
+
operation_id: str | None = None,
|
|
73
|
+
summary: str | None = None,
|
|
74
|
+
):
|
|
75
|
+
self.path = path
|
|
76
|
+
self.type_ = type_
|
|
77
|
+
self.name = endpoint.__name__ if name is None else name
|
|
78
|
+
self.memory_size = memory_size
|
|
79
|
+
self.timeout = timeout
|
|
80
|
+
self.method = method
|
|
81
|
+
self.endpoint = endpoint
|
|
82
|
+
self.path_regex, self.path_format, self.param_convertors = compile_path(path)
|
|
83
|
+
self.dependencies = list(dependencies or [])
|
|
84
|
+
self.operation_id = operation_id
|
|
85
|
+
self.unique_id = (
|
|
86
|
+
self.operation_id or f"{self.endpoint.__module__}.{self.endpoint.__name__}"
|
|
87
|
+
)
|
|
88
|
+
self.status_code = status_code
|
|
89
|
+
self.response_description = response_description
|
|
90
|
+
self.response_class = response_class
|
|
91
|
+
self.response_model_include = response_model_include
|
|
92
|
+
self.response_model_exclude = response_model_exclude
|
|
93
|
+
self.response_model_by_alias = response_model_by_alias
|
|
94
|
+
self.response_model_exclude_unset = response_model_exclude_unset
|
|
95
|
+
self.response_model_exclude_defaults = response_model_exclude_defaults
|
|
96
|
+
self.response_model_exclude_none = response_model_exclude_none
|
|
97
|
+
self.tags = tags or []
|
|
98
|
+
self.deprecated = deprecated
|
|
99
|
+
self.summary = summary
|
|
100
|
+
self.arn = get_arn(self.endpoint)
|
|
101
|
+
|
|
102
|
+
if isinstance(response_model, DefaultPlaceholder):
|
|
103
|
+
return_annotation = get_typed_return_annotation(endpoint)
|
|
104
|
+
if lenient_issubclass(return_annotation, ResponseAPIGateway):
|
|
105
|
+
response_model = None
|
|
106
|
+
else:
|
|
107
|
+
response_model = return_annotation
|
|
108
|
+
self.response_model = response_model
|
|
109
|
+
if self.response_model:
|
|
110
|
+
assert is_body_allowed_for_status_code(status_code), (
|
|
111
|
+
f"Status code {status_code} must not have a response body"
|
|
112
|
+
)
|
|
113
|
+
response_name = "Response_" + self.unique_id
|
|
114
|
+
self.response_field = create_model_field(
|
|
115
|
+
name=response_name,
|
|
116
|
+
type_=self.response_model,
|
|
117
|
+
mode="serialization",
|
|
118
|
+
)
|
|
119
|
+
else:
|
|
120
|
+
self.response_field = None # type: ignore
|
|
121
|
+
self.secure_cloned_response_field = None
|
|
122
|
+
|
|
123
|
+
self.description = description or inspect.cleandoc(self.endpoint.__doc__ or "")
|
|
124
|
+
# if a "form feed" character (page break) is found in the description text,
|
|
125
|
+
# truncate description text to the content preceding the first "form feed"
|
|
126
|
+
self.description = self.description.split("\f")[0].strip()
|
|
127
|
+
response_fields = {}
|
|
128
|
+
self.responses = responses or {}
|
|
129
|
+
for additional_status_code, response in self.responses.items():
|
|
130
|
+
assert isinstance(response, dict), "An additional response must be a dict"
|
|
131
|
+
model = response.get("model")
|
|
132
|
+
if model:
|
|
133
|
+
assert is_body_allowed_for_status_code(additional_status_code), (
|
|
134
|
+
f"Status code {additional_status_code} must not have a response body"
|
|
135
|
+
)
|
|
136
|
+
response_name = f"Response_{additional_status_code}_{self.unique_id}"
|
|
137
|
+
response_field = create_model_field(
|
|
138
|
+
name=response_name, type_=model, mode="serialization"
|
|
139
|
+
)
|
|
140
|
+
response_fields[additional_status_code] = response_field
|
|
141
|
+
if response_fields:
|
|
142
|
+
self.response_fields: dict[int | str, ModelField] = response_fields
|
|
143
|
+
else:
|
|
144
|
+
self.response_fields = {}
|
|
145
|
+
|
|
146
|
+
assert callable(endpoint), "An endpoint must be a callable"
|
|
147
|
+
self.dependant = get_dependant(
|
|
148
|
+
path=self.path_format, call=self.endpoint, type_=self.type_
|
|
149
|
+
)
|
|
150
|
+
for depends in self.dependencies[::-1]:
|
|
151
|
+
self.dependant.dependencies.insert(
|
|
152
|
+
0,
|
|
153
|
+
get_parameterless_sub_dependant(
|
|
154
|
+
depends=depends, path=self.path_format, type_=self.type_
|
|
155
|
+
),
|
|
156
|
+
)
|
|
157
|
+
self._flat_dependant = get_flat_dependant(self.dependant)
|
|
158
|
+
self._embed_body_fields = _should_embed_body_fields(
|
|
159
|
+
self._flat_dependant.body_params
|
|
160
|
+
)
|
|
161
|
+
self.body_field = get_body_field(
|
|
162
|
+
flat_dependant=self._flat_dependant,
|
|
163
|
+
name=self.unique_id,
|
|
164
|
+
embed_body_fields=self._embed_body_fields,
|
|
165
|
+
)
|
|
166
|
+
self.app = self.get_route_handler()
|
|
167
|
+
|
|
168
|
+
def get_route_handler(self):
|
|
169
|
+
return get_request_handler(
|
|
170
|
+
dependant=self.dependant,
|
|
171
|
+
status_code=self.status_code,
|
|
172
|
+
response_class=self.response_class,
|
|
173
|
+
response_field=self.response_field,
|
|
174
|
+
response_model_include=self.response_model_include,
|
|
175
|
+
response_model_exclude=self.response_model_exclude,
|
|
176
|
+
response_model_by_alias=self.response_model_by_alias,
|
|
177
|
+
response_model_exclude_unset=self.response_model_exclude_unset,
|
|
178
|
+
response_model_exclude_defaults=self.response_model_exclude_defaults,
|
|
179
|
+
response_model_exclude_none=self.response_model_exclude_none,
|
|
180
|
+
embed_body_fields=self._embed_body_fields,
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
class WellApi:
|
|
185
|
+
def __init__(
|
|
186
|
+
self,
|
|
187
|
+
title: str = "WellApi",
|
|
188
|
+
version: str = "0.1.0",
|
|
189
|
+
description: str = "",
|
|
190
|
+
openapi_tags: list[dict[str, Any]] | None = None,
|
|
191
|
+
servers: list[dict[str, str | Any]] | None = None,
|
|
192
|
+
queues: list[dict[str, Any]] | None = None,
|
|
193
|
+
debug: bool = False,
|
|
194
|
+
):
|
|
195
|
+
self.lambdas = []
|
|
196
|
+
self.exception_handlers = {}
|
|
197
|
+
self.user_middleware = []
|
|
198
|
+
self.debug = debug
|
|
199
|
+
self.title = title
|
|
200
|
+
self.version = version
|
|
201
|
+
self.description = description
|
|
202
|
+
self.openapi_tags = openapi_tags
|
|
203
|
+
self.servers = servers
|
|
204
|
+
self.queues = queues or []
|
|
205
|
+
|
|
206
|
+
def build_middleware_stack(self, app: Callable) -> Callable:
|
|
207
|
+
debug = self.debug
|
|
208
|
+
error_handler = None
|
|
209
|
+
exception_handlers: dict[
|
|
210
|
+
Any, Callable[[RequestAPIGateway, Exception], ResponseAPIGateway]
|
|
211
|
+
] = {}
|
|
212
|
+
|
|
213
|
+
for key, value in self.exception_handlers.items():
|
|
214
|
+
if key in (500, Exception):
|
|
215
|
+
error_handler = value
|
|
216
|
+
else:
|
|
217
|
+
exception_handlers[key] = value
|
|
218
|
+
|
|
219
|
+
middleware = (
|
|
220
|
+
[Middleware(ServerErrorMiddleware, handler=error_handler, debug=debug)]
|
|
221
|
+
+ self.user_middleware
|
|
222
|
+
+ [
|
|
223
|
+
Middleware(
|
|
224
|
+
ExceptionMiddleware, handlers=exception_handlers, debug=debug
|
|
225
|
+
)
|
|
226
|
+
]
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
for cls, args, kwargs in reversed(middleware):
|
|
230
|
+
app = cls(app, *args, **kwargs)
|
|
231
|
+
return request_response(app)
|
|
232
|
+
|
|
233
|
+
def add_endpoint(self, *args, **kwargs):
|
|
234
|
+
lambda_ = Lambda(*args, **kwargs)
|
|
235
|
+
self.lambdas.append(lambda_)
|
|
236
|
+
|
|
237
|
+
def endpoint(event, context):
|
|
238
|
+
return self.build_middleware_stack(lambda_.app)(event, context)
|
|
239
|
+
|
|
240
|
+
return endpoint
|
|
241
|
+
|
|
242
|
+
def add_exception_handler(
|
|
243
|
+
self,
|
|
244
|
+
exc_class_or_status_code: int | type[Exception],
|
|
245
|
+
handler: Callable[[RequestAPIGateway, Exception], ResponseAPIGateway],
|
|
246
|
+
) -> None: # pragma: no cover
|
|
247
|
+
self.exception_handlers[exc_class_or_status_code] = handler
|
|
248
|
+
|
|
249
|
+
def exception_handler(
|
|
250
|
+
self, exc_class_or_status_code: int | type[Exception]
|
|
251
|
+
) -> Callable:
|
|
252
|
+
def decorator(func):
|
|
253
|
+
self.add_exception_handler(exc_class_or_status_code, func)
|
|
254
|
+
return func
|
|
255
|
+
|
|
256
|
+
return decorator
|
|
257
|
+
|
|
258
|
+
def add_middleware(self, middleware_class, dispatch) -> None:
|
|
259
|
+
self.user_middleware.insert(0, Middleware(middleware_class, dispatch))
|
|
260
|
+
|
|
261
|
+
def middleware(self) -> Callable:
|
|
262
|
+
def decorator(func):
|
|
263
|
+
self.add_middleware(BaseMiddleware, dispatch=func)
|
|
264
|
+
return func
|
|
265
|
+
|
|
266
|
+
return decorator
|
|
267
|
+
|
|
268
|
+
def get(
|
|
269
|
+
self,
|
|
270
|
+
path: str,
|
|
271
|
+
*,
|
|
272
|
+
response_model: Any = Default(None),
|
|
273
|
+
status_code: int | None = None,
|
|
274
|
+
response_class: type[ResponseAPIGateway] | DefaultPlaceholder = Default(
|
|
275
|
+
ResponseAPIGateway
|
|
276
|
+
),
|
|
277
|
+
response_model_include: IncEx | None = None,
|
|
278
|
+
response_model_exclude: IncEx | None = None,
|
|
279
|
+
response_model_by_alias: bool = True,
|
|
280
|
+
response_model_exclude_unset: bool = False,
|
|
281
|
+
response_model_exclude_defaults: bool = False,
|
|
282
|
+
response_model_exclude_none: bool = False,
|
|
283
|
+
dependencies: Sequence[params.Depends] | None = None,
|
|
284
|
+
tags: list[str | Enum] | None = None,
|
|
285
|
+
):
|
|
286
|
+
def decorator(func):
|
|
287
|
+
lambda_ = self.add_endpoint(
|
|
288
|
+
path,
|
|
289
|
+
func,
|
|
290
|
+
type_="endpoint",
|
|
291
|
+
response_model=response_model,
|
|
292
|
+
status_code=status_code,
|
|
293
|
+
method="GET",
|
|
294
|
+
response_class=response_class,
|
|
295
|
+
response_model_include=response_model_include,
|
|
296
|
+
response_model_exclude=response_model_exclude,
|
|
297
|
+
response_model_by_alias=response_model_by_alias,
|
|
298
|
+
response_model_exclude_unset=response_model_exclude_unset,
|
|
299
|
+
response_model_exclude_defaults=response_model_exclude_defaults,
|
|
300
|
+
response_model_exclude_none=response_model_exclude_none,
|
|
301
|
+
dependencies=dependencies,
|
|
302
|
+
tags=tags,
|
|
303
|
+
)
|
|
304
|
+
return lambda_
|
|
305
|
+
|
|
306
|
+
return decorator
|
|
307
|
+
|
|
308
|
+
def post(
|
|
309
|
+
self,
|
|
310
|
+
path: str,
|
|
311
|
+
*,
|
|
312
|
+
response_model: Any = Default(None),
|
|
313
|
+
status_code: int | None = None,
|
|
314
|
+
response_class: type[ResponseAPIGateway] | DefaultPlaceholder = Default(
|
|
315
|
+
ResponseAPIGateway
|
|
316
|
+
),
|
|
317
|
+
response_model_include: IncEx | None = None,
|
|
318
|
+
response_model_exclude: IncEx | None = None,
|
|
319
|
+
response_model_by_alias: bool = True,
|
|
320
|
+
response_model_exclude_unset: bool = False,
|
|
321
|
+
response_model_exclude_defaults: bool = False,
|
|
322
|
+
response_model_exclude_none: bool = False,
|
|
323
|
+
dependencies: Sequence[params.Depends] | None = None,
|
|
324
|
+
tags: list[str | Enum] | None = None,
|
|
325
|
+
):
|
|
326
|
+
def decorator(func):
|
|
327
|
+
lambda_ = self.add_endpoint(
|
|
328
|
+
path,
|
|
329
|
+
func,
|
|
330
|
+
type_="endpoint",
|
|
331
|
+
response_model=response_model,
|
|
332
|
+
status_code=status_code,
|
|
333
|
+
method="POST",
|
|
334
|
+
response_class=response_class,
|
|
335
|
+
response_model_include=response_model_include,
|
|
336
|
+
response_model_exclude=response_model_exclude,
|
|
337
|
+
response_model_by_alias=response_model_by_alias,
|
|
338
|
+
response_model_exclude_unset=response_model_exclude_unset,
|
|
339
|
+
response_model_exclude_defaults=response_model_exclude_defaults,
|
|
340
|
+
response_model_exclude_none=response_model_exclude_none,
|
|
341
|
+
dependencies=dependencies,
|
|
342
|
+
tags=tags,
|
|
343
|
+
)
|
|
344
|
+
return lambda_
|
|
345
|
+
|
|
346
|
+
return decorator
|
|
347
|
+
|
|
348
|
+
def sqs(
|
|
349
|
+
self,
|
|
350
|
+
queue_name: str,
|
|
351
|
+
memory_size: int = 128,
|
|
352
|
+
timeout: int = 30,
|
|
353
|
+
dependencies: Sequence[params.Depends] | None = None,
|
|
354
|
+
):
|
|
355
|
+
def decorator(func):
|
|
356
|
+
lambda_ = self.add_endpoint(
|
|
357
|
+
queue_name,
|
|
358
|
+
func,
|
|
359
|
+
type_="queue",
|
|
360
|
+
memory_size=memory_size,
|
|
361
|
+
timeout=timeout,
|
|
362
|
+
dependencies=dependencies,
|
|
363
|
+
)
|
|
364
|
+
return lambda_
|
|
365
|
+
|
|
366
|
+
return decorator
|
|
367
|
+
|
|
368
|
+
def job(
|
|
369
|
+
self,
|
|
370
|
+
expression: str,
|
|
371
|
+
name: str | None = None,
|
|
372
|
+
memory_size: int = 128,
|
|
373
|
+
timeout: int = 30,
|
|
374
|
+
*,
|
|
375
|
+
dependencies: Sequence[params.Depends] | None = None,
|
|
376
|
+
):
|
|
377
|
+
def decorator(func):
|
|
378
|
+
lambda_ = self.add_endpoint(
|
|
379
|
+
expression,
|
|
380
|
+
func,
|
|
381
|
+
type_="job",
|
|
382
|
+
name=name,
|
|
383
|
+
memory_size=memory_size,
|
|
384
|
+
timeout=timeout,
|
|
385
|
+
dependencies=dependencies,
|
|
386
|
+
)
|
|
387
|
+
return lambda_
|
|
388
|
+
|
|
389
|
+
return decorator
|
wellapi/awsmodel.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from pydantic import BaseModel
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Message(BaseModel):
|
|
5
|
+
messageId: str
|
|
6
|
+
receiptHandle: str
|
|
7
|
+
body: str
|
|
8
|
+
attributes: dict
|
|
9
|
+
messageAttributes: dict
|
|
10
|
+
md5OfBody: str
|
|
11
|
+
eventSource: str
|
|
12
|
+
eventSourceARN: str
|
|
13
|
+
awsRegion: str
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class SQSEvent(BaseModel):
|
|
17
|
+
Records: list[Message]
|
|
File without changes
|
wellapi/build/cdk.py
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
|
|
4
|
+
# ruff: noqa: I001
|
|
5
|
+
from aws_cdk import (
|
|
6
|
+
Fn,
|
|
7
|
+
Duration,
|
|
8
|
+
aws_apigateway as apigw,
|
|
9
|
+
aws_events as events,
|
|
10
|
+
aws_events_targets as targets,
|
|
11
|
+
aws_iam as iam,
|
|
12
|
+
aws_lambda as _lambda,
|
|
13
|
+
aws_lambda_event_sources as lambda_event_source,
|
|
14
|
+
aws_sqs as sqs,
|
|
15
|
+
aws_s3_assets as s3_assets,
|
|
16
|
+
)
|
|
17
|
+
from aws_cdk.aws_lambda import CfnFunction
|
|
18
|
+
from constructs import Construct
|
|
19
|
+
|
|
20
|
+
from wellapi.applications import Lambda, WellApi
|
|
21
|
+
from wellapi.build.packager import package
|
|
22
|
+
from wellapi.openapi.utils import get_openapi
|
|
23
|
+
from wellapi.utils import import_app, load_handlers
|
|
24
|
+
|
|
25
|
+
OPENAPI_FILE = "openapi-spec.json"
|
|
26
|
+
APP_LAYOUT_FILE = "app_content.zip"
|
|
27
|
+
DEP_LAYOUT_FILE = "layer_content.zip"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class WellApiCDK(Construct):
|
|
31
|
+
"""
|
|
32
|
+
This class is used to create a Well API using AWS CDK.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
def __init__(
|
|
36
|
+
self,
|
|
37
|
+
scope: Construct,
|
|
38
|
+
id_: str,
|
|
39
|
+
*,
|
|
40
|
+
app_srt: str,
|
|
41
|
+
handlers_dir: str,
|
|
42
|
+
) -> None:
|
|
43
|
+
super().__init__(scope, id_)
|
|
44
|
+
|
|
45
|
+
self.app_srt = app_srt
|
|
46
|
+
self.handlers_dir = os.path.abspath(handlers_dir)
|
|
47
|
+
|
|
48
|
+
wellapi_app: WellApi = self._package_app()
|
|
49
|
+
|
|
50
|
+
# defining a Cfn Asset from the openAPI file
|
|
51
|
+
open_api_asset = s3_assets.Asset(self, "OpenApiAsset", path=OPENAPI_FILE)
|
|
52
|
+
transform_map = {"Location": open_api_asset.s3_object_url}
|
|
53
|
+
data = Fn.transform("AWS::Include", transform_map)
|
|
54
|
+
|
|
55
|
+
self.api = apigw.SpecRestApi(
|
|
56
|
+
self,
|
|
57
|
+
f"{wellapi_app.title}Api",
|
|
58
|
+
api_definition=apigw.ApiDefinition.from_inline(data),
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
for q in wellapi_app.queues:
|
|
62
|
+
queue = sqs.Queue(self, f"{q.queue_name}Queue", queue_name=q.queue_name)
|
|
63
|
+
|
|
64
|
+
shared_layer = [
|
|
65
|
+
_lambda.LayerVersion(
|
|
66
|
+
self,
|
|
67
|
+
"SharedLayer",
|
|
68
|
+
code=_lambda.Code.from_asset(DEP_LAYOUT_FILE),
|
|
69
|
+
compatible_runtimes=[_lambda.Runtime.PYTHON_3_12], # type: ignore
|
|
70
|
+
layer_version_name="shared_layer",
|
|
71
|
+
)
|
|
72
|
+
]
|
|
73
|
+
code_layer = _lambda.Code.from_asset(APP_LAYOUT_FILE)
|
|
74
|
+
|
|
75
|
+
lmbd: Lambda
|
|
76
|
+
for lmbd in wellapi_app.lambdas:
|
|
77
|
+
lambda_function = _lambda.Function(
|
|
78
|
+
self,
|
|
79
|
+
f"{lmbd.arn}Function",
|
|
80
|
+
function_name=f"{lmbd.arn}Function",
|
|
81
|
+
runtime=_lambda.Runtime.PYTHON_3_12, # type: ignore
|
|
82
|
+
handler=lmbd.unique_id,
|
|
83
|
+
memory_size=lmbd.memory_size,
|
|
84
|
+
timeout=Duration.seconds(lmbd.timeout),
|
|
85
|
+
code=code_layer,
|
|
86
|
+
layers=shared_layer, # type: ignore
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
if lmbd.type_ == "endpoint":
|
|
90
|
+
lambda_function.add_permission(
|
|
91
|
+
f"{lmbd.arn}Permission",
|
|
92
|
+
principal=iam.ServicePrincipal("apigateway.amazonaws.com"),
|
|
93
|
+
action="lambda:InvokeFunction",
|
|
94
|
+
source_arn=self.api.arn_for_execute_api(lmbd.method.upper()),
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
cfn_lambda: CfnFunction = lambda_function.node.default_child # type: ignore
|
|
98
|
+
cfn_lambda.override_logical_id(f"{lmbd.arn}Function")
|
|
99
|
+
# self.api.node.add_dependency(lambda_function)
|
|
100
|
+
|
|
101
|
+
if lmbd.type_ == "queue":
|
|
102
|
+
queue = sqs.Queue(
|
|
103
|
+
self,
|
|
104
|
+
f"{lmbd.name}Queue",
|
|
105
|
+
queue_name=lmbd.path,
|
|
106
|
+
visibility_timeout=Duration.seconds(lmbd.timeout),
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
sqs_event_source = lambda_event_source.SqsEventSource(queue) # type: ignore
|
|
110
|
+
|
|
111
|
+
# Add SQS event source to the Lambda function
|
|
112
|
+
lambda_function.add_event_source(sqs_event_source)
|
|
113
|
+
|
|
114
|
+
if lmbd.type_ == "job":
|
|
115
|
+
rule = events.Rule(
|
|
116
|
+
self,
|
|
117
|
+
f"{lmbd.name}Rule",
|
|
118
|
+
schedule=events.Schedule.expression(lmbd.path),
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
rule.add_target(targets.LambdaFunction(lambda_function)) # type: ignore
|
|
122
|
+
|
|
123
|
+
def _package_app(self) -> WellApi:
|
|
124
|
+
wellapi_app = import_app(self.app_srt)
|
|
125
|
+
load_handlers(self.handlers_dir)
|
|
126
|
+
|
|
127
|
+
resp = get_openapi(
|
|
128
|
+
title=wellapi_app.title,
|
|
129
|
+
version=wellapi_app.version,
|
|
130
|
+
openapi_version="3.0.1",
|
|
131
|
+
description=wellapi_app.description,
|
|
132
|
+
lambdas=wellapi_app.lambdas,
|
|
133
|
+
tags=wellapi_app.openapi_tags,
|
|
134
|
+
servers=wellapi_app.servers,
|
|
135
|
+
)
|
|
136
|
+
with open(OPENAPI_FILE, "w") as f:
|
|
137
|
+
json.dump(resp, f)
|
|
138
|
+
|
|
139
|
+
package(DEP_LAYOUT_FILE, APP_LAYOUT_FILE)
|
|
140
|
+
|
|
141
|
+
return wellapi_app
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import shutil
|
|
2
|
+
import subprocess
|
|
3
|
+
import zipfile
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
"""
|
|
7
|
+
https://docs.astral.sh/uv/guides/integration/aws-lambda/#deploying-a-zip-archive
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
EXPERT_DEP_FILE = [
|
|
11
|
+
"uv",
|
|
12
|
+
"export",
|
|
13
|
+
"--frozen",
|
|
14
|
+
"--no-dev",
|
|
15
|
+
"--no-editable",
|
|
16
|
+
"-orequirements.txt",
|
|
17
|
+
]
|
|
18
|
+
INSTALL_DEP = [
|
|
19
|
+
"uv",
|
|
20
|
+
"pip",
|
|
21
|
+
"install",
|
|
22
|
+
"--no-installer-metadata",
|
|
23
|
+
"--no-compile-bytecode",
|
|
24
|
+
"--python-platform",
|
|
25
|
+
"x86_64-manylinux2014",
|
|
26
|
+
"--python",
|
|
27
|
+
"3.12",
|
|
28
|
+
"--prefix",
|
|
29
|
+
"packages",
|
|
30
|
+
"-r",
|
|
31
|
+
"requirements.txt",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def install_dependencies():
|
|
36
|
+
# Експорт файлу requirements.txt
|
|
37
|
+
subprocess.run(EXPERT_DEP_FILE, check=True)
|
|
38
|
+
|
|
39
|
+
# Створення директорії для інсталяції
|
|
40
|
+
packages_path = Path("packages")
|
|
41
|
+
packages_path.mkdir(parents=True, exist_ok=True)
|
|
42
|
+
|
|
43
|
+
subprocess.run(INSTALL_DEP, check=True)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def copy_to_python_folder():
|
|
47
|
+
src = Path("packages/lib")
|
|
48
|
+
dst = Path("python/lib")
|
|
49
|
+
dst.parent.mkdir(exist_ok=True)
|
|
50
|
+
shutil.copytree(src, dst, dirs_exist_ok=True)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def create_zip(file_name: str, dir_name: str):
|
|
54
|
+
zip_path = Path(file_name)
|
|
55
|
+
with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as z:
|
|
56
|
+
for path in Path(dir_name).rglob("*"):
|
|
57
|
+
# TODO: remove this check
|
|
58
|
+
if str(path).startswith(".venv/"):
|
|
59
|
+
continue
|
|
60
|
+
z.write(path, path.relative_to(Path(dir_name).parent))
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def package_dependencies(layer_name):
|
|
64
|
+
install_dependencies()
|
|
65
|
+
copy_to_python_folder()
|
|
66
|
+
create_zip(layer_name, "python")
|
|
67
|
+
# clean up
|
|
68
|
+
shutil.rmtree("packages")
|
|
69
|
+
shutil.rmtree("python")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def package_app(app_name):
|
|
73
|
+
create_zip(app_name, ".")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def package(layer_name: str, app_name: str):
|
|
77
|
+
"""
|
|
78
|
+
Package the application into a zip file.
|
|
79
|
+
:return:
|
|
80
|
+
"""
|
|
81
|
+
package_app(app_name)
|
|
82
|
+
package_dependencies(layer_name)
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""
|
|
2
|
+
https://medium.com/@usamadar4777/aws-sam-template-open-api-3-0-1-integration-26e67375c4a0
|
|
3
|
+
|
|
4
|
+
https://aws.amazon.com/blogs/opensource/create-restful-apis-on-aws-with-openapi-specification-with-no-coding/
|
|
5
|
+
|
|
6
|
+
https://github.com/aws/aws-sam-cli/blob/develop/tests/integration/testdata/start_api/swagger-template.yaml
|
|
7
|
+
|
|
8
|
+
samcli.commands.build.command.do_cli
|
|
9
|
+
|
|
10
|
+
"""
|
wellapi/cli/__init__.py
ADDED
|
File without changes
|