update batch

This commit is contained in:
2025-05-30 15:32:31 -03:00
parent efd962cdba
commit b048febbd5
24 changed files with 455 additions and 69 deletions

View File

@@ -24,6 +24,7 @@ Example
""" """
from dataclasses import asdict, dataclass from dataclasses import asdict, dataclass
from enum import Enum
from typing import Any from typing import Any
from aws_lambda_powertools import Logger, Tracer from aws_lambda_powertools import Logger, Tracer
@@ -33,7 +34,6 @@ from aws_lambda_powertools.utilities.data_classes.api_gateway_authorizer_event i
APIGatewayAuthorizerResponseV2, APIGatewayAuthorizerResponseV2,
) )
from aws_lambda_powertools.utilities.typing import LambdaContext from aws_lambda_powertools.utilities.typing import LambdaContext
from botocore.endpoint_provider import Enum
from layercake.dynamodb import DynamoDBCollection, DynamoDBPersistenceLayer, KeyPair from layercake.dynamodb import DynamoDBCollection, DynamoDBPersistenceLayer, KeyPair
from layercake.funcs import pick from layercake.funcs import pick

View File

@@ -4,12 +4,12 @@ import boto3
def get_dynamodb_client(): def get_dynamodb_client():
sam_local = os.getenv('AWS_SAM_LOCAL') running_sam_local = os.getenv('AWS_SAM_LOCAL')
if os.getenv('AWS_LAMBDA_FUNCTION_NAME') and not sam_local: if os.getenv('AWS_LAMBDA_FUNCTION_NAME') and not running_sam_local:
return boto3.client('dynamodb') return boto3.client('dynamodb')
url = 'host.docker.internal' if sam_local else 'localhost' url = 'host.docker.internal' if running_sam_local else 'localhost'
return boto3.client('dynamodb', endpoint_url=f'http://{url}:8000') return boto3.client('dynamodb', endpoint_url=f'http://{url}:8000')

View File

@@ -40,7 +40,7 @@ class Course(BaseModel):
class Enrollment(BaseModel): class Enrollment(BaseModel):
id: UUID4 = Field(default_factory=uuid4) id: UUID4 | str = Field(default_factory=uuid4)
user: User user: User
course: Course course: Course
progress: int = Field(default=0, ge=0, le=100) progress: int = Field(default=0, ge=0, le=100)

View File

@@ -46,7 +46,10 @@ def cancel(id: str, payload: Cancel):
set_status_as_canceled( set_status_as_canceled(
id, id,
lock_hash=payload.lock_hash, lock_hash=payload.lock_hash,
author=user.model_dump(), # type: ignore author={
'id': user.id,
'name': user.name,
},
course=payload.course, # type: ignore course=payload.course, # type: ignore
vacancy_key=KeyPair.parse_obj(payload.vacancy), vacancy_key=KeyPair.parse_obj(payload.vacancy),
persistence_layer=enrollment_layer, persistence_layer=enrollment_layer,

View File

@@ -1,4 +1,5 @@
from datetime import datetime from datetime import datetime
from http import HTTPStatus
from aws_lambda_powertools.event_handler.api_gateway import Router from aws_lambda_powertools.event_handler.api_gateway import Router
from layercake.batch import BatchProcessor from layercake.batch import BatchProcessor
@@ -8,13 +9,15 @@ from layercake.dynamodb import (
) )
from pydantic import BaseModel from pydantic import BaseModel
from api_gateway import JSONResponse
from boto3clients import dynamodb_client from boto3clients import dynamodb_client
from config import ( from config import (
ENROLLMENT_TABLE, ENROLLMENT_TABLE,
USER_TABLE, USER_TABLE,
) )
from middlewares import Tenant, TenantMiddleware from middlewares import Tenant, TenantMiddleware
from models import Course, User from models import Course, Enrollment, User
from rules.enrollment import enroll
router = Router() router = Router()
@@ -28,6 +31,7 @@ processor = BatchProcessor()
class Item(BaseModel): class Item(BaseModel):
user: User user: User
course: Course course: Course
deduplication_window: dict = {}
schedule_date: datetime | None = None schedule_date: datetime | None = None
@@ -49,16 +53,33 @@ def enroll_(payload: Payload):
with processor(payload.items, handler, context): with processor(payload.items, handler, context):
processor.process() processor.process()
return {} print(processor.exceptions)
return JSONResponse(
HTTPStatus.OK,
{
'successes': processor.successes,
'failures': processor.failures,
'exceptions': [str(exc) for exc in processor.exceptions],
},
)
def handler(record: Item, context: dict): def handler(record: Item, context: dict):
tenant: Tenant = context['tenant'] tenant: Tenant = context['tenant']
# enroll( enrollment = Enrollment(
# enrollment=Enrollment(user=[]) user=record.user,
# tenant={ course=record.course,
# 'id': str(tenant.id), )
# 'name': tenant.name,
# }, enroll(
# persistence_layer=enrollment_layer, enrollment=enrollment,
# ) tenant={
'id': str(tenant.id),
'name': tenant.name,
},
deduplication_window=record.deduplication_window, # type: ignore
persistence_layer=enrollment_layer,
)
return enrollment

View File

@@ -125,6 +125,11 @@ def enroll(
ttl_expiration = ttl( ttl_expiration = ttl(
start_dt=now_ + timedelta(days=course.access_period - offset_days) start_dt=now_ + timedelta(days=course.access_period - offset_days)
) )
class DeduplicationConflictError(Exception):
def __init__(self, *args):
super().__init__('Enrollment already exists')
transact.put( transact.put(
item={ item={
'id': 'lock', 'id': 'lock',
@@ -134,6 +139,7 @@ def enroll(
'ttl': ttl_expiration, 'ttl': ttl_expiration,
}, },
cond_expr='attribute_not_exists(sk)', cond_expr='attribute_not_exists(sk)',
exc_cls=DeduplicationConflictError,
) )
transact.put( transact.put(
item={ item={

View File

@@ -4,7 +4,6 @@ from typing import TypedDict
from aws_lambda_powertools.event_handler.exceptions import ( from aws_lambda_powertools.event_handler.exceptions import (
BadRequestError, BadRequestError,
) )
from botocore.tokens import timedelta
from layercake.dateutils import now, ttl from layercake.dateutils import now, ttl
from layercake.dynamodb import ( from layercake.dynamodb import (
ComposeKey, ComposeKey,

View File

@@ -8,6 +8,7 @@ dependencies = ["layercake"]
[dependency-groups] [dependency-groups]
dev = [ dev = [
"boto3-stubs[essential]>=1.38.26",
"jsonlines>=4.0.0", "jsonlines>=4.0.0",
"pytest>=8.3.4", "pytest>=8.3.4",
"pytest-cov>=6.0.0", "pytest-cov>=6.0.0",

View File

@@ -1,3 +1,4 @@
import json
from http import HTTPMethod, HTTPStatus from http import HTTPMethod, HTTPStatus
from layercake.dynamodb import ( from layercake.dynamodb import (
@@ -36,6 +37,21 @@ def test_enroll(
'id': '6d69a34a-cefd-40aa-a89b-dceb694c3e61', 'id': '6d69a34a-cefd-40aa-a89b-dceb694c3e61',
'name': 'pytest', 'name': 'pytest',
}, },
'deduplication_window': {
'offset_days': 60,
},
},
{
'user': {
'id': '9a41e867-55e1-4573-bd27-7b5d1d1bcfde',
'name': 'Tiago Maciel',
'email': 'tiago@somosbeta.com.br',
'cpf': '08679004901',
},
'course': {
'id': '6d69a34a-cefd-40aa-a89b-dceb694c3e61',
'name': 'pytest',
},
}, },
], ],
}, },
@@ -43,8 +59,8 @@ def test_enroll(
lambda_context, lambda_context,
) )
assert r['statusCode'] == HTTPStatus.OK # assert r['statusCode'] == HTTPStatus.OK
print(r) print(json.loads(r['body']))
def test_vacancies( def test_vacancies(

View File

@@ -21,3 +21,4 @@
{"id": {"S": "email"}, "sk": {"S": "sergio@somosbeta.com.br"}} {"id": {"S": "email"}, "sk": {"S": "sergio@somosbeta.com.br"}}
{"id": {"S": "cpf"}, "sk": {"S": "07879819908"}} {"id": {"S": "cpf"}, "sk": {"S": "07879819908"}}
{"id": {"S": "cpf"}, "sk": {"S": "08679004901"}} {"id": {"S": "cpf"}, "sk": {"S": "08679004901"}}
{"id": {"S": "lock"}, "sk": {"S": "c2116a43f8f1aed659a10c83dab17ed3"}}

119
http-api/uv.lock generated
View File

@@ -103,6 +103,30 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/6e/98/bac2404ff6183e1aaeebfefe6f345d63a1395b9a710be5ad24dcad9538ed/boto3-1.37.20-py3-none-any.whl", hash = "sha256:225dbc75d79816cb9b28cc74a63c9fa0f2d70530d603dacd82634f362f6679c1", size = 139561, upload-time = "2025-03-25T19:21:44.723Z" }, { url = "https://files.pythonhosted.org/packages/6e/98/bac2404ff6183e1aaeebfefe6f345d63a1395b9a710be5ad24dcad9538ed/boto3-1.37.20-py3-none-any.whl", hash = "sha256:225dbc75d79816cb9b28cc74a63c9fa0f2d70530d603dacd82634f362f6679c1", size = 139561, upload-time = "2025-03-25T19:21:44.723Z" },
] ]
[[package]]
name = "boto3-stubs"
version = "1.38.26"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "botocore-stubs" },
{ name = "types-s3transfer" },
]
sdist = { url = "https://files.pythonhosted.org/packages/1e/89/9e2658210ac11459405ed8c82f47be533a5d16ae3b8203c90564b7a738a0/boto3_stubs-1.38.26.tar.gz", hash = "sha256:492e59e42323de43018ffa6d00d3bb2b93d1fead042e76c6a68fd0a0c0fe3236", size = 99065, upload-time = "2025-05-29T19:47:49.383Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/48/51/881ac3c7ebeefdfe0e712b1f4d815261707a492187ea301506168cf6fc20/boto3_stubs-1.38.26-py3-none-any.whl", hash = "sha256:3022b2a8f6925c60c9ce68c5e090ff9fd2bad0c918300395a1c242681a67c11c", size = 68669, upload-time = "2025-05-29T19:47:42.352Z" },
]
[package.optional-dependencies]
essential = [
{ name = "mypy-boto3-cloudformation" },
{ name = "mypy-boto3-dynamodb" },
{ name = "mypy-boto3-ec2" },
{ name = "mypy-boto3-lambda" },
{ name = "mypy-boto3-rds" },
{ name = "mypy-boto3-s3" },
{ name = "mypy-boto3-sqs" },
]
[[package]] [[package]]
name = "botocore" name = "botocore"
version = "1.37.20" version = "1.37.20"
@@ -117,6 +141,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/2d/5b/f96cf58c37704b907ac2f9cc94e45ba0a2aa3b2062421aa8b8614f1d78de/botocore-1.37.20-py3-none-any.whl", hash = "sha256:c34f4f25fda7c4f726adf5a948590bd6bd7892c05278d31e344b5908e7b43301", size = 13432464, upload-time = "2025-03-25T19:21:28.115Z" }, { url = "https://files.pythonhosted.org/packages/2d/5b/f96cf58c37704b907ac2f9cc94e45ba0a2aa3b2062421aa8b8614f1d78de/botocore-1.37.20-py3-none-any.whl", hash = "sha256:c34f4f25fda7c4f726adf5a948590bd6bd7892c05278d31e344b5908e7b43301", size = 13432464, upload-time = "2025-03-25T19:21:28.115Z" },
] ]
[[package]]
name = "botocore-stubs"
version = "1.38.26"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "types-awscrt" },
]
sdist = { url = "https://files.pythonhosted.org/packages/08/f2/fd2f5a8ef00bbcfe00c12b8c49e247510266929dff5578b6fec360967a21/botocore_stubs-1.38.26.tar.gz", hash = "sha256:3bbf7662fc97e28a50dc959752619cf57029194987268b4dc13df4e54767204c", size = 42315, upload-time = "2025-05-29T20:18:25.22Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/28/4a/f99ef1ff37620c0c23aa67e3d9de5fce0f98b38fd26e6d30438ee440c0fc/botocore_stubs-1.38.26-py3-none-any.whl", hash = "sha256:c86ac7d2c7e24ea50a866a9686a293dfe8b40281cc3465d79e2e0e48d35ad93b", size = 65628, upload-time = "2025-05-29T20:18:23.125Z" },
]
[[package]] [[package]]
name = "brotli" name = "brotli"
version = "1.1.0" version = "1.1.0"
@@ -450,6 +486,7 @@ dependencies = [
[package.dev-dependencies] [package.dev-dependencies]
dev = [ dev = [
{ name = "boto3-stubs", extra = ["essential"] },
{ name = "jsonlines" }, { name = "jsonlines" },
{ name = "pytest" }, { name = "pytest" },
{ name = "pytest-cov" }, { name = "pytest-cov" },
@@ -462,6 +499,7 @@ requires-dist = [{ name = "layercake", directory = "../layercake" }]
[package.metadata.requires-dev] [package.metadata.requires-dev]
dev = [ dev = [
{ name = "boto3-stubs", extras = ["essential"], specifier = ">=1.38.26" },
{ name = "jsonlines", specifier = ">=4.0.0" }, { name = "jsonlines", specifier = ">=4.0.0" },
{ name = "pytest", specifier = ">=8.3.4" }, { name = "pytest", specifier = ">=8.3.4" },
{ name = "pytest-cov", specifier = ">=6.0.0" }, { name = "pytest-cov", specifier = ">=6.0.0" },
@@ -584,6 +622,69 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/e0/2f/264c07a3f488260ea36c78cbc201b76e6baf9ef92e0c7f78657a6a5e5f22/meilisearch-0.34.0-py3-none-any.whl", hash = "sha256:fae8ad2a15d12c27fa0a1fff2ae2e4e3e2e22b869950408d63c87e2c095a9f61", size = 24373, upload-time = "2025-02-18T05:50:32.73Z" }, { url = "https://files.pythonhosted.org/packages/e0/2f/264c07a3f488260ea36c78cbc201b76e6baf9ef92e0c7f78657a6a5e5f22/meilisearch-0.34.0-py3-none-any.whl", hash = "sha256:fae8ad2a15d12c27fa0a1fff2ae2e4e3e2e22b869950408d63c87e2c095a9f61", size = 24373, upload-time = "2025-02-18T05:50:32.73Z" },
] ]
[[package]]
name = "mypy-boto3-cloudformation"
version = "1.38.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/40/d5/35b9301c8b2fb870e58401d13fec36de2c83f2ddef48398b8c89c9a58995/mypy_boto3_cloudformation-1.38.0.tar.gz", hash = "sha256:563399166c07e91e0695fb1e58103a248b2bee0db5e2c3f07155776dd6311805", size = 57702, upload-time = "2025-04-22T21:19:31.221Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/1e/2f/d20ad6e0113f982ea255fcb4ed94f70d0111757d7d03bfacebc2d9f60ba4/mypy_boto3_cloudformation-1.38.0-py3-none-any.whl", hash = "sha256:a1411aa5875b737492aaac5f7e8ce450f034c18f972eb608a9eba6fe35837f6a", size = 69607, upload-time = "2025-04-22T21:19:29.235Z" },
]
[[package]]
name = "mypy-boto3-dynamodb"
version = "1.38.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/6b/7f/72b68d275a80a42675c36249b80dd79ec5c7d9bd1f5cc93cdb572f866722/mypy_boto3_dynamodb-1.38.4.tar.gz", hash = "sha256:5cf3787631e312b3d75f89a6cbbbd4ad786a76f5d565af023febf03fbf23c0b5", size = 47461, upload-time = "2025-04-28T19:26:22.728Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/6a/35/3d0ceabb0a9f3765f509cb9dce6ddfa939114682b1acc442f52a755e9bc8/mypy_boto3_dynamodb-1.38.4-py3-none-any.whl", hash = "sha256:6b29d89c649eeb1e894118bee002cb8b1304c78da735b1503aa08e46b0abfdec", size = 56395, upload-time = "2025-04-28T19:26:16.947Z" },
]
[[package]]
name = "mypy-boto3-ec2"
version = "1.38.25"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d4/88/b9a99e010224b2ad9ea45b96f6689d9706841f2788e5ffe114cb0041e543/mypy_boto3_ec2-1.38.25.tar.gz", hash = "sha256:aed7d746c7c6af7e3f75424ad64829a7ce5b94dc871114a449c403ada22954cb", size = 400494, upload-time = "2025-05-28T19:42:21.197Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b4/22/f7caf22d014c7b0b62546a41b49030b73c162702870bfd1b1aaf48e2cc05/mypy_boto3_ec2-1.38.25-py3-none-any.whl", hash = "sha256:bad444d731669eab25fdcb7259901cb0db0fb26a4e1a79836a32aef6d674dbd0", size = 389882, upload-time = "2025-05-28T19:42:17.484Z" },
]
[[package]]
name = "mypy-boto3-lambda"
version = "1.38.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/8f/e3/0436071b28942788bdd22d6f91847654a7b1d167fb9d86c5779108e49ee9/mypy_boto3_lambda-1.38.0.tar.gz", hash = "sha256:ece7b3848c045e1be81c4f2b7482002c17ce7cb70de850661146103a8cb1a3fb", size = 41767, upload-time = "2025-04-22T21:27:54.666Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/17/09/602a39b39abd0d58d8b6bbee4c1552b64fadba2324676d7d45c3fa00fe7b/mypy_boto3_lambda-1.38.0-py3-none-any.whl", hash = "sha256:0dcb882826f61fd2751f6b98330b0e11085570654db85318aea018374ca88dc9", size = 48210, upload-time = "2025-04-22T21:27:52.034Z" },
]
[[package]]
name = "mypy-boto3-rds"
version = "1.38.20"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/97/b7/5ac46bee6617c8f955f6c28b8698a954f537812d84d655e3c887557421f0/mypy_boto3_rds-1.38.20.tar.gz", hash = "sha256:c6aa70c0cc5bc59959fec434206fbf8200386b583ff1f7e372154eaa41eb52e9", size = 85121, upload-time = "2025-05-20T23:30:09.631Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e7/ff/1a82134124ef41c040a40076c83b111d760f57007ec7b16649891fef0473/mypy_boto3_rds-1.38.20-py3-none-any.whl", hash = "sha256:9f600c24e687780fed1c8dc6d244b17dd0889f34705ec40c66df15e1caa420f4", size = 91368, upload-time = "2025-05-20T23:30:05.508Z" },
]
[[package]]
name = "mypy-boto3-s3"
version = "1.38.26"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/79/a5/366aec375b77cfe7820b7b3213318b147aefda6f12a035691541a5d557d1/mypy_boto3_s3-1.38.26.tar.gz", hash = "sha256:38a45dee5782d5c07ddea07ea50965c4d2ba7e77617c19f613b4c9f80f961b52", size = 73717, upload-time = "2025-05-29T19:43:03.468Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/3b/fa/251b651c18341c7491909994bd459b12ad05e13059d65bfa65d3afabdf8d/mypy_boto3_s3-1.38.26-py3-none-any.whl", hash = "sha256:1129d64be1aee863e04f0c92ac8d315578f13ccae64fa199b20ad0950d2b9616", size = 80321, upload-time = "2025-05-29T19:42:59.199Z" },
]
[[package]]
name = "mypy-boto3-sqs"
version = "1.38.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f0/a0/ef5c7bdb33af5d0a48029fed11401388fa68949c6c0f9b11b2e845f5fe0e/mypy_boto3_sqs-1.38.0.tar.gz", hash = "sha256:39aebc121a2fe20f962fd83b617fd916003605d6f6851fdf195337a0aa428fe1", size = 23541, upload-time = "2025-04-22T21:35:17.315Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a5/97/72fccc9aaa0e3c8f3f99b4edac580ede651808aefb47b0d2b52c18a3d16b/mypy_boto3_sqs-1.38.0-py3-none-any.whl", hash = "sha256:8e881c8492f6f51dcbe1cce9d9f05334f4b256b5843e227fa925e0f6e702b31d", size = 33669, upload-time = "2025-04-22T21:35:16.073Z" },
]
[[package]] [[package]]
name = "orjson" name = "orjson"
version = "3.10.16" version = "3.10.16"
@@ -938,6 +1039,24 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540, upload-time = "2024-11-24T20:12:19.698Z" }, { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540, upload-time = "2024-11-24T20:12:19.698Z" },
] ]
[[package]]
name = "types-awscrt"
version = "0.27.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/36/6c/583522cfb3c330e92e726af517a91c13247e555e021791a60f1b03c6ff16/types_awscrt-0.27.2.tar.gz", hash = "sha256:acd04f57119eb15626ab0ba9157fc24672421de56e7bd7b9f61681fedee44e91", size = 16304, upload-time = "2025-05-16T03:10:08.712Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/4c/82/1ee2e5c9d28deac086ab3a6ff07c8bc393ef013a083f546c623699881715/types_awscrt-0.27.2-py3-none-any.whl", hash = "sha256:49a045f25bbd5ad2865f314512afced933aed35ddbafc252e2268efa8a787e4e", size = 37761, upload-time = "2025-05-16T03:10:07.466Z" },
]
[[package]]
name = "types-s3transfer"
version = "0.13.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/42/c1/45038f259d6741c252801044e184fec4dbaeff939a58f6160d7c32bf4975/types_s3transfer-0.13.0.tar.gz", hash = "sha256:203dadcb9865c2f68fb44bc0440e1dc05b79197ba4a641c0976c26c9af75ef52", size = 14175, upload-time = "2025-05-28T02:16:07.614Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c8/5d/6bbe4bf6a79fb727945291aef88b5ecbdba857a603f1bbcf1a6be0d3f442/types_s3transfer-0.13.0-py3-none-any.whl", hash = "sha256:79c8375cbf48a64bff7654c02df1ec4b20d74f8c5672fc13e382f593ca5565b3", size = 19588, upload-time = "2025-05-28T02:16:06.709Z" },
]
[[package]] [[package]]
name = "typing-extensions" name = "typing-extensions"
version = "4.13.0" version = "4.13.0"

View File

@@ -20,8 +20,9 @@ class Status(Enum):
class Result(NamedTuple): class Result(NamedTuple):
status: Status status: Status
cause: Any input_record: Any
record: Any output: Any | None = None
cause: Any | None = None
class BatchProcessor(AbstractContextManager): class BatchProcessor(AbstractContextManager):
@@ -112,7 +113,12 @@ class BatchProcessor(AbstractContextManager):
result = self.handler(record) result = self.handler(record)
self.successes.append(record) self.successes.append(record)
return Result(Status.SUCCESS, result, record)
return Result(
status=Status.SUCCESS,
output=result,
input_record=record,
)
except Exception as exc: except Exception as exc:
exc_str = f'{type(exc).__name__}: {exc}' exc_str = f'{type(exc).__name__}: {exc}'
logger.debug(f'Record processing exception: {exc_str}') logger.debug(f'Record processing exception: {exc_str}')
@@ -120,4 +126,8 @@ class BatchProcessor(AbstractContextManager):
self.exceptions.append(exc) self.exceptions.append(exc)
self.failures.append(record) self.failures.append(record)
return Result(Status.FAIL, exc_str, record) return Result(
status=Status.FAIL,
input_record=record,
cause=exc,
)

View File

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

View File

@@ -18,11 +18,11 @@ def test_batch():
with processor(records=records, handler=record_handler) as p: with processor(records=records, handler=record_handler) as p:
processed_messages = p.process() processed_messages = p.process()
assert processed_messages == ( assert len(processed_messages) == 3
Result(Status.SUCCESS, True, True),
Result(Status.SUCCESS, True, True), fail_record = processed_messages[2]
Result(Status.FAIL, 'ValueError: Invalid record', False), assert isinstance(fail_record.cause, ValueError)
) assert str(fail_record.cause) == 'Invalid record'
assert processor.successes == [True, True] assert processor.successes == [True, True]
assert processor.failures == [False] assert processor.failures == [False]
@@ -30,9 +30,7 @@ def test_batch():
with processor(records=(False,), handler=record_handler): with processor(records=(False,), handler=record_handler):
processed_messages = processor.process() processed_messages = processor.process()
assert processed_messages == ( assert processed_messages[0].status == Status.FAIL
Result(Status.FAIL, 'ValueError: Invalid record', False),
)
assert processor.successes == [] assert processor.successes == []
assert processor.failures == [False] assert processor.failures == [False]
@@ -50,7 +48,7 @@ def test_batch_context():
processed_messages = processor.process() processed_messages = processor.process()
assert processed_messages == ( assert processed_messages == (
Result(Status.SUCCESS, 4, 2), Result(Status.SUCCESS, output=4, input_record=2),
Result(Status.SUCCESS, 6, 3), Result(Status.SUCCESS, output=6, input_record=3),
Result(Status.SUCCESS, 8, 4), Result(Status.SUCCESS, output=8, input_record=4),
) )

View File

@@ -1,3 +1,14 @@
import os
import boto3 import boto3
def get_dynamodb_client():
if os.getenv('AWS_LAMBDA_FUNCTION_NAME'):
return boto3.client('dynamodb')
return boto3.client('dynamodb', endpoint_url='http://localhost:8000')
dynamodb_client = get_dynamodb_client()
s3_client = boto3.client('s3') s3_client = boto3.client('s3')

View File

@@ -1 +1,4 @@
import os
USER_TABLE: str = os.getenv('USER_TABLE') # type: ignore
CHUNK_SIZE = 50 CHUNK_SIZE = 50

View File

@@ -1,5 +1,6 @@
import csv import csv
from io import StringIO from io import StringIO
from typing import TYPE_CHECKING
from aws_lambda_powertools.utilities.data_classes import ( from aws_lambda_powertools.utilities.data_classes import (
EventBridgeEvent, EventBridgeEvent,
@@ -9,6 +10,11 @@ from aws_lambda_powertools.utilities.typing import LambdaContext
from boto3clients import s3_client from boto3clients import s3_client
if TYPE_CHECKING:
from mypy_boto3_s3.client import S3Client
else:
S3Client = object
transport_params = {'client': s3_client} transport_params = {'client': s3_client}
@@ -36,7 +42,7 @@ def _get_s3_object_range(
*, *,
start_byte: int, start_byte: int,
end_byte: int, end_byte: int,
s3_client, s3_client: S3Client,
) -> StringIO: ) -> StringIO:
bucket, key = s3_uri.replace('s3://', '').split('/', 1) bucket, key = s3_uri.replace('s3://', '').split('/', 1)

View File

@@ -1,12 +1,17 @@
import urllib.parse as urllib_parse import urllib.parse as urllib_parse
from email.utils import parseaddr from email.utils import parseaddr
from typing import Any, Iterator
from aws_lambda_powertools import Logger from aws_lambda_powertools import Logger
from aws_lambda_powertools.utilities.data_classes import SESEvent, event_source from aws_lambda_powertools.utilities.data_classes import SESEvent, event_source
from aws_lambda_powertools.utilities.typing import LambdaContext from aws_lambda_powertools.utilities.typing import LambdaContext
from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair, SortKey
from boto3clients import dynamodb_client
from config import USER_TABLE
from ses_utils import get_header_value
logger = Logger(__name__) logger = Logger(__name__)
user_layer = DynamoDBPersistenceLayer(USER_TABLE, dynamodb_client)
@logger.inject_lambda_context @logger.inject_lambda_context
@@ -15,31 +20,21 @@ def lambda_handler(event: SESEvent, context: LambdaContext) -> dict:
ses = event.record.ses ses = event.record.ses
to = urllib_parse.unquote(ses.receipt.recipients[0]).lower() to = urllib_parse.unquote(ses.receipt.recipients[0]).lower()
name, email_from = parseaddr(get_header_value(ses.mail.headers, 'from')) name, email_from = parseaddr(get_header_value(ses.mail.headers, 'from'))
subject = get_header_value(
ses.mail.headers, org_id = user_layer.collection.get_item(
'subject', KeyPair('email', SortKey(to, path_spec='user_id')),
default='', raise_on_error=False,
raise_on_missing=False, default={},
) )
if email_from == 'sergio@somosbeta.com.br': if not org_id:
return {'disposition': 'CONTINUE'}
return {'disposition': 'STOP_RULE_SET'} return {'disposition': 'STOP_RULE_SET'}
print(
{
'id': f'mailbox#{org_id}',
'sk': ses.mail.message_id,
}
)
def get_header_value( return {'disposition': 'CONTINUE'}
headers: Iterator,
header_name: str,
*,
default: Any = None,
raise_on_missing: bool = True,
) -> str:
for header in headers:
if header.name.lower() == header_name:
return header.value
if raise_on_missing:
raise ValueError(f'{header_name} not found.')
return default

View File

@@ -25,8 +25,14 @@ Ignore all other fields.
""" """
csv_content = """ csv_content = """
Sérgio Rafael de Siqueira,10,07879819908,osergiosiqueria@gmail.com,cipa ,RICARDO GALLES BONET,ricardo.bonet@fanucamerica.com,424.430.528-93,NR-10 (RECICLAGEM)
Tiago Maciel,12,086.790.049-01,tiago@somosbeta.com.br,nr 10 ,RULIO SIEFERT SERA,rulio.sera@fanucamerica.com,063.916.859-08,NR-10 (RECICLAGEM)
,MACIEL FERREIRA BOMFIM,maciel.bomfim@fanucamerica.com,334.547.088-85,NR-10 (RECICLAGEM)
,JAIME EDUARDO GALVEZ AVILES,jaime.galvez@fanucamerica.com,280.238.818-50,NR-12
,JAIME EDUARDO GALVEZ AVILES,jaime.galvez@fanucamerica.com,280.238.818-50,NR-35 (RECICLAGEM)
,HIGOR MACHADO SILVA,higor.silva@fanucamerica.com,419.879.878-88,NR-12
,LÁZARO SOUZA DIAS,lazaro.dias@fanucamerica.com,067.179.825-19,NR-12
,JOÃO PEDRO AGUIAR GALASSO,joao.pedro@fanucamerica.com,570.403.588-40,NR-12
""" """
prompt = f""" prompt = f"""

View File

@@ -8,6 +8,8 @@ dependencies = ["layercake"]
[dependency-groups] [dependency-groups]
dev = [ dev = [
"boto3-stubs[essential]>=1.38.26",
"jsonlines>=4.0.0",
"pytest>=8.3.4", "pytest>=8.3.4",
"pytest-cov>=6.0.0", "pytest-cov>=6.0.0",
"ruff>=0.9.1", "ruff>=0.9.1",

View File

@@ -58,6 +58,9 @@ Resources:
Handler: events.email_receiving.lambda_handler Handler: events.email_receiving.lambda_handler
LoggingConfig: LoggingConfig:
LogGroup: !Ref EventLog LogGroup: !Ref EventLog
Policies:
- DynamoDBCrudPolicy:
TableName: !Ref UserTable
LambdaInvokePermission: LambdaInvokePermission:
Type: AWS::Lambda::Permission Type: AWS::Lambda::Permission
@@ -72,7 +75,7 @@ Resources:
Properties: Properties:
Bucket: !Ref BucketName Bucket: !Ref BucketName
PolicyDocument: PolicyDocument:
Version: "2012-10-17" Version: 2012-10-17
Statement: Statement:
- Effect: Allow - Effect: Allow
Principal: Principal:

View File

@@ -1,7 +1,21 @@
import os
from dataclasses import dataclass from dataclasses import dataclass
import jsonlines
import pytest import pytest
PYTEST_TABLE_NAME = 'pytest'
PK = 'id'
SK = 'sk'
# https://docs.pytest.org/en/7.1.x/reference/reference.html#pytest.hookspec.pytest_configure
def pytest_configure():
os.environ['TZ'] = 'America/Sao_Paulo'
os.environ['DYNAMODB_PARTITION_KEY'] = PK
os.environ['DYNAMODB_SORT_KEY'] = SK
os.environ['USER_TABLE'] = PYTEST_TABLE_NAME
@dataclass @dataclass
class LambdaContext: class LambdaContext:
@@ -14,3 +28,42 @@ class LambdaContext:
@pytest.fixture @pytest.fixture
def lambda_context() -> LambdaContext: def lambda_context() -> LambdaContext:
return LambdaContext() return LambdaContext()
@pytest.fixture
def dynamodb_client():
from boto3clients import dynamodb_client as client
client.create_table(
AttributeDefinitions=[
{'AttributeName': PK, 'AttributeType': 'S'},
{'AttributeName': SK, 'AttributeType': 'S'},
],
TableName=PYTEST_TABLE_NAME,
KeySchema=[
{'AttributeName': PK, 'KeyType': 'HASH'},
{'AttributeName': SK, 'KeyType': 'RANGE'},
],
ProvisionedThroughput={
'ReadCapacityUnits': 123,
'WriteCapacityUnits': 123,
},
)
yield client
client.delete_table(TableName=PYTEST_TABLE_NAME)
@pytest.fixture()
def dynamodb_persistence_layer(dynamodb_client):
from layercake.dynamodb import DynamoDBPersistenceLayer
return DynamoDBPersistenceLayer(PYTEST_TABLE_NAME, dynamodb_client)
@pytest.fixture()
def dynamodb_seeds(dynamodb_client):
with jsonlines.open('tests/seeds.jsonl') as lines:
for line in lines:
dynamodb_client.put_item(TableName=PYTEST_TABLE_NAME, Item=line)

View File

@@ -13,7 +13,7 @@ event = {
'source': 'sergio@somosbeta.com.br', 'source': 'sergio@somosbeta.com.br',
'messageId': '2994higq3tr7efijr3lj65etntffapgg1q7hea81', 'messageId': '2994higq3tr7efijr3lj65etntffapgg1q7hea81',
'destination': [ 'destination': [
'org+35980592000130@users.noreply.saladeaula.digital' 'org+15608435000190@users.noreply.saladeaula.digital'
], ],
'headersTruncated': False, 'headersTruncated': False,
'headers': [ 'headers': [
@@ -93,7 +93,7 @@ event = {
{'name': 'Subject', 'value': 'Re: test'}, {'name': 'Subject', 'value': 'Re: test'},
{ {
'name': 'To', 'name': 'To',
'value': 'org+35980592000130@users.noreply.saladeaula.digital', 'value': 'org+15608435000190@users.noreply.saladeaula.digital',
}, },
{ {
'name': 'Content-Type', 'name': 'Content-Type',
@@ -104,7 +104,7 @@ event = {
'returnPath': 'sergio@somosbeta.com.br', 'returnPath': 'sergio@somosbeta.com.br',
'from': ['"Sérgio Rafael Siqueira" <sergio@somosbeta.com.br>'], 'from': ['"Sérgio Rafael Siqueira" <sergio@somosbeta.com.br>'],
'date': 'Thu, 29 May 2025 12:50:26 -0300', 'date': 'Thu, 29 May 2025 12:50:26 -0300',
'to': ['org+35980592000130@users.noreply.saladeaula.digital'], 'to': ['org+15608435000190@users.noreply.saladeaula.digital'],
'messageId': '<CAMThe4=yMRJg4YOcACYAR509N1RyWyQgAghyVmr=NuSJnbondg@mail.gmail.com>', 'messageId': '<CAMThe4=yMRJg4YOcACYAR509N1RyWyQgAghyVmr=NuSJnbondg@mail.gmail.com>',
'subject': 'Re: test', 'subject': 'Re: test',
}, },
@@ -113,7 +113,7 @@ event = {
'timestamp': '2025-05-29T15:50:41.604Z', 'timestamp': '2025-05-29T15:50:41.604Z',
'processingTimeMillis': 1105, 'processingTimeMillis': 1105,
'recipients': [ 'recipients': [
'org+35980592000130@users.noreply.saladeaula.digital' 'org+15608435000190@users.noreply.saladeaula.digital'
], ],
'spamVerdict': {'status': 'PASS'}, 'spamVerdict': {'status': 'PASS'},
'virusVerdict': {'status': 'PASS'}, 'virusVerdict': {'status': 'PASS'},
@@ -132,5 +132,5 @@ event = {
} }
def test_email_receiving(lambda_context: LambdaContext): def test_email_receiving(dynamodb_seeds, lambda_context: LambdaContext):
assert app.lambda_handler(event, lambda_context) == {'disposition': 'CONTINUE'} assert app.lambda_handler(event, lambda_context) == {'disposition': 'CONTINUE'}

133
user-management/uv.lock generated
View File

@@ -103,6 +103,30 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/ce/89/634155fb209f50fd98da1cb11480bcdf6ed8d8ab68800d91cdb2bf59a8af/boto3-1.38.17-py3-none-any.whl", hash = "sha256:9b56c98fe7acb6559c24dacd838989878c60f3df2fb8ca5f311128419fd9f953", size = 139937, upload-time = "2025-05-15T19:35:14.663Z" }, { url = "https://files.pythonhosted.org/packages/ce/89/634155fb209f50fd98da1cb11480bcdf6ed8d8ab68800d91cdb2bf59a8af/boto3-1.38.17-py3-none-any.whl", hash = "sha256:9b56c98fe7acb6559c24dacd838989878c60f3df2fb8ca5f311128419fd9f953", size = 139937, upload-time = "2025-05-15T19:35:14.663Z" },
] ]
[[package]]
name = "boto3-stubs"
version = "1.38.26"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "botocore-stubs" },
{ name = "types-s3transfer" },
]
sdist = { url = "https://files.pythonhosted.org/packages/1e/89/9e2658210ac11459405ed8c82f47be533a5d16ae3b8203c90564b7a738a0/boto3_stubs-1.38.26.tar.gz", hash = "sha256:492e59e42323de43018ffa6d00d3bb2b93d1fead042e76c6a68fd0a0c0fe3236", size = 99065, upload-time = "2025-05-29T19:47:49.383Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/48/51/881ac3c7ebeefdfe0e712b1f4d815261707a492187ea301506168cf6fc20/boto3_stubs-1.38.26-py3-none-any.whl", hash = "sha256:3022b2a8f6925c60c9ce68c5e090ff9fd2bad0c918300395a1c242681a67c11c", size = 68669, upload-time = "2025-05-29T19:47:42.352Z" },
]
[package.optional-dependencies]
essential = [
{ name = "mypy-boto3-cloudformation" },
{ name = "mypy-boto3-dynamodb" },
{ name = "mypy-boto3-ec2" },
{ name = "mypy-boto3-lambda" },
{ name = "mypy-boto3-rds" },
{ name = "mypy-boto3-s3" },
{ name = "mypy-boto3-sqs" },
]
[[package]] [[package]]
name = "botocore" name = "botocore"
version = "1.38.17" version = "1.38.17"
@@ -117,6 +141,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/01/fc/9c08db2e89055999e996fee3537cbbfb4ed8ebf0d4ab1b1045e1819b76d8/botocore-1.38.17-py3-none-any.whl", hash = "sha256:ec75cf02fbd3dbec18187085ce387761eab16afdccfd0774fd168db3689c6cb6", size = 13564514, upload-time = "2025-05-15T19:35:00.231Z" }, { url = "https://files.pythonhosted.org/packages/01/fc/9c08db2e89055999e996fee3537cbbfb4ed8ebf0d4ab1b1045e1819b76d8/botocore-1.38.17-py3-none-any.whl", hash = "sha256:ec75cf02fbd3dbec18187085ce387761eab16afdccfd0774fd168db3689c6cb6", size = 13564514, upload-time = "2025-05-15T19:35:00.231Z" },
] ]
[[package]]
name = "botocore-stubs"
version = "1.38.26"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "types-awscrt" },
]
sdist = { url = "https://files.pythonhosted.org/packages/08/f2/fd2f5a8ef00bbcfe00c12b8c49e247510266929dff5578b6fec360967a21/botocore_stubs-1.38.26.tar.gz", hash = "sha256:3bbf7662fc97e28a50dc959752619cf57029194987268b4dc13df4e54767204c", size = 42315, upload-time = "2025-05-29T20:18:25.22Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/28/4a/f99ef1ff37620c0c23aa67e3d9de5fce0f98b38fd26e6d30438ee440c0fc/botocore_stubs-1.38.26-py3-none-any.whl", hash = "sha256:c86ac7d2c7e24ea50a866a9686a293dfe8b40281cc3465d79e2e0e48d35ad93b", size = 65628, upload-time = "2025-05-29T20:18:23.125Z" },
]
[[package]] [[package]]
name = "brotli" name = "brotli"
version = "1.1.0" version = "1.1.0"
@@ -469,6 +505,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/31/b4/b9b800c45527aadd64d5b442f9b932b00648617eb5d63d2c7a6587b7cafc/jmespath-1.0.1-py3-none-any.whl", hash = "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980", size = 20256, upload-time = "2022-06-17T18:00:10.251Z" }, { url = "https://files.pythonhosted.org/packages/31/b4/b9b800c45527aadd64d5b442f9b932b00648617eb5d63d2c7a6587b7cafc/jmespath-1.0.1-py3-none-any.whl", hash = "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980", size = 20256, upload-time = "2022-06-17T18:00:10.251Z" },
] ]
[[package]]
name = "jsonlines"
version = "4.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "attrs" },
]
sdist = { url = "https://files.pythonhosted.org/packages/35/87/bcda8e46c88d0e34cad2f09ee2d0c7f5957bccdb9791b0b934ec84d84be4/jsonlines-4.0.0.tar.gz", hash = "sha256:0c6d2c09117550c089995247f605ae4cf77dd1533041d366351f6f298822ea74", size = 11359, upload-time = "2023-09-01T12:34:44.187Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f8/62/d9ba6323b9202dd2fe166beab8a86d29465c41a0288cbe229fac60c1ab8d/jsonlines-4.0.0-py3-none-any.whl", hash = "sha256:185b334ff2ca5a91362993f42e83588a360cf95ce4b71a73548502bda52a7c55", size = 8701, upload-time = "2023-09-01T12:34:42.563Z" },
]
[[package]] [[package]]
name = "jsonpath-ng" name = "jsonpath-ng"
version = "1.7.0" version = "1.7.0"
@@ -545,6 +593,69 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/bf/3e/a3ec8d44b35e495444cac8ce3573b33adf19a9b6d70f2a51e4a971f17c81/meilisearch-0.34.1-py3-none-any.whl", hash = "sha256:43efa4521ce7dc3b065d404267ad5b3acb825602e6219b8b5356650306686cd4", size = 24918, upload-time = "2025-04-04T13:45:06.869Z" }, { url = "https://files.pythonhosted.org/packages/bf/3e/a3ec8d44b35e495444cac8ce3573b33adf19a9b6d70f2a51e4a971f17c81/meilisearch-0.34.1-py3-none-any.whl", hash = "sha256:43efa4521ce7dc3b065d404267ad5b3acb825602e6219b8b5356650306686cd4", size = 24918, upload-time = "2025-04-04T13:45:06.869Z" },
] ]
[[package]]
name = "mypy-boto3-cloudformation"
version = "1.38.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/40/d5/35b9301c8b2fb870e58401d13fec36de2c83f2ddef48398b8c89c9a58995/mypy_boto3_cloudformation-1.38.0.tar.gz", hash = "sha256:563399166c07e91e0695fb1e58103a248b2bee0db5e2c3f07155776dd6311805", size = 57702, upload-time = "2025-04-22T21:19:31.221Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/1e/2f/d20ad6e0113f982ea255fcb4ed94f70d0111757d7d03bfacebc2d9f60ba4/mypy_boto3_cloudformation-1.38.0-py3-none-any.whl", hash = "sha256:a1411aa5875b737492aaac5f7e8ce450f034c18f972eb608a9eba6fe35837f6a", size = 69607, upload-time = "2025-04-22T21:19:29.235Z" },
]
[[package]]
name = "mypy-boto3-dynamodb"
version = "1.38.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/6b/7f/72b68d275a80a42675c36249b80dd79ec5c7d9bd1f5cc93cdb572f866722/mypy_boto3_dynamodb-1.38.4.tar.gz", hash = "sha256:5cf3787631e312b3d75f89a6cbbbd4ad786a76f5d565af023febf03fbf23c0b5", size = 47461, upload-time = "2025-04-28T19:26:22.728Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/6a/35/3d0ceabb0a9f3765f509cb9dce6ddfa939114682b1acc442f52a755e9bc8/mypy_boto3_dynamodb-1.38.4-py3-none-any.whl", hash = "sha256:6b29d89c649eeb1e894118bee002cb8b1304c78da735b1503aa08e46b0abfdec", size = 56395, upload-time = "2025-04-28T19:26:16.947Z" },
]
[[package]]
name = "mypy-boto3-ec2"
version = "1.38.25"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d4/88/b9a99e010224b2ad9ea45b96f6689d9706841f2788e5ffe114cb0041e543/mypy_boto3_ec2-1.38.25.tar.gz", hash = "sha256:aed7d746c7c6af7e3f75424ad64829a7ce5b94dc871114a449c403ada22954cb", size = 400494, upload-time = "2025-05-28T19:42:21.197Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b4/22/f7caf22d014c7b0b62546a41b49030b73c162702870bfd1b1aaf48e2cc05/mypy_boto3_ec2-1.38.25-py3-none-any.whl", hash = "sha256:bad444d731669eab25fdcb7259901cb0db0fb26a4e1a79836a32aef6d674dbd0", size = 389882, upload-time = "2025-05-28T19:42:17.484Z" },
]
[[package]]
name = "mypy-boto3-lambda"
version = "1.38.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/8f/e3/0436071b28942788bdd22d6f91847654a7b1d167fb9d86c5779108e49ee9/mypy_boto3_lambda-1.38.0.tar.gz", hash = "sha256:ece7b3848c045e1be81c4f2b7482002c17ce7cb70de850661146103a8cb1a3fb", size = 41767, upload-time = "2025-04-22T21:27:54.666Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/17/09/602a39b39abd0d58d8b6bbee4c1552b64fadba2324676d7d45c3fa00fe7b/mypy_boto3_lambda-1.38.0-py3-none-any.whl", hash = "sha256:0dcb882826f61fd2751f6b98330b0e11085570654db85318aea018374ca88dc9", size = 48210, upload-time = "2025-04-22T21:27:52.034Z" },
]
[[package]]
name = "mypy-boto3-rds"
version = "1.38.20"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/97/b7/5ac46bee6617c8f955f6c28b8698a954f537812d84d655e3c887557421f0/mypy_boto3_rds-1.38.20.tar.gz", hash = "sha256:c6aa70c0cc5bc59959fec434206fbf8200386b583ff1f7e372154eaa41eb52e9", size = 85121, upload-time = "2025-05-20T23:30:09.631Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e7/ff/1a82134124ef41c040a40076c83b111d760f57007ec7b16649891fef0473/mypy_boto3_rds-1.38.20-py3-none-any.whl", hash = "sha256:9f600c24e687780fed1c8dc6d244b17dd0889f34705ec40c66df15e1caa420f4", size = 91368, upload-time = "2025-05-20T23:30:05.508Z" },
]
[[package]]
name = "mypy-boto3-s3"
version = "1.38.26"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/79/a5/366aec375b77cfe7820b7b3213318b147aefda6f12a035691541a5d557d1/mypy_boto3_s3-1.38.26.tar.gz", hash = "sha256:38a45dee5782d5c07ddea07ea50965c4d2ba7e77617c19f613b4c9f80f961b52", size = 73717, upload-time = "2025-05-29T19:43:03.468Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/3b/fa/251b651c18341c7491909994bd459b12ad05e13059d65bfa65d3afabdf8d/mypy_boto3_s3-1.38.26-py3-none-any.whl", hash = "sha256:1129d64be1aee863e04f0c92ac8d315578f13ccae64fa199b20ad0950d2b9616", size = 80321, upload-time = "2025-05-29T19:42:59.199Z" },
]
[[package]]
name = "mypy-boto3-sqs"
version = "1.38.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f0/a0/ef5c7bdb33af5d0a48029fed11401388fa68949c6c0f9b11b2e845f5fe0e/mypy_boto3_sqs-1.38.0.tar.gz", hash = "sha256:39aebc121a2fe20f962fd83b617fd916003605d6f6851fdf195337a0aa428fe1", size = 23541, upload-time = "2025-04-22T21:35:17.315Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a5/97/72fccc9aaa0e3c8f3f99b4edac580ede651808aefb47b0d2b52c18a3d16b/mypy_boto3_sqs-1.38.0-py3-none-any.whl", hash = "sha256:8e881c8492f6f51dcbe1cce9d9f05334f4b256b5843e227fa925e0f6e702b31d", size = 33669, upload-time = "2025-04-22T21:35:16.073Z" },
]
[[package]] [[package]]
name = "orjson" name = "orjson"
version = "3.10.18" version = "3.10.18"
@@ -896,6 +1007,24 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/5c/de/27c57899297163a4a84104d5cec0af3b1ac5faf62f44667e506373c6b8ce/tinyhtml5-2.0.0-py3-none-any.whl", hash = "sha256:13683277c5b176d070f82d099d977194b7a1e26815b016114f581a74bbfbf47e", size = 39793, upload-time = "2024-10-29T15:37:11.743Z" }, { url = "https://files.pythonhosted.org/packages/5c/de/27c57899297163a4a84104d5cec0af3b1ac5faf62f44667e506373c6b8ce/tinyhtml5-2.0.0-py3-none-any.whl", hash = "sha256:13683277c5b176d070f82d099d977194b7a1e26815b016114f581a74bbfbf47e", size = 39793, upload-time = "2024-10-29T15:37:11.743Z" },
] ]
[[package]]
name = "types-awscrt"
version = "0.27.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/36/6c/583522cfb3c330e92e726af517a91c13247e555e021791a60f1b03c6ff16/types_awscrt-0.27.2.tar.gz", hash = "sha256:acd04f57119eb15626ab0ba9157fc24672421de56e7bd7b9f61681fedee44e91", size = 16304, upload-time = "2025-05-16T03:10:08.712Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/4c/82/1ee2e5c9d28deac086ab3a6ff07c8bc393ef013a083f546c623699881715/types_awscrt-0.27.2-py3-none-any.whl", hash = "sha256:49a045f25bbd5ad2865f314512afced933aed35ddbafc252e2268efa8a787e4e", size = 37761, upload-time = "2025-05-16T03:10:07.466Z" },
]
[[package]]
name = "types-s3transfer"
version = "0.13.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/42/c1/45038f259d6741c252801044e184fec4dbaeff939a58f6160d7c32bf4975/types_s3transfer-0.13.0.tar.gz", hash = "sha256:203dadcb9865c2f68fb44bc0440e1dc05b79197ba4a641c0976c26c9af75ef52", size = 14175, upload-time = "2025-05-28T02:16:07.614Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c8/5d/6bbe4bf6a79fb727945291aef88b5ecbdba857a603f1bbcf1a6be0d3f442/types_s3transfer-0.13.0-py3-none-any.whl", hash = "sha256:79c8375cbf48a64bff7654c02df1ec4b20d74f8c5672fc13e382f593ca5565b3", size = 19588, upload-time = "2025-05-28T02:16:06.709Z" },
]
[[package]] [[package]]
name = "typing-extensions" name = "typing-extensions"
version = "4.13.2" version = "4.13.2"
@@ -936,6 +1065,8 @@ dependencies = [
[package.dev-dependencies] [package.dev-dependencies]
dev = [ dev = [
{ name = "boto3-stubs", extra = ["essential"] },
{ name = "jsonlines" },
{ name = "pytest" }, { name = "pytest" },
{ name = "pytest-cov" }, { name = "pytest-cov" },
{ name = "ruff" }, { name = "ruff" },
@@ -946,6 +1077,8 @@ requires-dist = [{ name = "layercake", directory = "../layercake" }]
[package.metadata.requires-dev] [package.metadata.requires-dev]
dev = [ dev = [
{ name = "boto3-stubs", extras = ["essential"], specifier = ">=1.38.26" },
{ name = "jsonlines", specifier = ">=4.0.0" },
{ name = "pytest", specifier = ">=8.3.4" }, { name = "pytest", specifier = ">=8.3.4" },
{ name = "pytest-cov", specifier = ">=6.0.0" }, { name = "pytest-cov", specifier = ">=6.0.0" },
{ name = "ruff", specifier = ">=0.9.1" }, { name = "ruff", specifier = ">=0.9.1" },