async-lambda-unstable 0.3.12__tar.gz → 0.4.3__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 (33) hide show
  1. {async-lambda-unstable-0.3.12 → async-lambda-unstable-0.4.3}/PKG-INFO +76 -1
  2. {async-lambda-unstable-0.3.12 → async-lambda-unstable-0.4.3}/README.md +75 -0
  3. {async-lambda-unstable-0.3.12 → async-lambda-unstable-0.4.3}/async_lambda/__init__.py +2 -1
  4. {async-lambda-unstable-0.3.12 → async-lambda-unstable-0.4.3}/async_lambda/controller.py +29 -2
  5. async-lambda-unstable-0.4.3/async_lambda/middleware.py +30 -0
  6. {async-lambda-unstable-0.3.12 → async-lambda-unstable-0.4.3}/async_lambda/models/task.py +10 -5
  7. {async-lambda-unstable-0.3.12 → async-lambda-unstable-0.4.3}/async_lambda_unstable.egg-info/PKG-INFO +76 -1
  8. {async-lambda-unstable-0.3.12 → async-lambda-unstable-0.4.3}/async_lambda_unstable.egg-info/SOURCES.txt +1 -0
  9. {async-lambda-unstable-0.3.12 → async-lambda-unstable-0.4.3}/async_lambda/build_config.py +0 -0
  10. {async-lambda-unstable-0.3.12 → async-lambda-unstable-0.4.3}/async_lambda/cli.py +0 -0
  11. {async-lambda-unstable-0.3.12 → async-lambda-unstable-0.4.3}/async_lambda/client.py +0 -0
  12. {async-lambda-unstable-0.3.12 → async-lambda-unstable-0.4.3}/async_lambda/config.py +0 -0
  13. {async-lambda-unstable-0.3.12 → async-lambda-unstable-0.4.3}/async_lambda/defer.py +0 -0
  14. {async-lambda-unstable-0.3.12 → async-lambda-unstable-0.4.3}/async_lambda/env.py +0 -0
  15. {async-lambda-unstable-0.3.12 → async-lambda-unstable-0.4.3}/async_lambda/models/__init__.py +0 -0
  16. {async-lambda-unstable-0.3.12 → async-lambda-unstable-0.4.3}/async_lambda/models/case_insensitive_dict.py +0 -0
  17. {async-lambda-unstable-0.3.12 → async-lambda-unstable-0.4.3}/async_lambda/models/events/__init__.py +0 -0
  18. {async-lambda-unstable-0.3.12 → async-lambda-unstable-0.4.3}/async_lambda/models/events/api_event.py +0 -0
  19. {async-lambda-unstable-0.3.12 → async-lambda-unstable-0.4.3}/async_lambda/models/events/base_event.py +0 -0
  20. {async-lambda-unstable-0.3.12 → async-lambda-unstable-0.4.3}/async_lambda/models/events/dynamodb_event.py +0 -0
  21. {async-lambda-unstable-0.3.12 → async-lambda-unstable-0.4.3}/async_lambda/models/events/managed_sqs_event.py +0 -0
  22. {async-lambda-unstable-0.3.12 → async-lambda-unstable-0.4.3}/async_lambda/models/events/scheduled_event.py +0 -0
  23. {async-lambda-unstable-0.3.12 → async-lambda-unstable-0.4.3}/async_lambda/models/events/unmanaged_sqs_event.py +0 -0
  24. {async-lambda-unstable-0.3.12 → async-lambda-unstable-0.4.3}/async_lambda/models/mock/mock_context.py +0 -0
  25. {async-lambda-unstable-0.3.12 → async-lambda-unstable-0.4.3}/async_lambda/models/mock/mock_event.py +0 -0
  26. {async-lambda-unstable-0.3.12 → async-lambda-unstable-0.4.3}/async_lambda/py.typed +0 -0
  27. {async-lambda-unstable-0.3.12 → async-lambda-unstable-0.4.3}/async_lambda/util.py +0 -0
  28. {async-lambda-unstable-0.3.12 → async-lambda-unstable-0.4.3}/async_lambda_unstable.egg-info/dependency_links.txt +0 -0
  29. {async-lambda-unstable-0.3.12 → async-lambda-unstable-0.4.3}/async_lambda_unstable.egg-info/entry_points.txt +0 -0
  30. {async-lambda-unstable-0.3.12 → async-lambda-unstable-0.4.3}/async_lambda_unstable.egg-info/requires.txt +0 -0
  31. {async-lambda-unstable-0.3.12 → async-lambda-unstable-0.4.3}/async_lambda_unstable.egg-info/top_level.txt +0 -0
  32. {async-lambda-unstable-0.3.12 → async-lambda-unstable-0.4.3}/pyproject.toml +0 -0
  33. {async-lambda-unstable-0.3.12 → async-lambda-unstable-0.4.3}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: async-lambda-unstable
3
- Version: 0.3.12
3
+ Version: 0.4.3
4
4
  Summary: A framework for creating AWS Lambda Async Workflows. - Unstable Branch
5
5
  Author-email: "Nuclei, Inc" <engineering@nuclei.ai>
6
6
  Description-Content-Type: text/markdown
@@ -186,8 +186,83 @@ def api_task(event: APIEvent):
186
186
  event.headers # request headers
187
187
  event.querystring_params # request querystring params
188
188
  event.body # request body
189
+ event.headers # This is a case insensitive dict
189
190
  ```
190
191
 
192
+ # Middleware
193
+
194
+ Middleware functions can be registered with controllers which will wrap the execution of tasks.
195
+ These functions can be configured to trigger on specific types of tasks and can trigger
196
+ side effects and modify the `event` or `response` objects.
197
+
198
+ Middleware functions must have the signature `Callable[[BaseEvent, Callable[[BaseEvent], T]], T]`.
199
+ The first argument is the `event`, and the second argument (`call_next`) is a function which will propagate the
200
+ calls down the middleware/task stack. The `call_next` function must be called, and its result in most cases be returned.
201
+ If this is not done then tasks will not run as expected.
202
+
203
+ **Extreme care should be taken with middleware as a simple mistake can have catastrophic effects.**
204
+
205
+ - Middleware functions are run in the order which they were registered and parent controller middleware will be run first.
206
+
207
+ - Middleware functions which are registered more than once will only be run once.
208
+
209
+ Registration can be done when the `AsyncLambdaController` is initialized with the parameter `middleware` or by using the `add_middleware` method.
210
+
211
+ Middleware functions have three sections:
212
+
213
+ 1. Pre task
214
+ 2. Task execution
215
+ 3. Post task
216
+
217
+ ```python
218
+ def async_lambda_middleware(event: BaseEvent, call_next):
219
+ # pre task
220
+ result = call_next(event) # task execution
221
+ # post task
222
+ return result
223
+ ```
224
+
225
+ If there are multiple middleware functions then `call_next` will actually be calling the next middleware function in the stack.
226
+
227
+ For example if there is middleware functions `A` and `B` registered in that order.
228
+ Then the execution order would go:
229
+
230
+ `A(Pre)` -> `B(Pre)` -> `Task` -> `B(Post)` -> `A(Post)`
231
+
232
+ EX:
233
+
234
+ ```python
235
+ def async_task_only_middleware(event: ManagedSQSEvent, call_next):
236
+ print(f"Invocation Payload: {event}")
237
+ result = call_next(event)
238
+ print(f"Invocation Result: {result}")
239
+ return result
240
+
241
+ def all_task_types_middleware(event: BaseEvent, call_next):
242
+ print(f"This event is of the type {type(event)}")
243
+ result = call_next(event)
244
+ print(f"The result is of the type {type(result)}")
245
+ return event
246
+
247
+ controller = AsyncLambdaController(middleware=[([BaseEvent], all_task_types_middleware)])
248
+
249
+ controller.add_middleware([ManagedSQSEvent], async_task_only_middleware)
250
+
251
+ @controller.async_task("ATask")
252
+ def a_task(event: ManagedSQSEvent):
253
+ pass
254
+
255
+ @controller.api_task("BTask", "/test", "get")
256
+ def b_task(event: APIEvent):
257
+ return "hello world"
258
+ ```
259
+
260
+ In this scenario when `ATask` is invoked first `all_task_types_middleware` will be run, then
261
+ `async_task_only_middleware` will be run and finally the `a_task` function will be executed.
262
+
263
+ When `BTask` is invoked first `all_task_types_middleware` will be run, and then the `b_task`
264
+ function will be executed
265
+
191
266
  # `async-lambda` config
192
267
 
193
268
  Configuration options can be set with the `.async_lambda/config.json` file.
@@ -178,8 +178,83 @@ def api_task(event: APIEvent):
178
178
  event.headers # request headers
179
179
  event.querystring_params # request querystring params
180
180
  event.body # request body
181
+ event.headers # This is a case insensitive dict
181
182
  ```
182
183
 
184
+ # Middleware
185
+
186
+ Middleware functions can be registered with controllers which will wrap the execution of tasks.
187
+ These functions can be configured to trigger on specific types of tasks and can trigger
188
+ side effects and modify the `event` or `response` objects.
189
+
190
+ Middleware functions must have the signature `Callable[[BaseEvent, Callable[[BaseEvent], T]], T]`.
191
+ The first argument is the `event`, and the second argument (`call_next`) is a function which will propagate the
192
+ calls down the middleware/task stack. The `call_next` function must be called, and its result in most cases be returned.
193
+ If this is not done then tasks will not run as expected.
194
+
195
+ **Extreme care should be taken with middleware as a simple mistake can have catastrophic effects.**
196
+
197
+ - Middleware functions are run in the order which they were registered and parent controller middleware will be run first.
198
+
199
+ - Middleware functions which are registered more than once will only be run once.
200
+
201
+ Registration can be done when the `AsyncLambdaController` is initialized with the parameter `middleware` or by using the `add_middleware` method.
202
+
203
+ Middleware functions have three sections:
204
+
205
+ 1. Pre task
206
+ 2. Task execution
207
+ 3. Post task
208
+
209
+ ```python
210
+ def async_lambda_middleware(event: BaseEvent, call_next):
211
+ # pre task
212
+ result = call_next(event) # task execution
213
+ # post task
214
+ return result
215
+ ```
216
+
217
+ If there are multiple middleware functions then `call_next` will actually be calling the next middleware function in the stack.
218
+
219
+ For example if there is middleware functions `A` and `B` registered in that order.
220
+ Then the execution order would go:
221
+
222
+ `A(Pre)` -> `B(Pre)` -> `Task` -> `B(Post)` -> `A(Post)`
223
+
224
+ EX:
225
+
226
+ ```python
227
+ def async_task_only_middleware(event: ManagedSQSEvent, call_next):
228
+ print(f"Invocation Payload: {event}")
229
+ result = call_next(event)
230
+ print(f"Invocation Result: {result}")
231
+ return result
232
+
233
+ def all_task_types_middleware(event: BaseEvent, call_next):
234
+ print(f"This event is of the type {type(event)}")
235
+ result = call_next(event)
236
+ print(f"The result is of the type {type(result)}")
237
+ return event
238
+
239
+ controller = AsyncLambdaController(middleware=[([BaseEvent], all_task_types_middleware)])
240
+
241
+ controller.add_middleware([ManagedSQSEvent], async_task_only_middleware)
242
+
243
+ @controller.async_task("ATask")
244
+ def a_task(event: ManagedSQSEvent):
245
+ pass
246
+
247
+ @controller.api_task("BTask", "/test", "get")
248
+ def b_task(event: APIEvent):
249
+ return "hello world"
250
+ ```
251
+
252
+ In this scenario when `ATask` is invoked first `all_task_types_middleware` will be run, then
253
+ `async_task_only_middleware` will be run and finally the `a_task` function will be executed.
254
+
255
+ When `BTask` is invoked first `all_task_types_middleware` will be run, and then the `b_task`
256
+ function will be executed
257
+
183
258
  # `async-lambda` config
184
259
 
185
260
  Configuration options can be set with the `.async_lambda/config.json` file.
@@ -7,9 +7,10 @@ from .env import enable_force_sync_mode as enable_force_sync_mode
7
7
  from .env import is_build_mode as is_build_mode
8
8
  from .models.case_insensitive_dict import CaseInsensitiveDict as CaseInsensitiveDict
9
9
  from .models.events.api_event import APIEvent as APIEvent
10
+ from .models.events.base_event import BaseEvent as BaseEvent
10
11
  from .models.events.dynamodb_event import DynamoDBEvent as DynamoDBEvent
11
12
  from .models.events.managed_sqs_event import ManagedSQSEvent as ManagedSQSEvent
12
13
  from .models.events.scheduled_event import ScheduledEvent as ScheduledEvent
13
14
  from .models.events.unmanaged_sqs_event import UnmanagedSQSEvent as UnmanagedSQSEvent
14
15
 
15
- __version__ = "0.3.12"
16
+ __version__ = "0.4.3"
@@ -4,13 +4,15 @@ import logging
4
4
  import re
5
5
  import time
6
6
  from datetime import datetime, timezone
7
- from typing import Any, Callable, Dict, Optional, Tuple
7
+ from typing import Any, Callable, Dict, List, Optional, Tuple, Type
8
8
  from uuid import uuid4
9
9
 
10
10
  from . import env
11
11
  from .build_config import get_build_config_for_stage
12
12
  from .client import get_s3_client, get_sqs_client
13
+ from .middleware import MET, RT, MiddlewareFunction, MiddlewareRegistration
13
14
  from .models.events.api_event import APIEvent
15
+ from .models.events.base_event import BaseEvent
14
16
  from .models.events.dynamodb_event import DynamoDBEvent
15
17
  from .models.events.managed_sqs_event import ManagedSQSEvent
16
18
  from .models.events.scheduled_event import ScheduledEvent
@@ -32,6 +34,8 @@ class AsyncLambdaController:
32
34
  current_lane: Optional[int] = None
33
35
  current_invocation_id: Optional[str] = None
34
36
  parent_controller: Optional["AsyncLambdaController"] = None
37
+ middleware: List[MiddlewareRegistration]
38
+
35
39
  dlq_task_id: Optional[str] = None
36
40
 
37
41
  def __init__(
@@ -39,11 +43,34 @@ class AsyncLambdaController:
39
43
  is_sub: bool = False,
40
44
  lane_count: Optional[int] = None,
41
45
  propagate_lane_assignment: Optional[bool] = None,
46
+ middleware: Optional[List[MiddlewareRegistration]] = None,
47
+ exception_handler: Optional[Callable[[Exception], None]] = None,
42
48
  ):
43
49
  self.tasks = dict()
44
50
  self.is_sub = is_sub
45
51
  self.lane_count = lane_count
46
52
  self.propagate_lane_assignment = propagate_lane_assignment
53
+ self.middleware = middleware or list()
54
+ self.exception_handler = exception_handler
55
+
56
+ def add_middleware(
57
+ self, event_types: List[Type[BaseEvent]], func: MiddlewareFunction[MET, RT]
58
+ ):
59
+ self.middleware.append((event_types, func))
60
+
61
+ def get_middleware_for_event(self, event: MET) -> List[MiddlewareFunction[MET, RT]]:
62
+ if self.parent_controller is not None:
63
+ _middleware_functions = self.parent_controller.get_middleware_for_event(
64
+ event
65
+ )
66
+ else:
67
+ _middleware_functions = list()
68
+
69
+ for event_types, func in self.middleware:
70
+ if any(isinstance(event, event_type) for event_type in event_types):
71
+ _middleware_functions.append(func)
72
+
73
+ return _middleware_functions
47
74
 
48
75
  def add_task(self, task: AsyncLambdaTask):
49
76
  """
@@ -382,7 +409,7 @@ class AsyncLambdaController:
382
409
  """
383
410
  Invoke an Async-Lambda task.
384
411
  """
385
- self._async_invoke(
412
+ return self._async_invoke(
386
413
  destination_task_id=task_id,
387
414
  payload=payload,
388
415
  delay=delay,
@@ -0,0 +1,30 @@
1
+ from typing import Callable, Generic, List, Tuple, Type, TypeVar
2
+
3
+ from .models.events.base_event import BaseEvent
4
+
5
+ MET = TypeVar("MET", bound=BaseEvent)
6
+ RT = TypeVar("RT")
7
+
8
+ MiddlewareFunction = Callable[[MET, Callable[[MET], RT]], RT]
9
+ MiddlewareRegistration = Tuple[List[Type[MET]], MiddlewareFunction[MET, RT]]
10
+
11
+
12
+ class MiddlewareStackExecutor(Generic[MET, RT]):
13
+ def __init__(
14
+ self,
15
+ middleware: List[MiddlewareFunction],
16
+ final: Callable[[MET], RT],
17
+ ):
18
+ self.middleware = middleware.copy()
19
+ self.final = final
20
+ self._ran_fns = list()
21
+
22
+ def call_next(self, event: MET) -> RT:
23
+ while True:
24
+ if len(self.middleware) == 0:
25
+ return self.final(event)
26
+ next_fn = self.middleware.pop(0)
27
+ if next_fn in self._ran_fns:
28
+ continue
29
+ self._ran_fns.append(next_fn)
30
+ return next_fn(event, self.call_next)
@@ -13,6 +13,7 @@ from ..config import config
13
13
  if TYPE_CHECKING:
14
14
  from ..controller import AsyncLambdaController # pragma: not covered
15
15
 
16
+ from ..middleware import RT, MiddlewareStackExecutor
16
17
  from .events.dynamodb_event import DynamoDBEvent
17
18
  from .events.managed_sqs_event import ManagedSQSEvent
18
19
  from .events.scheduled_event import ScheduledEvent
@@ -33,7 +34,7 @@ EventType = TypeVar(
33
34
  )
34
35
 
35
36
 
36
- class AsyncLambdaTask(Generic[EventType]):
37
+ class AsyncLambdaTask(Generic[EventType, RT]):
37
38
  controller: "AsyncLambdaController"
38
39
  task_id: str
39
40
  trigger_type: TaskTriggerType
@@ -46,12 +47,12 @@ class AsyncLambdaTask(Generic[EventType]):
46
47
  init_tasks: List[Union[Callable[[str], Any], Callable[[], Any]]]
47
48
  _has_run_init_tasks: bool
48
49
 
49
- executable: Callable[[EventType], Any]
50
+ executable: Callable[[EventType], RT]
50
51
 
51
52
  def __init__(
52
53
  self,
53
54
  controller: "AsyncLambdaController",
54
- executable: Callable[[EventType], Any],
55
+ executable: Callable[[EventType], RT],
55
56
  task_id: str,
56
57
  trigger_type: TaskTriggerType,
57
58
  trigger_config: Optional[dict] = None,
@@ -481,9 +482,13 @@ class AsyncLambdaTask(Generic[EventType]):
481
482
  return self.controller.get_task(self.trigger_config["dlq_task_id"])
482
483
  return self.controller.get_dlq_task()
483
484
 
484
- def execute(self, event: EventType) -> Any:
485
+ def execute(self, event: EventType) -> RT:
485
486
  """
486
487
  Executes the tasks function
487
488
  """
488
489
  self._run_init_tasks()
489
- return self.executable(event)
490
+ middleware = self.controller.get_middleware_for_event(event)
491
+ middleware_stack_executor = MiddlewareStackExecutor[EventType, RT](
492
+ middleware=middleware, final=self.executable
493
+ )
494
+ return middleware_stack_executor.call_next(event)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: async-lambda-unstable
3
- Version: 0.3.12
3
+ Version: 0.4.3
4
4
  Summary: A framework for creating AWS Lambda Async Workflows. - Unstable Branch
5
5
  Author-email: "Nuclei, Inc" <engineering@nuclei.ai>
6
6
  Description-Content-Type: text/markdown
@@ -186,8 +186,83 @@ def api_task(event: APIEvent):
186
186
  event.headers # request headers
187
187
  event.querystring_params # request querystring params
188
188
  event.body # request body
189
+ event.headers # This is a case insensitive dict
189
190
  ```
190
191
 
192
+ # Middleware
193
+
194
+ Middleware functions can be registered with controllers which will wrap the execution of tasks.
195
+ These functions can be configured to trigger on specific types of tasks and can trigger
196
+ side effects and modify the `event` or `response` objects.
197
+
198
+ Middleware functions must have the signature `Callable[[BaseEvent, Callable[[BaseEvent], T]], T]`.
199
+ The first argument is the `event`, and the second argument (`call_next`) is a function which will propagate the
200
+ calls down the middleware/task stack. The `call_next` function must be called, and its result in most cases be returned.
201
+ If this is not done then tasks will not run as expected.
202
+
203
+ **Extreme care should be taken with middleware as a simple mistake can have catastrophic effects.**
204
+
205
+ - Middleware functions are run in the order which they were registered and parent controller middleware will be run first.
206
+
207
+ - Middleware functions which are registered more than once will only be run once.
208
+
209
+ Registration can be done when the `AsyncLambdaController` is initialized with the parameter `middleware` or by using the `add_middleware` method.
210
+
211
+ Middleware functions have three sections:
212
+
213
+ 1. Pre task
214
+ 2. Task execution
215
+ 3. Post task
216
+
217
+ ```python
218
+ def async_lambda_middleware(event: BaseEvent, call_next):
219
+ # pre task
220
+ result = call_next(event) # task execution
221
+ # post task
222
+ return result
223
+ ```
224
+
225
+ If there are multiple middleware functions then `call_next` will actually be calling the next middleware function in the stack.
226
+
227
+ For example if there is middleware functions `A` and `B` registered in that order.
228
+ Then the execution order would go:
229
+
230
+ `A(Pre)` -> `B(Pre)` -> `Task` -> `B(Post)` -> `A(Post)`
231
+
232
+ EX:
233
+
234
+ ```python
235
+ def async_task_only_middleware(event: ManagedSQSEvent, call_next):
236
+ print(f"Invocation Payload: {event}")
237
+ result = call_next(event)
238
+ print(f"Invocation Result: {result}")
239
+ return result
240
+
241
+ def all_task_types_middleware(event: BaseEvent, call_next):
242
+ print(f"This event is of the type {type(event)}")
243
+ result = call_next(event)
244
+ print(f"The result is of the type {type(result)}")
245
+ return event
246
+
247
+ controller = AsyncLambdaController(middleware=[([BaseEvent], all_task_types_middleware)])
248
+
249
+ controller.add_middleware([ManagedSQSEvent], async_task_only_middleware)
250
+
251
+ @controller.async_task("ATask")
252
+ def a_task(event: ManagedSQSEvent):
253
+ pass
254
+
255
+ @controller.api_task("BTask", "/test", "get")
256
+ def b_task(event: APIEvent):
257
+ return "hello world"
258
+ ```
259
+
260
+ In this scenario when `ATask` is invoked first `all_task_types_middleware` will be run, then
261
+ `async_task_only_middleware` will be run and finally the `a_task` function will be executed.
262
+
263
+ When `BTask` is invoked first `all_task_types_middleware` will be run, and then the `b_task`
264
+ function will be executed
265
+
191
266
  # `async-lambda` config
192
267
 
193
268
  Configuration options can be set with the `.async_lambda/config.json` file.
@@ -8,6 +8,7 @@ async_lambda/config.py
8
8
  async_lambda/controller.py
9
9
  async_lambda/defer.py
10
10
  async_lambda/env.py
11
+ async_lambda/middleware.py
11
12
  async_lambda/py.typed
12
13
  async_lambda/util.py
13
14
  async_lambda/models/__init__.py