add batch

This commit is contained in:
2025-05-21 15:48:59 -03:00
parent 7f4fec6e1e
commit 249116cc76
20 changed files with 786 additions and 627 deletions

View File

@@ -0,0 +1,118 @@
import inspect
import logging
from contextlib import AbstractContextManager
from enum import Enum
from typing import Any, Callable, NamedTuple, Self, Sequence
logger = logging.getLogger(__name__)
logging.basicConfig(level=logging.INFO)
class Status(Enum):
SUCCESS = 'success'
FAIL = 'fail'
class Result(NamedTuple):
status: Status
cause: Any
record: Any
class BatchProcessor(AbstractContextManager):
"""
This class provides a structured way to process a sequence of records,
capturing both successful and failed executions along with exception details.
Batch processing utility inspired by AWS Lambda Powertools for Python:
https://github.com/aws-powertools/powertools-lambda-python/blob/develop/aws_lambda_powertools/utilities/batch/base.py
Attributes
----------
successes : list
List of successfully processed records.
failures : list
List of records that failed to process.
exceptions : list
List of exceptions raised during processing.
Examples
--------
>>> def handler(record, context):
... if record % 2 == 0:
... return record * 2
... else:
... raise ValueError("Odd number not allowed")
...
>>> records = [1, 2, 3, 4]
>>> processor = BatchProcessor()
>>> with processor(records, handler) as p:
... results = p.process()
...
>>> for result in results:
... print(result.status, result.cause, result.record)
Status.FAIL ValueError: Odd number not allowed 1
Status.SUCCESS 4 2
Status.FAIL ValueError: Odd number not allowed 3
Status.SUCCESS 8 4
"""
def __init__(self) -> None:
self.successes: list[Any] = []
self.failures: list[Any] = []
self.exceptions: list[BaseException] = []
def __call__(
self,
records: Sequence[Any],
handler: Callable[..., Any],
context: dict[str, Any] | None = None,
) -> Self:
self.records = records
self.handler = handler
if context is None:
self._handler_accepts_context = False
else:
self.context = context
self._handler_accepts_context = (
'context' in inspect.signature(self.handler).parameters
)
return self
def __enter__(self):
"""Remove results from previous execution."""
self.successes.clear()
self.failures.clear()
self.exceptions.clear()
return self
def __exit__(self, *exc_details) -> None:
pass
def process(self) -> Sequence[Result]:
return tuple(self._process_record(record) for record in self.records)
def _process_record(self, record: Any) -> Result:
"""
Processes a single record using the handler function with optional context.
Returns a Result indicating success or failure.
"""
try:
if self._handler_accepts_context:
result = self.handler(record, self.context)
else:
result = self.handler(record)
self.successes.append(record)
return Result(Status.SUCCESS, result, record)
except Exception as exc:
exc_str = f'{type(exc).__name__}: {exc}'
logger.debug(f'Record processing exception: {exc_str}')
self.exceptions.append(exc)
self.failures.append(record)
return Result(Status.FAIL, exc_str, record)

View File

@@ -0,0 +1,18 @@
import hashlib
def first_word(s: str) -> str:
"""Returns the first word from a string."""
return s.split(' ', 1)[0]
def truncate_str(s: str, maxlen: int = 30) -> str:
"""Truncates a string to fit within a maximum length, adding '...' if truncated."""
if len(s) <= maxlen:
return s
return s[: maxlen - 3] + '...'
def md5_hash(s: str) -> str:
"""Computes the MD5 hash of a string."""
return hashlib.md5(s.encode()).hexdigest()

View File

@@ -1,6 +1,6 @@
[project]
name = "layercake"
version = "0.2.15"
version = "0.2.17"
description = "Packages shared dependencies to optimize deployment and ensure consistency across functions."
readme = "README.md"
authors = [

View File

@@ -0,0 +1,56 @@
from layercake.batch import BatchProcessor, Result, Status
processor = BatchProcessor()
def test_batch():
def record_handler(record: bool):
if record:
return True
raise ValueError('Invalid record')
records = (
True,
True,
False,
)
with processor(records=records, handler=record_handler) as p:
processed_messages = p.process()
assert processed_messages == (
Result(Status.SUCCESS, True, True),
Result(Status.SUCCESS, True, True),
Result(Status.FAIL, 'ValueError: Invalid record', False),
)
assert processor.successes == [True, True]
assert processor.failures == [False]
with processor(records=(False,), handler=record_handler):
processed_messages = processor.process()
assert processed_messages == (
Result(Status.FAIL, 'ValueError: Invalid record', False),
)
assert processor.successes == []
assert processor.failures == [False]
def test_batch_context():
def record_handler(val: int, context: dict):
return val * context['multiplier']
with processor(
records=(2, 3, 4),
handler=record_handler,
context={'multiplier': 2},
):
processed_messages = processor.process()
assert processed_messages == (
Result(Status.SUCCESS, 4, 2),
Result(Status.SUCCESS, 6, 3),
Result(Status.SUCCESS, 8, 4),
)

2
layercake/uv.lock generated
View File

@@ -589,7 +589,7 @@ wheels = [
[[package]]
name = "layercake"
version = "0.2.14"
version = "0.2.17"
source = { editable = "." }
dependencies = [
{ name = "arnparse" },