From b048febbd5133edcf122860ecc9af2d38ce046b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9rgio=20Rafael=20Siqueira?= Date: Fri, 30 May 2025 15:32:31 -0300 Subject: [PATCH] update batch --- http-api/app/auth.py | 2 +- http-api/app/boto3clients.py | 6 +- http-api/app/models.py | 2 +- http-api/app/routes/enrollments/cancel.py | 5 +- http-api/app/routes/enrollments/enroll.py | 41 ++++-- http-api/app/rules/enrollment.py | 6 + http-api/app/rules/user.py | 1 - http-api/pyproject.toml | 1 + http-api/tests/routes/test_enrollments.py | 20 ++- http-api/tests/seeds.jsonl | 1 + http-api/uv.lock | 119 ++++++++++++++++ layercake/layercake/batch.py | 18 ++- layercake/pyproject.toml | 2 +- layercake/tests/test_batch.py | 20 ++- user-management/app/boto3clients.py | 11 ++ user-management/app/config.py | 3 + .../app/events/batch/read_csv_chunk.py | 8 +- user-management/app/events/email_receiving.py | 45 +++--- user-management/cf.py | 10 +- user-management/pyproject.toml | 2 + user-management/template.yaml | 5 +- user-management/tests/conftest.py | 53 +++++++ .../tests/events/test_email_receiving.py | 10 +- user-management/uv.lock | 133 ++++++++++++++++++ 24 files changed, 455 insertions(+), 69 deletions(-) diff --git a/http-api/app/auth.py b/http-api/app/auth.py index 7bb4692..7c242f7 100644 --- a/http-api/app/auth.py +++ b/http-api/app/auth.py @@ -24,6 +24,7 @@ Example """ from dataclasses import asdict, dataclass +from enum import Enum from typing import Any from aws_lambda_powertools import Logger, Tracer @@ -33,7 +34,6 @@ from aws_lambda_powertools.utilities.data_classes.api_gateway_authorizer_event i APIGatewayAuthorizerResponseV2, ) from aws_lambda_powertools.utilities.typing import LambdaContext -from botocore.endpoint_provider import Enum from layercake.dynamodb import DynamoDBCollection, DynamoDBPersistenceLayer, KeyPair from layercake.funcs import pick diff --git a/http-api/app/boto3clients.py b/http-api/app/boto3clients.py index 7975d25..71a6ba3 100644 --- a/http-api/app/boto3clients.py +++ b/http-api/app/boto3clients.py @@ -4,12 +4,12 @@ import boto3 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') - 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') diff --git a/http-api/app/models.py b/http-api/app/models.py index bb0c615..408a7ae 100644 --- a/http-api/app/models.py +++ b/http-api/app/models.py @@ -40,7 +40,7 @@ class Course(BaseModel): class Enrollment(BaseModel): - id: UUID4 = Field(default_factory=uuid4) + id: UUID4 | str = Field(default_factory=uuid4) user: User course: Course progress: int = Field(default=0, ge=0, le=100) diff --git a/http-api/app/routes/enrollments/cancel.py b/http-api/app/routes/enrollments/cancel.py index 681bed1..c441f91 100644 --- a/http-api/app/routes/enrollments/cancel.py +++ b/http-api/app/routes/enrollments/cancel.py @@ -46,7 +46,10 @@ def cancel(id: str, payload: Cancel): set_status_as_canceled( id, lock_hash=payload.lock_hash, - author=user.model_dump(), # type: ignore + author={ + 'id': user.id, + 'name': user.name, + }, course=payload.course, # type: ignore vacancy_key=KeyPair.parse_obj(payload.vacancy), persistence_layer=enrollment_layer, diff --git a/http-api/app/routes/enrollments/enroll.py b/http-api/app/routes/enrollments/enroll.py index 00d9f1a..8b0605a 100644 --- a/http-api/app/routes/enrollments/enroll.py +++ b/http-api/app/routes/enrollments/enroll.py @@ -1,4 +1,5 @@ from datetime import datetime +from http import HTTPStatus from aws_lambda_powertools.event_handler.api_gateway import Router from layercake.batch import BatchProcessor @@ -8,13 +9,15 @@ from layercake.dynamodb import ( ) from pydantic import BaseModel +from api_gateway import JSONResponse from boto3clients import dynamodb_client from config import ( ENROLLMENT_TABLE, USER_TABLE, ) from middlewares import Tenant, TenantMiddleware -from models import Course, User +from models import Course, Enrollment, User +from rules.enrollment import enroll router = Router() @@ -28,6 +31,7 @@ processor = BatchProcessor() class Item(BaseModel): user: User course: Course + deduplication_window: dict = {} schedule_date: datetime | None = None @@ -49,16 +53,33 @@ def enroll_(payload: Payload): with processor(payload.items, handler, context): 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): tenant: Tenant = context['tenant'] - # enroll( - # enrollment=Enrollment(user=[]) - # tenant={ - # 'id': str(tenant.id), - # 'name': tenant.name, - # }, - # persistence_layer=enrollment_layer, - # ) + enrollment = Enrollment( + user=record.user, + course=record.course, + ) + + enroll( + enrollment=enrollment, + tenant={ + 'id': str(tenant.id), + 'name': tenant.name, + }, + deduplication_window=record.deduplication_window, # type: ignore + persistence_layer=enrollment_layer, + ) + + return enrollment diff --git a/http-api/app/rules/enrollment.py b/http-api/app/rules/enrollment.py index 69a886b..ed6655e 100644 --- a/http-api/app/rules/enrollment.py +++ b/http-api/app/rules/enrollment.py @@ -125,6 +125,11 @@ def enroll( ttl_expiration = ttl( start_dt=now_ + timedelta(days=course.access_period - offset_days) ) + + class DeduplicationConflictError(Exception): + def __init__(self, *args): + super().__init__('Enrollment already exists') + transact.put( item={ 'id': 'lock', @@ -134,6 +139,7 @@ def enroll( 'ttl': ttl_expiration, }, cond_expr='attribute_not_exists(sk)', + exc_cls=DeduplicationConflictError, ) transact.put( item={ diff --git a/http-api/app/rules/user.py b/http-api/app/rules/user.py index 7c96b86..4a5e530 100644 --- a/http-api/app/rules/user.py +++ b/http-api/app/rules/user.py @@ -4,7 +4,6 @@ from typing import TypedDict from aws_lambda_powertools.event_handler.exceptions import ( BadRequestError, ) -from botocore.tokens import timedelta from layercake.dateutils import now, ttl from layercake.dynamodb import ( ComposeKey, diff --git a/http-api/pyproject.toml b/http-api/pyproject.toml index 4b2ce70..9f847ae 100644 --- a/http-api/pyproject.toml +++ b/http-api/pyproject.toml @@ -8,6 +8,7 @@ dependencies = ["layercake"] [dependency-groups] dev = [ + "boto3-stubs[essential]>=1.38.26", "jsonlines>=4.0.0", "pytest>=8.3.4", "pytest-cov>=6.0.0", diff --git a/http-api/tests/routes/test_enrollments.py b/http-api/tests/routes/test_enrollments.py index 8a88f98..5db04c7 100644 --- a/http-api/tests/routes/test_enrollments.py +++ b/http-api/tests/routes/test_enrollments.py @@ -1,3 +1,4 @@ +import json from http import HTTPMethod, HTTPStatus from layercake.dynamodb import ( @@ -36,6 +37,21 @@ def test_enroll( 'id': '6d69a34a-cefd-40aa-a89b-dceb694c3e61', '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, ) - assert r['statusCode'] == HTTPStatus.OK - print(r) + # assert r['statusCode'] == HTTPStatus.OK + print(json.loads(r['body'])) def test_vacancies( diff --git a/http-api/tests/seeds.jsonl b/http-api/tests/seeds.jsonl index df105aa..54284fa 100644 --- a/http-api/tests/seeds.jsonl +++ b/http-api/tests/seeds.jsonl @@ -21,3 +21,4 @@ {"id": {"S": "email"}, "sk": {"S": "sergio@somosbeta.com.br"}} {"id": {"S": "cpf"}, "sk": {"S": "07879819908"}} {"id": {"S": "cpf"}, "sk": {"S": "08679004901"}} +{"id": {"S": "lock"}, "sk": {"S": "c2116a43f8f1aed659a10c83dab17ed3"}} \ No newline at end of file diff --git a/http-api/uv.lock b/http-api/uv.lock index 4319431..34bb53a 100644 --- a/http-api/uv.lock +++ b/http-api/uv.lock @@ -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" }, ] +[[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]] name = "botocore" 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" }, ] +[[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]] name = "brotli" version = "1.1.0" @@ -450,6 +486,7 @@ dependencies = [ [package.dev-dependencies] dev = [ + { name = "boto3-stubs", extra = ["essential"] }, { name = "jsonlines" }, { name = "pytest" }, { name = "pytest-cov" }, @@ -462,6 +499,7 @@ requires-dist = [{ name = "layercake", directory = "../layercake" }] [package.metadata.requires-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-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" }, ] +[[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]] name = "orjson" 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" }, ] +[[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]] name = "typing-extensions" version = "4.13.0" diff --git a/layercake/layercake/batch.py b/layercake/layercake/batch.py index 574276f..9ef90a0 100644 --- a/layercake/layercake/batch.py +++ b/layercake/layercake/batch.py @@ -20,8 +20,9 @@ class Status(Enum): class Result(NamedTuple): status: Status - cause: Any - record: Any + input_record: Any + output: Any | None = None + cause: Any | None = None class BatchProcessor(AbstractContextManager): @@ -112,7 +113,12 @@ class BatchProcessor(AbstractContextManager): result = self.handler(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: exc_str = f'{type(exc).__name__}: {exc}' logger.debug(f'Record processing exception: {exc_str}') @@ -120,4 +126,8 @@ class BatchProcessor(AbstractContextManager): self.exceptions.append(exc) self.failures.append(record) - return Result(Status.FAIL, exc_str, record) + return Result( + status=Status.FAIL, + input_record=record, + cause=exc, + ) diff --git a/layercake/pyproject.toml b/layercake/pyproject.toml index c244d0c..c593209 100644 --- a/layercake/pyproject.toml +++ b/layercake/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "layercake" -version = "0.6.2" +version = "0.6.3" description = "Packages shared dependencies to optimize deployment and ensure consistency across functions." readme = "README.md" authors = [ diff --git a/layercake/tests/test_batch.py b/layercake/tests/test_batch.py index afdbbca..318d3ad 100644 --- a/layercake/tests/test_batch.py +++ b/layercake/tests/test_batch.py @@ -18,11 +18,11 @@ def test_batch(): 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 len(processed_messages) == 3 + + fail_record = processed_messages[2] + assert isinstance(fail_record.cause, ValueError) + assert str(fail_record.cause) == 'Invalid record' assert processor.successes == [True, True] assert processor.failures == [False] @@ -30,9 +30,7 @@ def test_batch(): with processor(records=(False,), handler=record_handler): processed_messages = processor.process() - assert processed_messages == ( - Result(Status.FAIL, 'ValueError: Invalid record', False), - ) + assert processed_messages[0].status == Status.FAIL assert processor.successes == [] assert processor.failures == [False] @@ -50,7 +48,7 @@ def test_batch_context(): processed_messages = processor.process() assert processed_messages == ( - Result(Status.SUCCESS, 4, 2), - Result(Status.SUCCESS, 6, 3), - Result(Status.SUCCESS, 8, 4), + Result(Status.SUCCESS, output=4, input_record=2), + Result(Status.SUCCESS, output=6, input_record=3), + Result(Status.SUCCESS, output=8, input_record=4), ) diff --git a/user-management/app/boto3clients.py b/user-management/app/boto3clients.py index 3a55987..b8cf4f5 100644 --- a/user-management/app/boto3clients.py +++ b/user-management/app/boto3clients.py @@ -1,3 +1,14 @@ +import os + 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') diff --git a/user-management/app/config.py b/user-management/app/config.py index d6ebccc..f988eaa 100644 --- a/user-management/app/config.py +++ b/user-management/app/config.py @@ -1 +1,4 @@ +import os + +USER_TABLE: str = os.getenv('USER_TABLE') # type: ignore CHUNK_SIZE = 50 diff --git a/user-management/app/events/batch/read_csv_chunk.py b/user-management/app/events/batch/read_csv_chunk.py index ed93858..d7657f1 100644 --- a/user-management/app/events/batch/read_csv_chunk.py +++ b/user-management/app/events/batch/read_csv_chunk.py @@ -1,5 +1,6 @@ import csv from io import StringIO +from typing import TYPE_CHECKING from aws_lambda_powertools.utilities.data_classes import ( EventBridgeEvent, @@ -9,6 +10,11 @@ from aws_lambda_powertools.utilities.typing import LambdaContext from boto3clients import s3_client +if TYPE_CHECKING: + from mypy_boto3_s3.client import S3Client +else: + S3Client = object + transport_params = {'client': s3_client} @@ -36,7 +42,7 @@ def _get_s3_object_range( *, start_byte: int, end_byte: int, - s3_client, + s3_client: S3Client, ) -> StringIO: bucket, key = s3_uri.replace('s3://', '').split('/', 1) diff --git a/user-management/app/events/email_receiving.py b/user-management/app/events/email_receiving.py index 23dc9c7..ea6ebaf 100644 --- a/user-management/app/events/email_receiving.py +++ b/user-management/app/events/email_receiving.py @@ -1,12 +1,17 @@ import urllib.parse as urllib_parse from email.utils import parseaddr -from typing import Any, Iterator from aws_lambda_powertools import Logger from aws_lambda_powertools.utilities.data_classes import SESEvent, event_source 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__) +user_layer = DynamoDBPersistenceLayer(USER_TABLE, dynamodb_client) @logger.inject_lambda_context @@ -15,31 +20,21 @@ def lambda_handler(event: SESEvent, context: LambdaContext) -> dict: ses = event.record.ses to = urllib_parse.unquote(ses.receipt.recipients[0]).lower() name, email_from = parseaddr(get_header_value(ses.mail.headers, 'from')) - subject = get_header_value( - ses.mail.headers, - 'subject', - default='', - raise_on_missing=False, + + org_id = user_layer.collection.get_item( + KeyPair('email', SortKey(to, path_spec='user_id')), + raise_on_error=False, + default={}, ) - if email_from == 'sergio@somosbeta.com.br': - return {'disposition': 'CONTINUE'} + if not org_id: + 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( - 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 + return {'disposition': 'CONTINUE'} diff --git a/user-management/cf.py b/user-management/cf.py index 2bae2e4..d2d8e64 100644 --- a/user-management/cf.py +++ b/user-management/cf.py @@ -25,8 +25,14 @@ Ignore all other fields. """ csv_content = """ -Sérgio Rafael de Siqueira,10,07879819908,osergiosiqueria@gmail.com,cipa -Tiago Maciel,12,086.790.049-01,tiago@somosbeta.com.br,nr 10 +,RICARDO GALLES BONET,ricardo.bonet@fanucamerica.com,424.430.528-93,NR-10 (RECICLAGEM) +,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""" diff --git a/user-management/pyproject.toml b/user-management/pyproject.toml index ea64db6..f11de82 100644 --- a/user-management/pyproject.toml +++ b/user-management/pyproject.toml @@ -8,6 +8,8 @@ dependencies = ["layercake"] [dependency-groups] dev = [ + "boto3-stubs[essential]>=1.38.26", + "jsonlines>=4.0.0", "pytest>=8.3.4", "pytest-cov>=6.0.0", "ruff>=0.9.1", diff --git a/user-management/template.yaml b/user-management/template.yaml index 7f741db..834f89b 100644 --- a/user-management/template.yaml +++ b/user-management/template.yaml @@ -58,6 +58,9 @@ Resources: Handler: events.email_receiving.lambda_handler LoggingConfig: LogGroup: !Ref EventLog + Policies: + - DynamoDBCrudPolicy: + TableName: !Ref UserTable LambdaInvokePermission: Type: AWS::Lambda::Permission @@ -72,7 +75,7 @@ Resources: Properties: Bucket: !Ref BucketName PolicyDocument: - Version: "2012-10-17" + Version: 2012-10-17 Statement: - Effect: Allow Principal: diff --git a/user-management/tests/conftest.py b/user-management/tests/conftest.py index ace6b2e..f02c5b5 100644 --- a/user-management/tests/conftest.py +++ b/user-management/tests/conftest.py @@ -1,7 +1,21 @@ +import os from dataclasses import dataclass +import jsonlines 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 class LambdaContext: @@ -14,3 +28,42 @@ class LambdaContext: @pytest.fixture def lambda_context() -> 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) diff --git a/user-management/tests/events/test_email_receiving.py b/user-management/tests/events/test_email_receiving.py index 854d352..2b45b0e 100644 --- a/user-management/tests/events/test_email_receiving.py +++ b/user-management/tests/events/test_email_receiving.py @@ -13,7 +13,7 @@ event = { 'source': 'sergio@somosbeta.com.br', 'messageId': '2994higq3tr7efijr3lj65etntffapgg1q7hea81', 'destination': [ - 'org+35980592000130@users.noreply.saladeaula.digital' + 'org+15608435000190@users.noreply.saladeaula.digital' ], 'headersTruncated': False, 'headers': [ @@ -93,7 +93,7 @@ event = { {'name': 'Subject', 'value': 'Re: test'}, { 'name': 'To', - 'value': 'org+35980592000130@users.noreply.saladeaula.digital', + 'value': 'org+15608435000190@users.noreply.saladeaula.digital', }, { 'name': 'Content-Type', @@ -104,7 +104,7 @@ event = { 'returnPath': 'sergio@somosbeta.com.br', 'from': ['"Sérgio Rafael Siqueira" '], 'date': 'Thu, 29 May 2025 12:50:26 -0300', - 'to': ['org+35980592000130@users.noreply.saladeaula.digital'], + 'to': ['org+15608435000190@users.noreply.saladeaula.digital'], 'messageId': '', 'subject': 'Re: test', }, @@ -113,7 +113,7 @@ event = { 'timestamp': '2025-05-29T15:50:41.604Z', 'processingTimeMillis': 1105, 'recipients': [ - 'org+35980592000130@users.noreply.saladeaula.digital' + 'org+15608435000190@users.noreply.saladeaula.digital' ], 'spamVerdict': {'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'} diff --git a/user-management/uv.lock b/user-management/uv.lock index 121dbd1..bf5f797 100644 --- a/user-management/uv.lock +++ b/user-management/uv.lock @@ -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" }, ] +[[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]] name = "botocore" 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" }, ] +[[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]] name = "brotli" 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" }, ] +[[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]] name = "jsonpath-ng" 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" }, ] +[[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]] name = "orjson" 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" }, ] +[[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]] name = "typing-extensions" version = "4.13.2" @@ -936,6 +1065,8 @@ dependencies = [ [package.dev-dependencies] dev = [ + { name = "boto3-stubs", extra = ["essential"] }, + { name = "jsonlines" }, { name = "pytest" }, { name = "pytest-cov" }, { name = "ruff" }, @@ -946,6 +1077,8 @@ requires-dist = [{ name = "layercake", directory = "../layercake" }] [package.metadata.requires-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-cov", specifier = ">=6.0.0" }, { name = "ruff", specifier = ">=0.9.1" },