diff --git a/api.saladeaula.digital/app/routes/enrollments/cancel.py b/api.saladeaula.digital/app/routes/enrollments/cancel.py new file mode 100644 index 0000000..f1d7ee5 --- /dev/null +++ b/api.saladeaula.digital/app/routes/enrollments/cancel.py @@ -0,0 +1,14 @@ +from aws_lambda_powertools import Logger +from aws_lambda_powertools.event_handler.api_gateway import Router +from layercake.dynamodb import DynamoDBPersistenceLayer + +from boto3clients import dynamodb_client +from config import ENROLLMENT_TABLE + +logger = Logger(__name__) +router = Router() +dyn = DynamoDBPersistenceLayer(ENROLLMENT_TABLE, dynamodb_client) + + +@router.patch('/') +def cancel(): ... diff --git a/api.saladeaula.digital/app/routes/enrollments/dedup_window.py b/api.saladeaula.digital/app/routes/enrollments/dedup_window.py new file mode 100644 index 0000000..b723146 --- /dev/null +++ b/api.saladeaula.digital/app/routes/enrollments/dedup_window.py @@ -0,0 +1,28 @@ +from http import HTTPStatus +from typing import Annotated + +from aws_lambda_powertools.event_handler.api_gateway import Router +from aws_lambda_powertools.event_handler.openapi.params import Body +from layercake.dynamodb import ( + DynamoDBPersistenceLayer, + KeyPair, +) + +from api_gateway import JSONResponse +from boto3clients import dynamodb_client +from config import ENROLLMENT_TABLE + +router = Router() +dyn = DynamoDBPersistenceLayer(ENROLLMENT_TABLE, dynamodb_client) + + +@router.patch('//dedupwindow', compress=True) +def dedup_window( + enrollment_id: str, + lock_hash: Annotated[str, Body(embed=True)], +): + with dyn.transact_writer() as transact: + transact.delete(key=KeyPair(enrollment_id, 'LOCK')) + transact.delete(key=KeyPair('LOCK', lock_hash)) + + return JSONResponse(HTTPStatus.NO_CONTENT) diff --git a/api.saladeaula.digital/app/routes/enrollments/download_cert.py b/api.saladeaula.digital/app/routes/enrollments/download_cert.py new file mode 100644 index 0000000..e149e7a --- /dev/null +++ b/api.saladeaula.digital/app/routes/enrollments/download_cert.py @@ -0,0 +1,22 @@ +from aws_lambda_powertools.event_handler.api_gateway import Router + +from boto3clients import s3_client +from config import BUCKET_NAME + +router = Router() + + +@router.get('//download') +def download(enrollment_id: str): + params = { + 'Bucket': BUCKET_NAME, + 'Key': f'certs/{enrollment_id}.pdf', + 'ResponseContentDisposition': f'attachment; filename="{enrollment_id}.pdf"', + } + return { + 'presigned_url': s3_client.generate_presigned_url( + ClientMethod='get_object', + Params=params, + ExpiresIn=300, # 5 minutes + ) + } diff --git a/api.saladeaula.digital/app/routes/enrollments/enroll.py b/api.saladeaula.digital/app/routes/enrollments/enroll.py new file mode 100644 index 0000000..7390ac5 --- /dev/null +++ b/api.saladeaula.digital/app/routes/enrollments/enroll.py @@ -0,0 +1,14 @@ +from aws_lambda_powertools import Logger +from aws_lambda_powertools.event_handler.api_gateway import Router +from layercake.dynamodb import DynamoDBPersistenceLayer + +from boto3clients import dynamodb_client +from config import ENROLLMENT_TABLE + +logger = Logger(__name__) +router = Router() +dyn = DynamoDBPersistenceLayer(ENROLLMENT_TABLE, dynamodb_client) + + +@router.post('/') +def enroll(): ... diff --git a/api.saladeaula.digital/app/routes/users/orgs.py b/api.saladeaula.digital/app/routes/users/orgs.py new file mode 100644 index 0000000..b984b9b --- /dev/null +++ b/api.saladeaula.digital/app/routes/users/orgs.py @@ -0,0 +1,21 @@ +from aws_lambda_powertools import Logger +from aws_lambda_powertools.event_handler.api_gateway import Router +from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair + +from boto3clients import dynamodb_client +from config import USER_TABLE + +logger = Logger(__name__) +router = Router() +dyn = DynamoDBPersistenceLayer(USER_TABLE, dynamodb_client) + + +@router.get('//orgs') +def get_orgs(user_id: str): + start_key = router.current_event.get_query_string_value('start_key', None) + + return dyn.collection.query( + # Post-migration (users): rename `orgs` to `ORG` + key=KeyPair(user_id, 'orgs'), + start_key=start_key, + ) diff --git a/api.saladeaula.digital/template.yaml b/api.saladeaula.digital/template.yaml index 956e38a..aa4e7eb 100644 --- a/api.saladeaula.digital/template.yaml +++ b/api.saladeaula.digital/template.yaml @@ -20,7 +20,7 @@ Globals: Architectures: - x86_64 Layers: - - !Sub arn:aws:lambda:sa-east-1:336641857101:layer:layercake:97 + - !Sub arn:aws:lambda:sa-east-1:336641857101:layer:layercake:99 Environment: Variables: TZ: America/Sao_Paulo diff --git a/api.saladeaula.digital/tests/routes/test_courses.py b/api.saladeaula.digital/tests/routes/test_courses.py index 03d36b6..de6949d 100644 --- a/api.saladeaula.digital/tests/routes/test_courses.py +++ b/api.saladeaula.digital/tests/routes/test_courses.py @@ -60,7 +60,7 @@ def test_edit_course( assert ( r['cert']['s3_uri'] - == 's3://saladeaula.digital/certs/2a8963fc-4694-4fe2-953a-316d1b10f1f5.html' + == 's3://saladeaula.digital/certs/templates/2a8963fc-4694-4fe2-953a-316d1b10f1f5.html' ) diff --git a/api.saladeaula.digital/uv.lock b/api.saladeaula.digital/uv.lock index 5fad3d1..3375698 100644 --- a/api.saladeaula.digital/uv.lock +++ b/api.saladeaula.digital/uv.lock @@ -592,7 +592,7 @@ wheels = [ [[package]] name = "layercake" -version = "0.10.1" +version = "0.11.0" source = { directory = "../layercake" } dependencies = [ { name = "arnparse" }, diff --git a/enrollments-events/app/events/allocate_slots.py b/enrollments-events/app/events/allocate_slots.py index b7a47bd..16bda7b 100644 --- a/enrollments-events/app/events/allocate_slots.py +++ b/enrollments-events/app/events/allocate_slots.py @@ -86,7 +86,7 @@ class Course: def _get_courses(ids: set) -> tuple[Course, ...]: pairs = tuple(KeyPair(idx, '0') for idx in ids) - result = course_layer.collection.get_items( + r = course_layer.collection.get_items( KeyChain(pairs), flatten_top=False, ) @@ -96,7 +96,7 @@ def _get_courses(ids: set) -> tuple[Course, ...]: name=obj['name'], access_period=obj['access_period'], ) - for idx, obj in result.items() + for idx, obj in r.items() ) return courses diff --git a/enrollments-events/app/events/issue_cert.py b/enrollments-events/app/events/issue_cert.py index ec0f4fc..1010376 100644 --- a/enrollments-events/app/events/issue_cert.py +++ b/enrollments-events/app/events/issue_cert.py @@ -1,5 +1,6 @@ import json from datetime import datetime, timedelta +from typing import NotRequired, TypedDict import requests from aws_lambda_powertools import Logger @@ -21,8 +22,7 @@ from config import ( ) logger = Logger(__name__) -enrollment_layer = DynamoDBPersistenceLayer(ENROLLMENT_TABLE, dynamodb_client) -course_layer = DynamoDBPersistenceLayer(COURSE_TABLE, dynamodb_client) +dyn = DynamoDBPersistenceLayer(ENROLLMENT_TABLE, dynamodb_client) @event_source(data_class=EventBridgeEvent) @@ -32,10 +32,11 @@ def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool: now_ = now() enrollment_id = new_image['id'] course_id = new_image['course']['id'] - cert = course_layer.collection.get_item( + cert = dyn.collection.get_item( KeyPair( pk=course_id, sk=SortKey('0', path_spec='cert', rename_key='cert'), + table_name=COURSE_TABLE, ), raise_on_error=False, default=None, @@ -49,76 +50,36 @@ def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool: started_at: datetime = fromisoformat(new_image['started_at']) # type: ignore completed_at: datetime = fromisoformat(new_image['completed_at']) # type: ignore # Certificate may have no expiration - cert_expires_at = ( + expires_at = ( completed_at + timedelta(days=int(cert['exp_interval'])) if cert.get('exp_interval', 0) > 0 else None ) + s3_uri = _gen_cert( + enrollment_id, + cert=cert, + user=new_image['user'], + score=new_image['score'], + started_at=started_at, + completed_at=completed_at, + expires_at=expires_at, + ) - try: - if 's3_uri' not in cert: - raise ValueError('Template URI is missing') + update_expr = 'SET cert = :cert, updated_at = :now' + expr_attr_values = { + ':now': now_, + ':cert': {'issued_at': now_} | ({'s3_uri': s3_uri} if s3_uri else {}), + } - # Send template URI and data to Paperforge API to generate a PDF - r = requests.post( - PAPERFORGE_API, - data=json.dumps( - { - 'template_uri': cert['s3_uri'], - 'sign_uri': ESIGN_URI, - 'args': { - 'name': new_image['user']['name'], - 'cpf': _cpffmt(new_image['user']['cpf']), - 'score': new_image['score'], - 'started_at': started_at.strftime('%d/%m/%Y'), - 'completed_at': completed_at.strftime('%d/%m/%Y'), - 'today': _datefmt(now_), - 'year': now_.strftime('%Y'), - } - | ( - {'expires_at': cert_expires_at.strftime('%d/%m/%Y')} - if cert_expires_at - else {} - ), - }, - ), - timeout=5, - ) - r.raise_for_status() + if expires_at: + update_expr = 'SET cert = :cert, cert_expires_at = :cert_expires_at, \ + updated_at = :now' + expr_attr_values[':cert_expires_at'] = expires_at - object_key = f'certs/{enrollment_id}.pdf' - s3_uri = f's3://{BUCKET_NAME}/{object_key}' - - s3_client.put_object( - Bucket=BUCKET_NAME, - Key=object_key, - Body=r.content, - ContentType='application/pdf', - ) - - logger.debug(f'PDF uploaded successfully to {s3_uri}') - except ValueError as exc: - # PDF generation fails if template URI is missing - s3_uri = None - logger.exception(exc) - except requests.exceptions.RequestException as exc: - logger.exception(exc) - raise - - return enrollment_layer.update_item( - key=KeyPair( - pk=enrollment_id, - sk='0', - ), - update_expr='SET cert = :cert, updated_at = :now', - expr_attr_values={ - ':now': now_, - ':cert': { - 'issued_at': now_, - } - | ({'expires_at': cert_expires_at} if cert_expires_at else {}) - | ({'s3_uri': s3_uri} if s3_uri else {}), - }, + return dyn.update_item( + key=KeyPair(pk=enrollment_id, sk='0'), + update_expr=update_expr, + expr_attr_values=expr_attr_values, cond_expr='attribute_exists(sk)', ) @@ -143,3 +104,67 @@ def _datefmt(dt: datetime) -> str: 'Dezembro', ] return f'{dt.day:02d} de {months[dt.month - 1]} de {dt.year}' + + +User = TypedDict('User', {'name': str, 'cpf': str}) +Cert = TypedDict('Cert', {'s3_uri': NotRequired[str]}) + + +def _gen_cert( + id: str, + *, + score: int | float, + cert: Cert, + user: User, + started_at: datetime, + completed_at: datetime, + expires_at: datetime | None = None, +) -> str | None: + now_ = now() + + if 's3_uri' not in cert: + logger.debug('Template URI is missing') + return None + + try: + # Send template URI and data to Paperforge API to generate a PDF + r = requests.post( + PAPERFORGE_API, + data=json.dumps( + { + 'template_uri': cert['s3_uri'], + 'sign_uri': ESIGN_URI, + 'args': { + 'name': user['name'], + 'cpf': _cpffmt(user['cpf']), + 'score': score, + 'started_at': started_at.strftime('%d/%m/%Y'), + 'completed_at': completed_at.strftime('%d/%m/%Y'), + 'today': _datefmt(now_), + 'year': now_.strftime('%Y'), + 'expires_at': expires_at.strftime('%d/%m/%Y') + if expires_at + else None, + }, + }, + ), + timeout=5, + ) + r.raise_for_status() + + object_key = f'certs/{id}.pdf' + s3_uri = f's3://{BUCKET_NAME}/{object_key}' + + s3_client.put_object( + Bucket=BUCKET_NAME, + Key=object_key, + Body=r.content, + ContentType='application/pdf', + ) + + logger.debug(f'PDF uploaded successfully to {s3_uri}') + except requests.exceptions.RequestException as exc: + logger.exception(exc) + raise + + return s3_uri diff --git a/enrollments-events/app/events/reenroll_if_failed.py b/enrollments-events/app/events/reenroll_if_failed.py index 09b55c1..6c2f756 100644 --- a/enrollments-events/app/events/reenroll_if_failed.py +++ b/enrollments-events/app/events/reenroll_if_failed.py @@ -29,10 +29,7 @@ def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool: path_spec='offset_days', rename_key='dedup_window_offset_days', ) - + SortKey('ORG', rename_key='org') - + SortKey('konviva'), - # Post-migration: uncomment the following lines - # + SortKey('KONVIVA', rename_key='konviva') + + SortKey('ORG', rename_key='org'), flatten_top=False, ) user = User.model_validate(new_image['user']) diff --git a/enrollments-events/app/events/reporting/append_cert.py b/enrollments-events/app/events/reporting/append_cert.py index a27f74a..3371e29 100644 --- a/enrollments-events/app/events/reporting/append_cert.py +++ b/enrollments-events/app/events/reporting/append_cert.py @@ -1,5 +1,5 @@ import os -from datetime import datetime, timedelta +from datetime import timedelta import pytz from aws_lambda_powertools import Logger @@ -8,7 +8,6 @@ from aws_lambda_powertools.utilities.data_classes import ( event_source, ) from aws_lambda_powertools.utilities.typing import LambdaContext -from glom import glom from layercake.dateutils import fromisoformat, now, ttl from layercake.dynamodb import DynamoDBPersistenceLayer from layercake.funcs import pick @@ -25,14 +24,11 @@ tz = os.getenv('TZ', 'UTC') @logger.inject_lambda_context def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool | None: new_image = event.detail['new_image'] - expires_at = glom(new_image, 'cert.expires_at', default=None) - - if not expires_at: - return None - enrollment_id = new_image['id'] org_id = new_image['org_id'] - expires_at: datetime = fromisoformat(expires_at).replace(tzinfo=pytz.timezone(tz)) # type: ignore + expires_at = fromisoformat(new_image['cert_expires_at']).replace( # type: ignore + tzinfo=pytz.timezone(tz) + ) # The reporting month is the month before the certificate expires month_start = (expires_at.replace(day=1) - timedelta(days=1)).replace(day=1) now_ = now() @@ -59,11 +55,10 @@ def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool | No item={ 'id': pk, 'sk': f'{sk}#ENROLLMENT#{enrollment_id}', - 'enrollment_id': new_image['id'], 'user': pick(('id', 'name'), new_image['user']), 'course': pick(('id', 'name'), new_image['course']), 'enrolled_at': new_image['created_at'], - 'expires_at': expires_at, # type: ignore + 'expires_at': expires_at, 'completed_at': new_image['completed_at'], 'created_at': now_, }, diff --git a/enrollments-events/app/events/reporting/send_report_email.py b/enrollments-events/app/events/reporting/send_report_email.py index 6e680ee..bf55367 100644 --- a/enrollments-events/app/events/reporting/send_report_email.py +++ b/enrollments-events/app/events/reporting/send_report_email.py @@ -52,14 +52,14 @@ def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool: # Key pattern `CERT#REPORTING#ORG#{org_id}` *_, org_id = old_image['id'].split('#') event_name = old_image['sk'] - target_month = datetime.strptime(old_image['target_month'], '%Y-%m').date() - month = _monthfmt(target_month) + target_month = old_image['target_month'] + pretty_month = _monthfmt(datetime.strptime(target_month, '%Y-%m').date()) now_ = now() - result = enrollment_layer.collection.query( + r = enrollment_layer.collection.query( KeyPair( pk=old_image['id'], - sk='MONTH#{}#ENROLLMENT'.format(target_month.strftime('%Y-%m')), + sk=f'MONTH#{target_month}#ENROLLMENT', ), limit=150, ) @@ -68,8 +68,8 @@ def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool: { 'template_uri': CERT_REPORTING_URI, 'args': { - 'month': month, - 'items': result['items'], + 'month': pretty_month, + 'items': r['items'], }, }, cls=Encoder, @@ -83,14 +83,14 @@ def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool: to=_get_admin_emails(org_id), reply_to=REPLY_TO, bcc=BCC, - subject=SUBJECT.format(month=month), + subject=SUBJECT.format(month=pretty_month), ) - emailmsg.add_alternative(MESSAGE.format(month=month)) + emailmsg.add_alternative(MESSAGE.format(month=pretty_month)) attachment = MIMEApplication(r.content) attachment.add_header( 'Content-Disposition', 'attachment', - filename='{}.pdf'.format(target_month.strftime('%Y-%m')), + filename=f'{target_month}.pdf', ) emailmsg.attach(attachment) diff --git a/enrollments-events/app/events/stopgap/patch_course_metadata.py b/enrollments-events/app/events/stopgap/patch_course_metadata.py index daee882..9aae6b6 100644 --- a/enrollments-events/app/events/stopgap/patch_course_metadata.py +++ b/enrollments-events/app/events/stopgap/patch_course_metadata.py @@ -1,5 +1,3 @@ -import json -import sqlite3 from datetime import timedelta from aws_lambda_powertools import Logger @@ -17,8 +15,6 @@ from config import ( ENROLLMENT_TABLE, ) -sqlite3.register_converter('json', json.loads) - logger = Logger(__name__) enrollment_layer = DynamoDBPersistenceLayer(ENROLLMENT_TABLE, dynamodb_client) course_layer = DynamoDBPersistenceLayer(COURSE_TABLE, dynamodb_client) diff --git a/enrollments-events/app/events/stopgap/set_subscription_covered.py b/enrollments-events/app/events/stopgap/set_subscription_covered.py index 788c393..34e322c 100644 --- a/enrollments-events/app/events/stopgap/set_subscription_covered.py +++ b/enrollments-events/app/events/stopgap/set_subscription_covered.py @@ -37,7 +37,7 @@ def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool: transact.update( key=KeyPair(new_image['id'], '0'), update_expr='SET subscription_covered = :subscription_covered, \ - updated_at = :now', + updated_at = :now', expr_attr_values={ ':subscription_covered': True, ':now': now_, diff --git a/enrollments-events/template.yaml b/enrollments-events/template.yaml index 09d6ebf..0c2747f 100644 --- a/enrollments-events/template.yaml +++ b/enrollments-events/template.yaml @@ -26,7 +26,7 @@ Globals: Architectures: - x86_64 Layers: - - !Sub arn:aws:lambda:sa-east-1:336641857101:layer:layercake:98 + - !Sub arn:aws:lambda:sa-east-1:336641857101:layer:layercake:99 Environment: Variables: TZ: America/Sao_Paulo @@ -96,6 +96,8 @@ Resources: detail: new_image: sk: ["0"] + access_expires_at: + - exists: false EventPatchKonvivaFunction: Type: AWS::Serverless::Function @@ -355,6 +357,8 @@ Resources: sk: ["0"] new_image: status: [COMPLETED] + cert_expires_at: + - exists: true org_id: - exists: true diff --git a/enrollments-events/tests/conftest.py b/enrollments-events/tests/conftest.py index dcc83be..0dc9c06 100644 --- a/enrollments-events/tests/conftest.py +++ b/enrollments-events/tests/conftest.py @@ -1,11 +1,10 @@ import os from dataclasses import dataclass -from uuid import uuid4 import jsonlines import pytest -PYTEST_TABLE_NAME = f'pytest-{uuid4()}' +PYTEST_TABLE_NAME = 'pytest' PK = 'id' SK = 'sk' diff --git a/enrollments-events/tests/events/reporting/test_append_cert.py b/enrollments-events/tests/events/reporting/test_append_cert.py index 4d4f323..2ed50f7 100644 --- a/enrollments-events/tests/events/reporting/test_append_cert.py +++ b/enrollments-events/tests/events/reporting/test_append_cert.py @@ -15,7 +15,7 @@ def test_append_cert( dynamodb_persistence_layer: DynamoDBPersistenceLayer, lambda_context: LambdaContext, ): - expires_at = now() + timedelta(days=360) + cert_expires_at = now() + timedelta(days=360) event = { 'detail': { 'new_image': { @@ -25,9 +25,7 @@ def test_append_cert( 'id': '431', 'name': 'How to Sing Better', }, - 'cert': { - 'expires_at': expires_at.isoformat(), - }, + 'cert_expires_at': cert_expires_at.isoformat(), 'user': { 'id': '1234', 'name': 'Tobias Summit', @@ -41,7 +39,7 @@ def test_append_cert( assert app.lambda_handler(event, lambda_context) # type: ignore # The reporting month is the month before the certificate expires - month_start = (expires_at.replace(day=1) - timedelta(days=1)).replace(day=1) + month_start = (cert_expires_at.replace(day=1) - timedelta(days=1)).replace(day=1) report_sk = 'MONTH#{}#SCHEDULE#SEND_REPORT_EMAIL'.format( month_start.strftime('%Y-%m') ) @@ -54,7 +52,7 @@ def test_append_cert( ) + SortKey( sk='MONTH#{}#ENROLLMENT#e45019d8-be7a-4a82-9b37-12a01f0127bb'.format( - expires_at.strftime('%Y-%m') + cert_expires_at.strftime('%Y-%m') ), rename_key='enrollment', ), diff --git a/enrollments-events/tests/events/test_issue_cert.py b/enrollments-events/tests/events/test_issue_cert.py index 7336d5a..734dac8 100644 --- a/enrollments-events/tests/events/test_issue_cert.py +++ b/enrollments-events/tests/events/test_issue_cert.py @@ -36,3 +36,43 @@ def test_issue_cert( ) assert 'cert' in r + assert 'cert_expires_at' in r + assert ( + r['cert']['s3_uri'] + == 's3://saladeaula.digital/certs/1ee108ae-67d4-4545-bf6d-4e641cdaa4e0.pdf' + ) + + +def test_non_exp_interval( + seeds, + dynamodb_persistence_layer: DynamoDBPersistenceLayer, + lambda_context: LambdaContext, +): + enrollment_id = '1ee108ae-67d4-4545-bf6d-4e641cdaa4e0' + event = { + 'detail': { + 'new_image': { + 'id': enrollment_id, + 'completed_at': '2025-09-21T14:20:36.276467-03:00', + 'started_at': '2025-09-19T14:34:54.704548-03:00', + 'user': { + 'name': 'Josh Kiszka', + 'cpf': '74630003037', + }, + 'course': { + 'id': '12334', + 'name': 'pytest', + }, + 'score': 79, + 'status': 'COMPLETED', + } + } + } + assert app.lambda_handler(event, lambda_context) # type: ignore + + r = dynamodb_persistence_layer.get_item( + key=KeyPair('1ee108ae-67d4-4545-bf6d-4e641cdaa4e0', '0') + ) + + assert 'cert' in r + assert 'cert_expires_at' not in r diff --git a/enrollments-events/tests/seeds.jsonl b/enrollments-events/tests/seeds.jsonl index e50a5f1..403fbb8 100644 --- a/enrollments-events/tests/seeds.jsonl +++ b/enrollments-events/tests/seeds.jsonl @@ -12,7 +12,7 @@ // Course {"id": "123", "sk": "0", "access_period": 360, "cert": {"exp_interval": 700, "s3_uri": "s3://saladeaula.digital/certs/samples/cipa-grau-de-risco-1.html"}, "created_at": "2025-07-14T15:09:18.559528-03:00", "metadata__konviva_class_id": "281", "name": "pytest", "tenant_id": "*"} -{"id": "12334", "sk": "0", "access_period": 360} +{"id": "12334", "sk": "0", "access_period": 360, "cert": {"s3_uri": "s3://saladeaula.digital/certs/samples/cipa-grau-de-risco-1.html"}} {"id": "a955518e-ebcb-4441-b914-ddc9ecef84f0", "sk": "0", "access_period": "360", "cert": {"exp_interval": 360}, "created_at": "2025-07-14T15:09:18.559528-03:00", "metadata__konviva_class_id": "281", "name": "NR-11 Operador de Munck", "tenant_id": "*"} {"id": "6a403773-aeac-4e6a-ac39-dc958e4be52a", "sk": "0", "access_period": "360", "cert": {"exp_interval": 360}, "created_at": "2025-07-14T15:09:18.559528-03:00", "metadata__konviva_class_id": "281", "name": "Reciclagem em NR-11 - Operador de Empilhadeira", "tenant_id": "*"} {"id": "e1c44881-2fe3-484e-ada2-12b6bf5b9398", "sk": "0", "name": "NR-35 Segurança nos Trabalhos em Altura (Teórico)", "updated_at": "2025-08-22T00:00:24.431267-03:00", "access_period": 360, "created_at": "2024-12-30T00:11:33.088916-03:00", "metadata__konviva_class_id": 1, "tenant_id": "*", "cert": {"exp_interval": 700}, "metadata__unit_price": 119} diff --git a/enrollments-events/uv.lock b/enrollments-events/uv.lock index f62a44e..571a7d8 100644 --- a/enrollments-events/uv.lock +++ b/enrollments-events/uv.lock @@ -501,7 +501,7 @@ wheels = [ [[package]] name = "layercake" -version = "0.10.1" +version = "0.11.0" source = { directory = "../layercake" } dependencies = [ { name = "arnparse" }, diff --git a/id.saladeaula.digital/template.yaml b/id.saladeaula.digital/template.yaml index 4ef0749..de9fff2 100644 --- a/id.saladeaula.digital/template.yaml +++ b/id.saladeaula.digital/template.yaml @@ -14,7 +14,7 @@ Globals: Architectures: - x86_64 Layers: - - !Sub arn:aws:lambda:sa-east-1:336641857101:layer:layercake:98 + - !Sub arn:aws:lambda:sa-east-1:336641857101:layer:layercake:99 Environment: Variables: TZ: America/Sao_Paulo diff --git a/id.saladeaula.digital/uv.lock b/id.saladeaula.digital/uv.lock index ac7882c..ee3fb72 100644 --- a/id.saladeaula.digital/uv.lock +++ b/id.saladeaula.digital/uv.lock @@ -507,7 +507,7 @@ wheels = [ [[package]] name = "layercake" -version = "0.10.1" +version = "0.11.0" source = { directory = "../layercake" } dependencies = [ { name = "arnparse" }, diff --git a/konviva-events/app/enrollment.py b/konviva-events/app/enrollment.py index 9804f7c..7258dbf 100644 --- a/konviva-events/app/enrollment.py +++ b/konviva-events/app/enrollment.py @@ -1,20 +1,17 @@ from decimal import Decimal +from aws_lambda_powertools import Logger from aws_lambda_powertools.event_handler.exceptions import ( BadRequestError, NotFoundError, ) -from botocore.args import logger -from glom import glom from layercake.dateutils import now, ttl from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair, SortKey, TransactKey from layercake.strutils import md5_hash -from boto3clients import dynamodb_client from config import COURSE_TABLE -# @TODO Find a better way -course_layer = DynamoDBPersistenceLayer(COURSE_TABLE, dynamodb_client) +logger = Logger(__name__) def update_progress( @@ -119,15 +116,8 @@ def set_score( ), ) user_id = enrollment['user']['id'] - course_id = glom(enrollment, 'course.id') - exp_interval = course_layer.collection.get_item( - KeyPair( - pk=course_id, - sk=SortKey('0', path_spec='cert.exp_interval'), - ), - raise_on_error=False, - default=0, - ) + course_id = enrollment['course']['id'] + dedup_window_offset_days = int(enrollment['dedup_window_offset_days']) try: if score >= 70: @@ -138,8 +128,7 @@ def set_score( progress=progress, user_id=user_id, course_id=course_id, - cert_exp_interval=int(exp_interval), - dedup_window_offset_days=int(enrollment['dedup_window_offset_days']), + dedup_window_offset_days=dedup_window_offset_days, dynamodb_persistence_layer=dynamodb_persistence_layer, ) @@ -165,23 +154,21 @@ def _set_status_as_completed( *, user_id: str, course_id: str, - cert_exp_interval: int, dedup_window_offset_days: int, dynamodb_persistence_layer: DynamoDBPersistenceLayer, ) -> bool: now_ = now() lock_hash = md5_hash(f'{user_id}{course_id}') - cert_exp_ttl = ttl( - start_dt=now_, - days=cert_exp_interval, - ) - cert_exp_reminder_ttl = ttl( - start_dt=now_, - days=cert_exp_interval - 30, - ) - dedup_lock_ttl = ttl( - start_dt=now_, - days=cert_exp_interval - dedup_window_offset_days, + exp_interval = int( + dynamodb_persistence_layer.collection.get_item( + KeyPair( + pk=course_id, + sk=SortKey('0', path_spec='cert.exp_interval'), + table_name=COURSE_TABLE, + ), + raise_on_error=False, + default=0, + ) ) with dynamodb_persistence_layer.transact_writer() as transact: @@ -204,12 +191,17 @@ def _set_status_as_completed( exc_cls=EnrollmentConflictError, ) - if cert_exp_interval: + if exp_interval: + dedup_lock_ttl = ttl( + start_dt=now_, + days=exp_interval - dedup_window_offset_days, + ) + transact.put( item={ 'id': id, 'sk': 'SCHEDULE#SET_CERT_EXPIRED', - 'ttl': cert_exp_ttl, + 'ttl': ttl(start_dt=now_, days=exp_interval), 'created_at': now_, } ) @@ -217,7 +209,7 @@ def _set_status_as_completed( item={ 'id': id, 'sk': 'SCHEDULE#REMINDER_CERT_EXPIRATION_BEFORE_30_DAYS', - 'ttl': cert_exp_reminder_ttl, + 'ttl': ttl(start_dt=now_, days=exp_interval - 30), 'created_at': now_, } ) diff --git a/konviva-events/app/events/cancel.py b/konviva-events/app/events/cancel.py index 5c1441c..a03c432 100644 --- a/konviva-events/app/events/cancel.py +++ b/konviva-events/app/events/cancel.py @@ -23,10 +23,10 @@ def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool: # r = dyn.get_item(KeyPair(new_image['id'], 'KONVIVA')) try: - result = konviva.cancel_enrollment(r['enrollment_id']) + r = konviva.cancel_enrollment(r['enrollment_id']) except Exception as exc: logger.exception(exc) return False else: - logger.info('Enrollment canceled', result=result) + logger.info('Enrollment canceled', result=r) return True diff --git a/konviva-events/app/events/create_user.py b/konviva-events/app/events/create_user.py index 66f8add..f8706ed 100644 --- a/konviva-events/app/events/create_user.py +++ b/konviva-events/app/events/create_user.py @@ -35,10 +35,10 @@ def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool: except konviva.EmailAlreadyExistsError as exc: logger.exception(exc, email=new_image['email']) - result = konviva.get_users_by_email(new_image['email']) - user_id = glom(result, '0.IDUsuario') + r = konviva.get_users_by_email(new_image['email']) + user_id = glom(r, '0.IDUsuario') - if not result: + if not r: raise UserNotFoundError('User not found') except Exception: raise diff --git a/konviva-events/app/events/update_user.py b/konviva-events/app/events/update_user.py index 3d6b298..4947699 100644 --- a/konviva-events/app/events/update_user.py +++ b/konviva-events/app/events/update_user.py @@ -31,10 +31,10 @@ def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool: payload.update(CPF=new_image['cpf']) try: - result = konviva.update_user(id=user_id, **payload) + r = konviva.update_user(id=user_id, **payload) except Exception as exc: logger.exception(exc) return False else: - logger.info('User updated', result=result, payload=payload) + logger.info('User updated', result=r, payload=payload) return True diff --git a/konviva-events/template.yaml b/konviva-events/template.yaml index 5a4a49e..2f00050 100644 --- a/konviva-events/template.yaml +++ b/konviva-events/template.yaml @@ -20,7 +20,7 @@ Globals: Architectures: - x86_64 Layers: - - !Sub arn:aws:lambda:sa-east-1:336641857101:layer:layercake:96 + - !Sub arn:aws:lambda:sa-east-1:336641857101:layer:layercake:99 Environment: Variables: TZ: America/Sao_Paulo diff --git a/konviva-events/uv.lock b/konviva-events/uv.lock index 7f2a808..aaa32fa 100644 --- a/konviva-events/uv.lock +++ b/konviva-events/uv.lock @@ -497,7 +497,7 @@ dev = [ [[package]] name = "layercake" -version = "0.10.1" +version = "0.11.0" source = { directory = "../layercake" } dependencies = [ { name = "arnparse" }, diff --git a/layercake/layercake/dynamodb.py b/layercake/layercake/dynamodb.py index 6f8d291..e185a57 100644 --- a/layercake/layercake/dynamodb.py +++ b/layercake/layercake/dynamodb.py @@ -253,7 +253,6 @@ class KeyPair(Key): sk: str, *, rename_key: str | None = None, - retain_key: bool = False, table_name: str | None = None, ) -> None: """ @@ -267,24 +266,17 @@ class KeyPair(Key): The sort key. rename_key : str, optional If provided, renames the sort key in the output. - retain_key : bool, optional - Use the key itself as value if True; otherwise, use the extracted value. table_name : str, optional """ super().__init__(**{PK: pk, SK: sk}) self._rename_key = rename_key - self._retain_key = retain_key self._table_name = table_name @property def rename_key(self) -> str | None: return self._rename_key - @property - def retain_key(self) -> bool: - return self._retain_key - @property def table_name(self) -> str | None: return self._table_name @@ -435,14 +427,11 @@ class TransactWriter: if cond_expr: attrs['ConditionExpression'] = cond_expr - if not table_name: - table_name = self._table_name - self._add_op_and_process( TransactOperation( { 'Put': dict( - TableName=table_name, + TableName=table_name or self._table_name, Item=serialize(item), **attrs, ) @@ -473,14 +462,11 @@ class TransactWriter: if expr_attr_values: attrs['ExpressionAttributeValues'] = serialize(expr_attr_values) - if not table_name: - table_name = self._table_name - self._add_op_and_process( TransactOperation( { 'Update': dict( - TableName=table_name, + TableName=table_name or self._table_name, Key=serialize(key), UpdateExpression=update_expr, **attrs, @@ -511,14 +497,11 @@ class TransactWriter: if expr_attr_values: attrs['ExpressionAttributeValues'] = serialize(expr_attr_values) - if not table_name: - table_name = self._table_name - self._add_op_and_process( TransactOperation( { 'Delete': dict( - TableName=table_name, + TableName=table_name or self._table_name, Key=serialize(key), **attrs, ) @@ -545,14 +528,11 @@ class TransactWriter: if expr_attr_values: attrs['ExpressionAttributeValues'] = serialize(expr_attr_values) - if not table_name: - table_name = self._table_name - self._add_op_and_process( TransactOperation( { 'ConditionCheck': dict( - TableName=table_name, + TableName=table_name or self._table_name, Key=serialize(key), **attrs, ) @@ -619,6 +599,7 @@ class DynamoDBPersistenceLayer: filter_expr: str | None = None, limit: int | None = None, index_forward: bool = True, + table_name: str | None = None, ) -> dict[str, Any]: """You must provide the name of the partition key attribute and a single value for that attribute. @@ -637,7 +618,7 @@ class DynamoDBPersistenceLayer: - https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/dynamodb/client/query.html """ attrs: dict = { - 'TableName': self.table_name, + 'TableName': table_name or self.table_name, 'KeyConditionExpression': key_cond_expr, 'ScanIndexForward': index_forward, } @@ -658,18 +639,18 @@ class DynamoDBPersistenceLayer: attrs['Limit'] = limit try: - response = self.client.query(**attrs) + r = self.client.query(**attrs) except ClientError as err: logger.info(attrs) logger.exception(err) raise else: return dict( - items=[deserialize(v) for v in response.get('Items', [])], - last_key=response.get('LastEvaluatedKey', None), + items=[deserialize(v) for v in r.get('Items', [])], + last_key=r.get('LastEvaluatedKey', None), ) - def get_item(self, key: dict) -> dict: + def get_item(self, key: dict, table_name: str | None = None) -> dict: """The GetItem operation returns a set of attributes for the item with the given primary key. @@ -677,22 +658,28 @@ class DynamoDBPersistenceLayer: there will be no Item element in the response. """ attrs = { - 'TableName': self.table_name, + 'TableName': table_name or self.table_name, 'Key': serialize(key), } try: - response = self.client.get_item(**attrs) + r = self.client.get_item(**attrs) except ClientError as err: logger.info(attrs) logger.exception(err) raise else: - return deserialize(response.get('Item', {})) + return deserialize(r.get('Item', {})) - def put_item(self, item: dict, *, cond_expr: str | None = None) -> bool: + def put_item( + self, + item: dict, + *, + cond_expr: str | None = None, + table_name: str | None = None, + ) -> bool: attrs = { - 'TableName': self.table_name, + 'TableName': table_name or self.table_name, 'Item': serialize(item), } @@ -716,9 +703,10 @@ class DynamoDBPersistenceLayer: cond_expr: str | None = None, expr_attr_names: dict | None = None, expr_attr_values: dict | None = None, + table_name: str | None = None, ) -> bool: attrs: dict = { - 'TableName': self.table_name, + 'TableName': table_name or self.table_name, 'Key': serialize(key), 'UpdateExpression': update_expr, } @@ -748,13 +736,14 @@ class DynamoDBPersistenceLayer: cond_expr: str | None = None, expr_attr_names: dict | None = None, expr_attr_values: dict | None = None, + table_name: str | None = None, ) -> bool: """Deletes a single item in a table by primary key. You can perform a conditional delete operation that deletes the item if it exists, or if it has an expected attribute value. """ attrs: dict = { - 'TableName': self.table_name, + 'TableName': table_name or self.table_name, 'Key': serialize(key), } @@ -780,9 +769,13 @@ class DynamoDBPersistenceLayer: def collection(self) -> 'DynamoDBCollection': return DynamoDBCollection(self) - def transact_writer(self, flush_amount: int = 50) -> TransactWriter: + def transact_writer( + self, + flush_amount: int = 50, + table_name: str | None = None, + ) -> TransactWriter: return TransactWriter( - table_name=self.table_name, + table_name=table_name or self.table_name, client=self.client, flush_amount=flush_amount, ) @@ -913,19 +906,20 @@ class DynamoDBCollection: Raises the provided exception if the item is not found and raise_on_error is True. """ - exc_cls = exc_cls or self.exc_cls - data = self.persistence_layer.get_item(key) + table_name = getattr(key, 'table_name', None) path_spec = getattr(key[SK], 'path_spec', None) + r = self.persistence_layer.get_item(key, table_name) - if raise_on_error and not data: + if raise_on_error and not r: + exc_cls = exc_cls or self.exc_cls raise exc_cls(f'Item with {key} not found.') - if path_spec and data: + if path_spec and r: from glom import glom - return glom(data, path_spec, default=default) + return glom(r, path_spec, default=default) - return data or default + return r or default def put_item( self, @@ -954,6 +948,8 @@ class DynamoDBCollection: bool True if the operation is successful, False otherwise. """ + table_name = getattr(key, 'table_name', None) + if isinstance(ttl, int): kwargs.update({'ttl': ttl}) @@ -963,6 +959,7 @@ class DynamoDBCollection: return self.persistence_layer.put_item( item=key | kwargs, cond_expr=cond_expr, + table_name=table_name, ) def delete_item( @@ -991,11 +988,14 @@ class DynamoDBCollection: bool True if the item is successfully deleted, False otherwise. """ + table_name = getattr(key, 'table_name', None) + return self.persistence_layer.delete_item( key=key, cond_expr=cond_expr, expr_attr_names=expr_attr_names, expr_attr_values=expr_attr_values, + table_name=table_name, ) def get_items( @@ -1059,7 +1059,7 @@ class DynamoDBCollection: if not key.pairs: return {} - sortkeys = key.pairs[1:] if flatten_top else key.pairs + pairs = key.pairs[1:] if flatten_top else key.pairs client = self.persistence_layer.client table_name = self.persistence_layer.table_name @@ -1074,8 +1074,8 @@ class DynamoDBCollection: for pair in key.pairs ] - response = client.transact_get_items(TransactItems=transact_items) # type: ignore - items = [deserialize(r.get('Item', {})) for r in response.get('Responses', [])] + r = client.transact_get_items(TransactItems=transact_items) # type: ignore + items = [deserialize(r.get('Item', {})) for r in r.get('Responses', [])] if flatten_top: head, *tail = items @@ -1103,16 +1103,14 @@ class DynamoDBCollection: if getattr(sk, 'rename_key', None): return sk.rename_key - if not isinstance(sk, SortKey): - return pk + if isinstance(sk, SortKey): + return sk.removeprefix(sk.remove_prefix or '') - key = pk if pair.retain_key else sk - - return key.removeprefix(sk.remove_prefix or '') + return pk return head | { _map_key(pair): _extract_sk_values(pair, obj) - for pair, obj in zip(sortkeys, tail) + for pair, obj in zip(pairs, tail) if obj } @@ -1179,7 +1177,7 @@ class DynamoDBCollection: else '#pk = :pk' ) - response = self.persistence_layer.query( + r = self.persistence_layer.query( key_cond_expr=key_cond_expr, expr_attr_name=key.expr_attr_name() | expr_attr_name, expr_attr_values=key.expr_attr_values() | expr_attr_values, @@ -1189,10 +1187,8 @@ class DynamoDBCollection: start_key=_startkey_b64decode(start_key) if start_key else {}, ) - items = response['items'] - last_key = ( - _startkey_b64encode(response['last_key']) if response['last_key'] else None - ) + items = r['items'] + last_key = _startkey_b64encode(r['last_key']) if r['last_key'] else None def _removeprefix( items: list[dict[str, Any]], /, key: str, prefix: str diff --git a/layercake/pyproject.toml b/layercake/pyproject.toml index 2a852fb..726b380 100644 --- a/layercake/pyproject.toml +++ b/layercake/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "layercake" -version = "0.10.1" +version = "0.11.0" description = "Packages shared dependencies to optimize deployment and ensure consistency across functions." readme = "README.md" authors = [ diff --git a/layercake/tests/test_dynamodb.py b/layercake/tests/test_dynamodb.py index e911593..ce13cc4 100644 --- a/layercake/tests/test_dynamodb.py +++ b/layercake/tests/test_dynamodb.py @@ -178,7 +178,7 @@ def test_collection_get_item_path_spec( KeyPair( pk='5OxmMjL-ujoR5IMGegQz', sk=SortKey( - ComposeKey('sergio@somosbeta.com.br', prefix='emails'), + 'emails#sergio@somosbeta.com.br', path_spec='mx_record_exists', ), ), @@ -193,7 +193,7 @@ def test_collection_put_item( assert dynamodb_persistence_layer.collection.put_item( KeyPair( '5OxmMjL-ujoR5IMGegQz', - ComposeKey('6d1044d5-18c5-437c-9219-fc2ace7e5ebc', prefix='orgs'), + 'orgs#6d1044d5-18c5-437c-9219-fc2ace7e5ebc', ), name='Beta Educação', ttl=ttl(days=3), @@ -202,7 +202,7 @@ def test_collection_put_item( data = dynamodb_persistence_layer.collection.get_item( KeyPair( pk='5OxmMjL-ujoR5IMGegQz', - sk=ComposeKey('6d1044d5-18c5-437c-9219-fc2ace7e5ebc', prefix='orgs'), + sk='orgs#6d1044d5-18c5-437c-9219-fc2ace7e5ebc', ), ) @@ -219,7 +219,7 @@ def test_collection_delete_item( assert dynamodb_persistence_layer.collection.delete_item( KeyPair( '5OxmMjL-ujoR5IMGegQz', - ComposeKey('sergio@somsbeta.com.br', prefix='emails'), + 'emails#sergio@somsbeta.com.br', ) ) @@ -232,6 +232,7 @@ def test_collection_query( logs = dynamodb_persistence_layer.collection.query( PartitionKey( ComposeKey('5OxmMjL-ujoR5IMGegQz', prefix='logs'), + # 'logs#5OxmMjL-ujoR5IMGegQz', ), ) assert len(logs['items']) == 2 @@ -280,7 +281,6 @@ def test_collection_get_items( 'cJtK9SsnJhKPyxESe7g3DG', table_name=dynamodb_persistence_layer.table_name ) + SortKey('0') - + SortKey('0') + SortKey( 'metadata#billing_policy', path_spec='payment_method', @@ -407,7 +407,7 @@ def test_collection_get_items_pair_path_spec( + KeyPair( 'email', SortKey('osergiosiqueira@gmail.com', path_spec='user_id'), - retain_key=True, + rename_key='email', ), flatten_top=False, ) diff --git a/layercake/uv.lock b/layercake/uv.lock index a1faa01..de6a492 100644 --- a/layercake/uv.lock +++ b/layercake/uv.lock @@ -675,7 +675,7 @@ wheels = [ [[package]] name = "layercake" -version = "0.10.0" +version = "0.10.1" source = { editable = "." } dependencies = [ { name = "arnparse" }, diff --git a/orders-events/app/events/append_org_id.py b/orders-events/app/events/append_org_id.py index e10ca69..80336a9 100644 --- a/orders-events/app/events/append_org_id.py +++ b/orders-events/app/events/append_org_id.py @@ -24,7 +24,7 @@ order_layer = DynamoDBPersistenceLayer(ORDER_TABLE, dynamodb_client) def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool: new_image = event.detail['new_image'] now_ = now() - data = user_layer.collection.get_items( + r = user_layer.collection.get_items( KeyPair( pk='cnpj', sk=SortKey(new_image['cnpj'], path_spec='user_id'), @@ -40,10 +40,10 @@ def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool: # Sometimes the function executes before the user insertion completes, # so an exception is raised to trigger a retry. - if len(data) < 2: + if len(r) < 2: raise ValueError('IDs not found') - logger.info('IDs found', data=data) + logger.info('IDs found', result=r) with order_layer.transact_writer() as transact: transact.update( @@ -52,7 +52,7 @@ def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool: # Post-migration: uncomment the following line # update_expr='SET org_id = :org_id, updated_at = :updated_at', expr_attr_values={ - ':org_id': data['org_id'], + ':org_id': r['org_id'], ':updated_at': now_, }, ) @@ -61,7 +61,7 @@ def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool: key=KeyPair(new_image['id'], 'author'), update_expr='SET user_id = :user_id, updated_at = :updated_at', expr_attr_values={ - ':user_id': data['user_id'], + ':user_id': r['user_id'], ':updated_at': now_, }, ) diff --git a/orders-events/app/events/remove_slots_if_canceled.py b/orders-events/app/events/remove_slots_if_canceled.py index 9d1f5ca..38b1032 100644 --- a/orders-events/app/events/remove_slots_if_canceled.py +++ b/orders-events/app/events/remove_slots_if_canceled.py @@ -23,10 +23,10 @@ def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool: new_image = event.detail['new_image'] order_id = new_image['id'] org_id = new_image['tenant_id'] - # Post-migration: Uncomment the following line + # Post-migration (orders): Uncomment the following line # org_id = new_image['org_id'] - result = enrollment_layer.collection.query( + r = enrollment_layer.collection.query( KeyPair( # Post-migration: Uncomment the following line # f'SLOT#ORG#{org_id}', @@ -38,12 +38,12 @@ def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool: logger.info( 'Slots found', - total_items=len(result['items']), - slots=result['items'], + total_items=len(r['items']), + slots=r['items'], ) with enrollment_layer.batch_writer() as batch: - for pair in result['items']: + for pair in r['items']: batch.delete_item( Key={ 'id': {'S': pair['id']}, diff --git a/orders-events/template.yaml b/orders-events/template.yaml index 2f61ea4..90296ad 100644 --- a/orders-events/template.yaml +++ b/orders-events/template.yaml @@ -26,7 +26,7 @@ Globals: Architectures: - x86_64 Layers: - - !Sub arn:aws:lambda:sa-east-1:336641857101:layer:layercake:98 + - !Sub arn:aws:lambda:sa-east-1:336641857101:layer:layercake:99 Environment: Variables: TZ: America/Sao_Paulo diff --git a/orders-events/uv.lock b/orders-events/uv.lock index 348fe3c..15d56d2 100644 --- a/orders-events/uv.lock +++ b/orders-events/uv.lock @@ -576,7 +576,7 @@ wheels = [ [[package]] name = "layercake" -version = "0.10.1" +version = "0.11.0" source = { directory = "../layercake" } dependencies = [ { name = "arnparse" },