add cert_expires_at
This commit is contained in:
14
api.saladeaula.digital/app/routes/enrollments/cancel.py
Normal file
14
api.saladeaula.digital/app/routes/enrollments/cancel.py
Normal file
@@ -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(): ...
|
||||||
@@ -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('/<enrollment_id>/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)
|
||||||
@@ -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('/<enrollment_id>/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
|
||||||
|
)
|
||||||
|
}
|
||||||
14
api.saladeaula.digital/app/routes/enrollments/enroll.py
Normal file
14
api.saladeaula.digital/app/routes/enrollments/enroll.py
Normal file
@@ -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(): ...
|
||||||
21
api.saladeaula.digital/app/routes/users/orgs.py
Normal file
21
api.saladeaula.digital/app/routes/users/orgs.py
Normal file
@@ -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('/<user_id>/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,
|
||||||
|
)
|
||||||
@@ -20,7 +20,7 @@ Globals:
|
|||||||
Architectures:
|
Architectures:
|
||||||
- x86_64
|
- x86_64
|
||||||
Layers:
|
Layers:
|
||||||
- !Sub arn:aws:lambda:sa-east-1:336641857101:layer:layercake:97
|
- !Sub arn:aws:lambda:sa-east-1:336641857101:layer:layercake:99
|
||||||
Environment:
|
Environment:
|
||||||
Variables:
|
Variables:
|
||||||
TZ: America/Sao_Paulo
|
TZ: America/Sao_Paulo
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ def test_edit_course(
|
|||||||
|
|
||||||
assert (
|
assert (
|
||||||
r['cert']['s3_uri']
|
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'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
2
api.saladeaula.digital/uv.lock
generated
2
api.saladeaula.digital/uv.lock
generated
@@ -592,7 +592,7 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "layercake"
|
name = "layercake"
|
||||||
version = "0.10.1"
|
version = "0.11.0"
|
||||||
source = { directory = "../layercake" }
|
source = { directory = "../layercake" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "arnparse" },
|
{ name = "arnparse" },
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ class Course:
|
|||||||
|
|
||||||
def _get_courses(ids: set) -> tuple[Course, ...]:
|
def _get_courses(ids: set) -> tuple[Course, ...]:
|
||||||
pairs = tuple(KeyPair(idx, '0') for idx in ids)
|
pairs = tuple(KeyPair(idx, '0') for idx in ids)
|
||||||
result = course_layer.collection.get_items(
|
r = course_layer.collection.get_items(
|
||||||
KeyChain(pairs),
|
KeyChain(pairs),
|
||||||
flatten_top=False,
|
flatten_top=False,
|
||||||
)
|
)
|
||||||
@@ -96,7 +96,7 @@ def _get_courses(ids: set) -> tuple[Course, ...]:
|
|||||||
name=obj['name'],
|
name=obj['name'],
|
||||||
access_period=obj['access_period'],
|
access_period=obj['access_period'],
|
||||||
)
|
)
|
||||||
for idx, obj in result.items()
|
for idx, obj in r.items()
|
||||||
)
|
)
|
||||||
|
|
||||||
return courses
|
return courses
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import json
|
import json
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
from typing import NotRequired, TypedDict
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
from aws_lambda_powertools import Logger
|
from aws_lambda_powertools import Logger
|
||||||
@@ -21,8 +22,7 @@ from config import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
logger = Logger(__name__)
|
logger = Logger(__name__)
|
||||||
enrollment_layer = DynamoDBPersistenceLayer(ENROLLMENT_TABLE, dynamodb_client)
|
dyn = DynamoDBPersistenceLayer(ENROLLMENT_TABLE, dynamodb_client)
|
||||||
course_layer = DynamoDBPersistenceLayer(COURSE_TABLE, dynamodb_client)
|
|
||||||
|
|
||||||
|
|
||||||
@event_source(data_class=EventBridgeEvent)
|
@event_source(data_class=EventBridgeEvent)
|
||||||
@@ -32,10 +32,11 @@ def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool:
|
|||||||
now_ = now()
|
now_ = now()
|
||||||
enrollment_id = new_image['id']
|
enrollment_id = new_image['id']
|
||||||
course_id = new_image['course']['id']
|
course_id = new_image['course']['id']
|
||||||
cert = course_layer.collection.get_item(
|
cert = dyn.collection.get_item(
|
||||||
KeyPair(
|
KeyPair(
|
||||||
pk=course_id,
|
pk=course_id,
|
||||||
sk=SortKey('0', path_spec='cert', rename_key='cert'),
|
sk=SortKey('0', path_spec='cert', rename_key='cert'),
|
||||||
|
table_name=COURSE_TABLE,
|
||||||
),
|
),
|
||||||
raise_on_error=False,
|
raise_on_error=False,
|
||||||
default=None,
|
default=None,
|
||||||
@@ -49,76 +50,36 @@ def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool:
|
|||||||
started_at: datetime = fromisoformat(new_image['started_at']) # type: ignore
|
started_at: datetime = fromisoformat(new_image['started_at']) # type: ignore
|
||||||
completed_at: datetime = fromisoformat(new_image['completed_at']) # type: ignore
|
completed_at: datetime = fromisoformat(new_image['completed_at']) # type: ignore
|
||||||
# Certificate may have no expiration
|
# Certificate may have no expiration
|
||||||
cert_expires_at = (
|
expires_at = (
|
||||||
completed_at + timedelta(days=int(cert['exp_interval']))
|
completed_at + timedelta(days=int(cert['exp_interval']))
|
||||||
if cert.get('exp_interval', 0) > 0
|
if cert.get('exp_interval', 0) > 0
|
||||||
else None
|
else None
|
||||||
)
|
)
|
||||||
|
s3_uri = _gen_cert(
|
||||||
try:
|
enrollment_id,
|
||||||
if 's3_uri' not in cert:
|
cert=cert,
|
||||||
raise ValueError('Template URI is missing')
|
user=new_image['user'],
|
||||||
|
score=new_image['score'],
|
||||||
# Send template URI and data to Paperforge API to generate a PDF
|
started_at=started_at,
|
||||||
r = requests.post(
|
completed_at=completed_at,
|
||||||
PAPERFORGE_API,
|
expires_at=expires_at,
|
||||||
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()
|
|
||||||
|
|
||||||
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}')
|
update_expr = 'SET cert = :cert, updated_at = :now'
|
||||||
except ValueError as exc:
|
expr_attr_values = {
|
||||||
# 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_,
|
':now': now_,
|
||||||
':cert': {
|
':cert': {'issued_at': now_} | ({'s3_uri': s3_uri} if s3_uri else {}),
|
||||||
'issued_at': now_,
|
|
||||||
}
|
}
|
||||||
| ({'expires_at': cert_expires_at} if cert_expires_at else {})
|
|
||||||
| ({'s3_uri': s3_uri} if s3_uri else {}),
|
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
|
||||||
|
|
||||||
|
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)',
|
cond_expr='attribute_exists(sk)',
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -143,3 +104,67 @@ def _datefmt(dt: datetime) -> str:
|
|||||||
'Dezembro',
|
'Dezembro',
|
||||||
]
|
]
|
||||||
return f'{dt.day:02d} de {months[dt.month - 1]} de {dt.year}'
|
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
|
||||||
|
|||||||
@@ -29,10 +29,7 @@ def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool:
|
|||||||
path_spec='offset_days',
|
path_spec='offset_days',
|
||||||
rename_key='dedup_window_offset_days',
|
rename_key='dedup_window_offset_days',
|
||||||
)
|
)
|
||||||
+ SortKey('ORG', rename_key='org')
|
+ SortKey('ORG', rename_key='org'),
|
||||||
+ SortKey('konviva'),
|
|
||||||
# Post-migration: uncomment the following lines
|
|
||||||
# + SortKey('KONVIVA', rename_key='konviva')
|
|
||||||
flatten_top=False,
|
flatten_top=False,
|
||||||
)
|
)
|
||||||
user = User.model_validate(new_image['user'])
|
user = User.model_validate(new_image['user'])
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import os
|
import os
|
||||||
from datetime import datetime, timedelta
|
from datetime import timedelta
|
||||||
|
|
||||||
import pytz
|
import pytz
|
||||||
from aws_lambda_powertools import Logger
|
from aws_lambda_powertools import Logger
|
||||||
@@ -8,7 +8,6 @@ from aws_lambda_powertools.utilities.data_classes import (
|
|||||||
event_source,
|
event_source,
|
||||||
)
|
)
|
||||||
from aws_lambda_powertools.utilities.typing import LambdaContext
|
from aws_lambda_powertools.utilities.typing import LambdaContext
|
||||||
from glom import glom
|
|
||||||
from layercake.dateutils import fromisoformat, now, ttl
|
from layercake.dateutils import fromisoformat, now, ttl
|
||||||
from layercake.dynamodb import DynamoDBPersistenceLayer
|
from layercake.dynamodb import DynamoDBPersistenceLayer
|
||||||
from layercake.funcs import pick
|
from layercake.funcs import pick
|
||||||
@@ -25,14 +24,11 @@ tz = os.getenv('TZ', 'UTC')
|
|||||||
@logger.inject_lambda_context
|
@logger.inject_lambda_context
|
||||||
def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool | None:
|
def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool | None:
|
||||||
new_image = event.detail['new_image']
|
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']
|
enrollment_id = new_image['id']
|
||||||
org_id = new_image['org_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
|
# 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 = (expires_at.replace(day=1) - timedelta(days=1)).replace(day=1)
|
||||||
now_ = now()
|
now_ = now()
|
||||||
@@ -59,11 +55,10 @@ def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool | No
|
|||||||
item={
|
item={
|
||||||
'id': pk,
|
'id': pk,
|
||||||
'sk': f'{sk}#ENROLLMENT#{enrollment_id}',
|
'sk': f'{sk}#ENROLLMENT#{enrollment_id}',
|
||||||
'enrollment_id': new_image['id'],
|
|
||||||
'user': pick(('id', 'name'), new_image['user']),
|
'user': pick(('id', 'name'), new_image['user']),
|
||||||
'course': pick(('id', 'name'), new_image['course']),
|
'course': pick(('id', 'name'), new_image['course']),
|
||||||
'enrolled_at': new_image['created_at'],
|
'enrolled_at': new_image['created_at'],
|
||||||
'expires_at': expires_at, # type: ignore
|
'expires_at': expires_at,
|
||||||
'completed_at': new_image['completed_at'],
|
'completed_at': new_image['completed_at'],
|
||||||
'created_at': now_,
|
'created_at': now_,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -52,14 +52,14 @@ def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool:
|
|||||||
# Key pattern `CERT#REPORTING#ORG#{org_id}`
|
# Key pattern `CERT#REPORTING#ORG#{org_id}`
|
||||||
*_, org_id = old_image['id'].split('#')
|
*_, org_id = old_image['id'].split('#')
|
||||||
event_name = old_image['sk']
|
event_name = old_image['sk']
|
||||||
target_month = datetime.strptime(old_image['target_month'], '%Y-%m').date()
|
target_month = old_image['target_month']
|
||||||
month = _monthfmt(target_month)
|
pretty_month = _monthfmt(datetime.strptime(target_month, '%Y-%m').date())
|
||||||
now_ = now()
|
now_ = now()
|
||||||
|
|
||||||
result = enrollment_layer.collection.query(
|
r = enrollment_layer.collection.query(
|
||||||
KeyPair(
|
KeyPair(
|
||||||
pk=old_image['id'],
|
pk=old_image['id'],
|
||||||
sk='MONTH#{}#ENROLLMENT'.format(target_month.strftime('%Y-%m')),
|
sk=f'MONTH#{target_month}#ENROLLMENT',
|
||||||
),
|
),
|
||||||
limit=150,
|
limit=150,
|
||||||
)
|
)
|
||||||
@@ -68,8 +68,8 @@ def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool:
|
|||||||
{
|
{
|
||||||
'template_uri': CERT_REPORTING_URI,
|
'template_uri': CERT_REPORTING_URI,
|
||||||
'args': {
|
'args': {
|
||||||
'month': month,
|
'month': pretty_month,
|
||||||
'items': result['items'],
|
'items': r['items'],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
cls=Encoder,
|
cls=Encoder,
|
||||||
@@ -83,14 +83,14 @@ def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool:
|
|||||||
to=_get_admin_emails(org_id),
|
to=_get_admin_emails(org_id),
|
||||||
reply_to=REPLY_TO,
|
reply_to=REPLY_TO,
|
||||||
bcc=BCC,
|
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 = MIMEApplication(r.content)
|
||||||
attachment.add_header(
|
attachment.add_header(
|
||||||
'Content-Disposition',
|
'Content-Disposition',
|
||||||
'attachment',
|
'attachment',
|
||||||
filename='{}.pdf'.format(target_month.strftime('%Y-%m')),
|
filename=f'{target_month}.pdf',
|
||||||
)
|
)
|
||||||
emailmsg.attach(attachment)
|
emailmsg.attach(attachment)
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import json
|
|
||||||
import sqlite3
|
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
|
||||||
from aws_lambda_powertools import Logger
|
from aws_lambda_powertools import Logger
|
||||||
@@ -17,8 +15,6 @@ from config import (
|
|||||||
ENROLLMENT_TABLE,
|
ENROLLMENT_TABLE,
|
||||||
)
|
)
|
||||||
|
|
||||||
sqlite3.register_converter('json', json.loads)
|
|
||||||
|
|
||||||
logger = Logger(__name__)
|
logger = Logger(__name__)
|
||||||
enrollment_layer = DynamoDBPersistenceLayer(ENROLLMENT_TABLE, dynamodb_client)
|
enrollment_layer = DynamoDBPersistenceLayer(ENROLLMENT_TABLE, dynamodb_client)
|
||||||
course_layer = DynamoDBPersistenceLayer(COURSE_TABLE, dynamodb_client)
|
course_layer = DynamoDBPersistenceLayer(COURSE_TABLE, dynamodb_client)
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ Globals:
|
|||||||
Architectures:
|
Architectures:
|
||||||
- x86_64
|
- x86_64
|
||||||
Layers:
|
Layers:
|
||||||
- !Sub arn:aws:lambda:sa-east-1:336641857101:layer:layercake:98
|
- !Sub arn:aws:lambda:sa-east-1:336641857101:layer:layercake:99
|
||||||
Environment:
|
Environment:
|
||||||
Variables:
|
Variables:
|
||||||
TZ: America/Sao_Paulo
|
TZ: America/Sao_Paulo
|
||||||
@@ -96,6 +96,8 @@ Resources:
|
|||||||
detail:
|
detail:
|
||||||
new_image:
|
new_image:
|
||||||
sk: ["0"]
|
sk: ["0"]
|
||||||
|
access_expires_at:
|
||||||
|
- exists: false
|
||||||
|
|
||||||
EventPatchKonvivaFunction:
|
EventPatchKonvivaFunction:
|
||||||
Type: AWS::Serverless::Function
|
Type: AWS::Serverless::Function
|
||||||
@@ -355,6 +357,8 @@ Resources:
|
|||||||
sk: ["0"]
|
sk: ["0"]
|
||||||
new_image:
|
new_image:
|
||||||
status: [COMPLETED]
|
status: [COMPLETED]
|
||||||
|
cert_expires_at:
|
||||||
|
- exists: true
|
||||||
org_id:
|
org_id:
|
||||||
- exists: true
|
- exists: true
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
import os
|
import os
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from uuid import uuid4
|
|
||||||
|
|
||||||
import jsonlines
|
import jsonlines
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
PYTEST_TABLE_NAME = f'pytest-{uuid4()}'
|
PYTEST_TABLE_NAME = 'pytest'
|
||||||
PK = 'id'
|
PK = 'id'
|
||||||
SK = 'sk'
|
SK = 'sk'
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ def test_append_cert(
|
|||||||
dynamodb_persistence_layer: DynamoDBPersistenceLayer,
|
dynamodb_persistence_layer: DynamoDBPersistenceLayer,
|
||||||
lambda_context: LambdaContext,
|
lambda_context: LambdaContext,
|
||||||
):
|
):
|
||||||
expires_at = now() + timedelta(days=360)
|
cert_expires_at = now() + timedelta(days=360)
|
||||||
event = {
|
event = {
|
||||||
'detail': {
|
'detail': {
|
||||||
'new_image': {
|
'new_image': {
|
||||||
@@ -25,9 +25,7 @@ def test_append_cert(
|
|||||||
'id': '431',
|
'id': '431',
|
||||||
'name': 'How to Sing Better',
|
'name': 'How to Sing Better',
|
||||||
},
|
},
|
||||||
'cert': {
|
'cert_expires_at': cert_expires_at.isoformat(),
|
||||||
'expires_at': expires_at.isoformat(),
|
|
||||||
},
|
|
||||||
'user': {
|
'user': {
|
||||||
'id': '1234',
|
'id': '1234',
|
||||||
'name': 'Tobias Summit',
|
'name': 'Tobias Summit',
|
||||||
@@ -41,7 +39,7 @@ def test_append_cert(
|
|||||||
assert app.lambda_handler(event, lambda_context) # type: ignore
|
assert app.lambda_handler(event, lambda_context) # type: ignore
|
||||||
|
|
||||||
# The reporting month is the month before the certificate expires
|
# 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(
|
report_sk = 'MONTH#{}#SCHEDULE#SEND_REPORT_EMAIL'.format(
|
||||||
month_start.strftime('%Y-%m')
|
month_start.strftime('%Y-%m')
|
||||||
)
|
)
|
||||||
@@ -54,7 +52,7 @@ def test_append_cert(
|
|||||||
)
|
)
|
||||||
+ SortKey(
|
+ SortKey(
|
||||||
sk='MONTH#{}#ENROLLMENT#e45019d8-be7a-4a82-9b37-12a01f0127bb'.format(
|
sk='MONTH#{}#ENROLLMENT#e45019d8-be7a-4a82-9b37-12a01f0127bb'.format(
|
||||||
expires_at.strftime('%Y-%m')
|
cert_expires_at.strftime('%Y-%m')
|
||||||
),
|
),
|
||||||
rename_key='enrollment',
|
rename_key='enrollment',
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -36,3 +36,43 @@ def test_issue_cert(
|
|||||||
)
|
)
|
||||||
|
|
||||||
assert 'cert' in r
|
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
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
|
|
||||||
// Course
|
// 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": "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": "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": "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}
|
{"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}
|
||||||
|
|||||||
2
enrollments-events/uv.lock
generated
2
enrollments-events/uv.lock
generated
@@ -501,7 +501,7 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "layercake"
|
name = "layercake"
|
||||||
version = "0.10.1"
|
version = "0.11.0"
|
||||||
source = { directory = "../layercake" }
|
source = { directory = "../layercake" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "arnparse" },
|
{ name = "arnparse" },
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ Globals:
|
|||||||
Architectures:
|
Architectures:
|
||||||
- x86_64
|
- x86_64
|
||||||
Layers:
|
Layers:
|
||||||
- !Sub arn:aws:lambda:sa-east-1:336641857101:layer:layercake:98
|
- !Sub arn:aws:lambda:sa-east-1:336641857101:layer:layercake:99
|
||||||
Environment:
|
Environment:
|
||||||
Variables:
|
Variables:
|
||||||
TZ: America/Sao_Paulo
|
TZ: America/Sao_Paulo
|
||||||
|
|||||||
2
id.saladeaula.digital/uv.lock
generated
2
id.saladeaula.digital/uv.lock
generated
@@ -507,7 +507,7 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "layercake"
|
name = "layercake"
|
||||||
version = "0.10.1"
|
version = "0.11.0"
|
||||||
source = { directory = "../layercake" }
|
source = { directory = "../layercake" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "arnparse" },
|
{ name = "arnparse" },
|
||||||
|
|||||||
@@ -1,20 +1,17 @@
|
|||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
|
from aws_lambda_powertools import Logger
|
||||||
from aws_lambda_powertools.event_handler.exceptions import (
|
from aws_lambda_powertools.event_handler.exceptions import (
|
||||||
BadRequestError,
|
BadRequestError,
|
||||||
NotFoundError,
|
NotFoundError,
|
||||||
)
|
)
|
||||||
from botocore.args import logger
|
|
||||||
from glom import glom
|
|
||||||
from layercake.dateutils import now, ttl
|
from layercake.dateutils import now, ttl
|
||||||
from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair, SortKey, TransactKey
|
from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair, SortKey, TransactKey
|
||||||
from layercake.strutils import md5_hash
|
from layercake.strutils import md5_hash
|
||||||
|
|
||||||
from boto3clients import dynamodb_client
|
|
||||||
from config import COURSE_TABLE
|
from config import COURSE_TABLE
|
||||||
|
|
||||||
# @TODO Find a better way
|
logger = Logger(__name__)
|
||||||
course_layer = DynamoDBPersistenceLayer(COURSE_TABLE, dynamodb_client)
|
|
||||||
|
|
||||||
|
|
||||||
def update_progress(
|
def update_progress(
|
||||||
@@ -119,15 +116,8 @@ def set_score(
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
user_id = enrollment['user']['id']
|
user_id = enrollment['user']['id']
|
||||||
course_id = glom(enrollment, 'course.id')
|
course_id = enrollment['course']['id']
|
||||||
exp_interval = course_layer.collection.get_item(
|
dedup_window_offset_days = int(enrollment['dedup_window_offset_days'])
|
||||||
KeyPair(
|
|
||||||
pk=course_id,
|
|
||||||
sk=SortKey('0', path_spec='cert.exp_interval'),
|
|
||||||
),
|
|
||||||
raise_on_error=False,
|
|
||||||
default=0,
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if score >= 70:
|
if score >= 70:
|
||||||
@@ -138,8 +128,7 @@ def set_score(
|
|||||||
progress=progress,
|
progress=progress,
|
||||||
user_id=user_id,
|
user_id=user_id,
|
||||||
course_id=course_id,
|
course_id=course_id,
|
||||||
cert_exp_interval=int(exp_interval),
|
dedup_window_offset_days=dedup_window_offset_days,
|
||||||
dedup_window_offset_days=int(enrollment['dedup_window_offset_days']),
|
|
||||||
dynamodb_persistence_layer=dynamodb_persistence_layer,
|
dynamodb_persistence_layer=dynamodb_persistence_layer,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -165,23 +154,21 @@ def _set_status_as_completed(
|
|||||||
*,
|
*,
|
||||||
user_id: str,
|
user_id: str,
|
||||||
course_id: str,
|
course_id: str,
|
||||||
cert_exp_interval: int,
|
|
||||||
dedup_window_offset_days: int,
|
dedup_window_offset_days: int,
|
||||||
dynamodb_persistence_layer: DynamoDBPersistenceLayer,
|
dynamodb_persistence_layer: DynamoDBPersistenceLayer,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
now_ = now()
|
now_ = now()
|
||||||
lock_hash = md5_hash(f'{user_id}{course_id}')
|
lock_hash = md5_hash(f'{user_id}{course_id}')
|
||||||
cert_exp_ttl = ttl(
|
exp_interval = int(
|
||||||
start_dt=now_,
|
dynamodb_persistence_layer.collection.get_item(
|
||||||
days=cert_exp_interval,
|
KeyPair(
|
||||||
|
pk=course_id,
|
||||||
|
sk=SortKey('0', path_spec='cert.exp_interval'),
|
||||||
|
table_name=COURSE_TABLE,
|
||||||
|
),
|
||||||
|
raise_on_error=False,
|
||||||
|
default=0,
|
||||||
)
|
)
|
||||||
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,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
with dynamodb_persistence_layer.transact_writer() as transact:
|
with dynamodb_persistence_layer.transact_writer() as transact:
|
||||||
@@ -204,12 +191,17 @@ def _set_status_as_completed(
|
|||||||
exc_cls=EnrollmentConflictError,
|
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(
|
transact.put(
|
||||||
item={
|
item={
|
||||||
'id': id,
|
'id': id,
|
||||||
'sk': 'SCHEDULE#SET_CERT_EXPIRED',
|
'sk': 'SCHEDULE#SET_CERT_EXPIRED',
|
||||||
'ttl': cert_exp_ttl,
|
'ttl': ttl(start_dt=now_, days=exp_interval),
|
||||||
'created_at': now_,
|
'created_at': now_,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -217,7 +209,7 @@ def _set_status_as_completed(
|
|||||||
item={
|
item={
|
||||||
'id': id,
|
'id': id,
|
||||||
'sk': 'SCHEDULE#REMINDER_CERT_EXPIRATION_BEFORE_30_DAYS',
|
'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_,
|
'created_at': now_,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -23,10 +23,10 @@ def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool:
|
|||||||
# r = dyn.get_item(KeyPair(new_image['id'], 'KONVIVA'))
|
# r = dyn.get_item(KeyPair(new_image['id'], 'KONVIVA'))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
result = konviva.cancel_enrollment(r['enrollment_id'])
|
r = konviva.cancel_enrollment(r['enrollment_id'])
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.exception(exc)
|
logger.exception(exc)
|
||||||
return False
|
return False
|
||||||
else:
|
else:
|
||||||
logger.info('Enrollment canceled', result=result)
|
logger.info('Enrollment canceled', result=r)
|
||||||
return True
|
return True
|
||||||
|
|||||||
@@ -35,10 +35,10 @@ def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool:
|
|||||||
except konviva.EmailAlreadyExistsError as exc:
|
except konviva.EmailAlreadyExistsError as exc:
|
||||||
logger.exception(exc, email=new_image['email'])
|
logger.exception(exc, email=new_image['email'])
|
||||||
|
|
||||||
result = konviva.get_users_by_email(new_image['email'])
|
r = konviva.get_users_by_email(new_image['email'])
|
||||||
user_id = glom(result, '0.IDUsuario')
|
user_id = glom(r, '0.IDUsuario')
|
||||||
|
|
||||||
if not result:
|
if not r:
|
||||||
raise UserNotFoundError('User not found')
|
raise UserNotFoundError('User not found')
|
||||||
except Exception:
|
except Exception:
|
||||||
raise
|
raise
|
||||||
|
|||||||
@@ -31,10 +31,10 @@ def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool:
|
|||||||
payload.update(CPF=new_image['cpf'])
|
payload.update(CPF=new_image['cpf'])
|
||||||
|
|
||||||
try:
|
try:
|
||||||
result = konviva.update_user(id=user_id, **payload)
|
r = konviva.update_user(id=user_id, **payload)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.exception(exc)
|
logger.exception(exc)
|
||||||
return False
|
return False
|
||||||
else:
|
else:
|
||||||
logger.info('User updated', result=result, payload=payload)
|
logger.info('User updated', result=r, payload=payload)
|
||||||
return True
|
return True
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ Globals:
|
|||||||
Architectures:
|
Architectures:
|
||||||
- x86_64
|
- x86_64
|
||||||
Layers:
|
Layers:
|
||||||
- !Sub arn:aws:lambda:sa-east-1:336641857101:layer:layercake:96
|
- !Sub arn:aws:lambda:sa-east-1:336641857101:layer:layercake:99
|
||||||
Environment:
|
Environment:
|
||||||
Variables:
|
Variables:
|
||||||
TZ: America/Sao_Paulo
|
TZ: America/Sao_Paulo
|
||||||
|
|||||||
2
konviva-events/uv.lock
generated
2
konviva-events/uv.lock
generated
@@ -497,7 +497,7 @@ dev = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "layercake"
|
name = "layercake"
|
||||||
version = "0.10.1"
|
version = "0.11.0"
|
||||||
source = { directory = "../layercake" }
|
source = { directory = "../layercake" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "arnparse" },
|
{ name = "arnparse" },
|
||||||
|
|||||||
@@ -253,7 +253,6 @@ class KeyPair(Key):
|
|||||||
sk: str,
|
sk: str,
|
||||||
*,
|
*,
|
||||||
rename_key: str | None = None,
|
rename_key: str | None = None,
|
||||||
retain_key: bool = False,
|
|
||||||
table_name: str | None = None,
|
table_name: str | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
@@ -267,24 +266,17 @@ class KeyPair(Key):
|
|||||||
The sort key.
|
The sort key.
|
||||||
rename_key : str, optional
|
rename_key : str, optional
|
||||||
If provided, renames the sort key in the output.
|
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
|
table_name : str, optional
|
||||||
"""
|
"""
|
||||||
|
|
||||||
super().__init__(**{PK: pk, SK: sk})
|
super().__init__(**{PK: pk, SK: sk})
|
||||||
self._rename_key = rename_key
|
self._rename_key = rename_key
|
||||||
self._retain_key = retain_key
|
|
||||||
self._table_name = table_name
|
self._table_name = table_name
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def rename_key(self) -> str | None:
|
def rename_key(self) -> str | None:
|
||||||
return self._rename_key
|
return self._rename_key
|
||||||
|
|
||||||
@property
|
|
||||||
def retain_key(self) -> bool:
|
|
||||||
return self._retain_key
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def table_name(self) -> str | None:
|
def table_name(self) -> str | None:
|
||||||
return self._table_name
|
return self._table_name
|
||||||
@@ -435,14 +427,11 @@ class TransactWriter:
|
|||||||
if cond_expr:
|
if cond_expr:
|
||||||
attrs['ConditionExpression'] = cond_expr
|
attrs['ConditionExpression'] = cond_expr
|
||||||
|
|
||||||
if not table_name:
|
|
||||||
table_name = self._table_name
|
|
||||||
|
|
||||||
self._add_op_and_process(
|
self._add_op_and_process(
|
||||||
TransactOperation(
|
TransactOperation(
|
||||||
{
|
{
|
||||||
'Put': dict(
|
'Put': dict(
|
||||||
TableName=table_name,
|
TableName=table_name or self._table_name,
|
||||||
Item=serialize(item),
|
Item=serialize(item),
|
||||||
**attrs,
|
**attrs,
|
||||||
)
|
)
|
||||||
@@ -473,14 +462,11 @@ class TransactWriter:
|
|||||||
if expr_attr_values:
|
if expr_attr_values:
|
||||||
attrs['ExpressionAttributeValues'] = serialize(expr_attr_values)
|
attrs['ExpressionAttributeValues'] = serialize(expr_attr_values)
|
||||||
|
|
||||||
if not table_name:
|
|
||||||
table_name = self._table_name
|
|
||||||
|
|
||||||
self._add_op_and_process(
|
self._add_op_and_process(
|
||||||
TransactOperation(
|
TransactOperation(
|
||||||
{
|
{
|
||||||
'Update': dict(
|
'Update': dict(
|
||||||
TableName=table_name,
|
TableName=table_name or self._table_name,
|
||||||
Key=serialize(key),
|
Key=serialize(key),
|
||||||
UpdateExpression=update_expr,
|
UpdateExpression=update_expr,
|
||||||
**attrs,
|
**attrs,
|
||||||
@@ -511,14 +497,11 @@ class TransactWriter:
|
|||||||
if expr_attr_values:
|
if expr_attr_values:
|
||||||
attrs['ExpressionAttributeValues'] = serialize(expr_attr_values)
|
attrs['ExpressionAttributeValues'] = serialize(expr_attr_values)
|
||||||
|
|
||||||
if not table_name:
|
|
||||||
table_name = self._table_name
|
|
||||||
|
|
||||||
self._add_op_and_process(
|
self._add_op_and_process(
|
||||||
TransactOperation(
|
TransactOperation(
|
||||||
{
|
{
|
||||||
'Delete': dict(
|
'Delete': dict(
|
||||||
TableName=table_name,
|
TableName=table_name or self._table_name,
|
||||||
Key=serialize(key),
|
Key=serialize(key),
|
||||||
**attrs,
|
**attrs,
|
||||||
)
|
)
|
||||||
@@ -545,14 +528,11 @@ class TransactWriter:
|
|||||||
if expr_attr_values:
|
if expr_attr_values:
|
||||||
attrs['ExpressionAttributeValues'] = serialize(expr_attr_values)
|
attrs['ExpressionAttributeValues'] = serialize(expr_attr_values)
|
||||||
|
|
||||||
if not table_name:
|
|
||||||
table_name = self._table_name
|
|
||||||
|
|
||||||
self._add_op_and_process(
|
self._add_op_and_process(
|
||||||
TransactOperation(
|
TransactOperation(
|
||||||
{
|
{
|
||||||
'ConditionCheck': dict(
|
'ConditionCheck': dict(
|
||||||
TableName=table_name,
|
TableName=table_name or self._table_name,
|
||||||
Key=serialize(key),
|
Key=serialize(key),
|
||||||
**attrs,
|
**attrs,
|
||||||
)
|
)
|
||||||
@@ -619,6 +599,7 @@ class DynamoDBPersistenceLayer:
|
|||||||
filter_expr: str | None = None,
|
filter_expr: str | None = None,
|
||||||
limit: int | None = None,
|
limit: int | None = None,
|
||||||
index_forward: bool = True,
|
index_forward: bool = True,
|
||||||
|
table_name: str | None = None,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""You must provide the name of the partition key attribute
|
"""You must provide the name of the partition key attribute
|
||||||
and a single value for that 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
|
- https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/dynamodb/client/query.html
|
||||||
"""
|
"""
|
||||||
attrs: dict = {
|
attrs: dict = {
|
||||||
'TableName': self.table_name,
|
'TableName': table_name or self.table_name,
|
||||||
'KeyConditionExpression': key_cond_expr,
|
'KeyConditionExpression': key_cond_expr,
|
||||||
'ScanIndexForward': index_forward,
|
'ScanIndexForward': index_forward,
|
||||||
}
|
}
|
||||||
@@ -658,18 +639,18 @@ class DynamoDBPersistenceLayer:
|
|||||||
attrs['Limit'] = limit
|
attrs['Limit'] = limit
|
||||||
|
|
||||||
try:
|
try:
|
||||||
response = self.client.query(**attrs)
|
r = self.client.query(**attrs)
|
||||||
except ClientError as err:
|
except ClientError as err:
|
||||||
logger.info(attrs)
|
logger.info(attrs)
|
||||||
logger.exception(err)
|
logger.exception(err)
|
||||||
raise
|
raise
|
||||||
else:
|
else:
|
||||||
return dict(
|
return dict(
|
||||||
items=[deserialize(v) for v in response.get('Items', [])],
|
items=[deserialize(v) for v in r.get('Items', [])],
|
||||||
last_key=response.get('LastEvaluatedKey', None),
|
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
|
"""The GetItem operation returns a set of attributes for the item
|
||||||
with the given primary key.
|
with the given primary key.
|
||||||
|
|
||||||
@@ -677,22 +658,28 @@ class DynamoDBPersistenceLayer:
|
|||||||
there will be no Item element in the response.
|
there will be no Item element in the response.
|
||||||
"""
|
"""
|
||||||
attrs = {
|
attrs = {
|
||||||
'TableName': self.table_name,
|
'TableName': table_name or self.table_name,
|
||||||
'Key': serialize(key),
|
'Key': serialize(key),
|
||||||
}
|
}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
response = self.client.get_item(**attrs)
|
r = self.client.get_item(**attrs)
|
||||||
except ClientError as err:
|
except ClientError as err:
|
||||||
logger.info(attrs)
|
logger.info(attrs)
|
||||||
logger.exception(err)
|
logger.exception(err)
|
||||||
raise
|
raise
|
||||||
else:
|
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 = {
|
attrs = {
|
||||||
'TableName': self.table_name,
|
'TableName': table_name or self.table_name,
|
||||||
'Item': serialize(item),
|
'Item': serialize(item),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -716,9 +703,10 @@ class DynamoDBPersistenceLayer:
|
|||||||
cond_expr: str | None = None,
|
cond_expr: str | None = None,
|
||||||
expr_attr_names: dict | None = None,
|
expr_attr_names: dict | None = None,
|
||||||
expr_attr_values: dict | None = None,
|
expr_attr_values: dict | None = None,
|
||||||
|
table_name: str | None = None,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
attrs: dict = {
|
attrs: dict = {
|
||||||
'TableName': self.table_name,
|
'TableName': table_name or self.table_name,
|
||||||
'Key': serialize(key),
|
'Key': serialize(key),
|
||||||
'UpdateExpression': update_expr,
|
'UpdateExpression': update_expr,
|
||||||
}
|
}
|
||||||
@@ -748,13 +736,14 @@ class DynamoDBPersistenceLayer:
|
|||||||
cond_expr: str | None = None,
|
cond_expr: str | None = None,
|
||||||
expr_attr_names: dict | None = None,
|
expr_attr_names: dict | None = None,
|
||||||
expr_attr_values: dict | None = None,
|
expr_attr_values: dict | None = None,
|
||||||
|
table_name: str | None = None,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""Deletes a single item in a table by primary key. You can perform
|
"""Deletes a single item in a table by primary key. You can perform
|
||||||
a conditional delete operation that deletes the item if it exists,
|
a conditional delete operation that deletes the item if it exists,
|
||||||
or if it has an expected attribute value.
|
or if it has an expected attribute value.
|
||||||
"""
|
"""
|
||||||
attrs: dict = {
|
attrs: dict = {
|
||||||
'TableName': self.table_name,
|
'TableName': table_name or self.table_name,
|
||||||
'Key': serialize(key),
|
'Key': serialize(key),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -780,9 +769,13 @@ class DynamoDBPersistenceLayer:
|
|||||||
def collection(self) -> 'DynamoDBCollection':
|
def collection(self) -> 'DynamoDBCollection':
|
||||||
return DynamoDBCollection(self)
|
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(
|
return TransactWriter(
|
||||||
table_name=self.table_name,
|
table_name=table_name or self.table_name,
|
||||||
client=self.client,
|
client=self.client,
|
||||||
flush_amount=flush_amount,
|
flush_amount=flush_amount,
|
||||||
)
|
)
|
||||||
@@ -913,19 +906,20 @@ class DynamoDBCollection:
|
|||||||
Raises the provided exception if the item is not found
|
Raises the provided exception if the item is not found
|
||||||
and raise_on_error is True.
|
and raise_on_error is True.
|
||||||
"""
|
"""
|
||||||
exc_cls = exc_cls or self.exc_cls
|
table_name = getattr(key, 'table_name', None)
|
||||||
data = self.persistence_layer.get_item(key)
|
|
||||||
path_spec = getattr(key[SK], 'path_spec', 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.')
|
raise exc_cls(f'Item with {key} not found.')
|
||||||
|
|
||||||
if path_spec and data:
|
if path_spec and r:
|
||||||
from glom import glom
|
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(
|
def put_item(
|
||||||
self,
|
self,
|
||||||
@@ -954,6 +948,8 @@ class DynamoDBCollection:
|
|||||||
bool
|
bool
|
||||||
True if the operation is successful, False otherwise.
|
True if the operation is successful, False otherwise.
|
||||||
"""
|
"""
|
||||||
|
table_name = getattr(key, 'table_name', None)
|
||||||
|
|
||||||
if isinstance(ttl, int):
|
if isinstance(ttl, int):
|
||||||
kwargs.update({'ttl': ttl})
|
kwargs.update({'ttl': ttl})
|
||||||
|
|
||||||
@@ -963,6 +959,7 @@ class DynamoDBCollection:
|
|||||||
return self.persistence_layer.put_item(
|
return self.persistence_layer.put_item(
|
||||||
item=key | kwargs,
|
item=key | kwargs,
|
||||||
cond_expr=cond_expr,
|
cond_expr=cond_expr,
|
||||||
|
table_name=table_name,
|
||||||
)
|
)
|
||||||
|
|
||||||
def delete_item(
|
def delete_item(
|
||||||
@@ -991,11 +988,14 @@ class DynamoDBCollection:
|
|||||||
bool
|
bool
|
||||||
True if the item is successfully deleted, False otherwise.
|
True if the item is successfully deleted, False otherwise.
|
||||||
"""
|
"""
|
||||||
|
table_name = getattr(key, 'table_name', None)
|
||||||
|
|
||||||
return self.persistence_layer.delete_item(
|
return self.persistence_layer.delete_item(
|
||||||
key=key,
|
key=key,
|
||||||
cond_expr=cond_expr,
|
cond_expr=cond_expr,
|
||||||
expr_attr_names=expr_attr_names,
|
expr_attr_names=expr_attr_names,
|
||||||
expr_attr_values=expr_attr_values,
|
expr_attr_values=expr_attr_values,
|
||||||
|
table_name=table_name,
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_items(
|
def get_items(
|
||||||
@@ -1059,7 +1059,7 @@ class DynamoDBCollection:
|
|||||||
if not key.pairs:
|
if not key.pairs:
|
||||||
return {}
|
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
|
client = self.persistence_layer.client
|
||||||
table_name = self.persistence_layer.table_name
|
table_name = self.persistence_layer.table_name
|
||||||
|
|
||||||
@@ -1074,8 +1074,8 @@ class DynamoDBCollection:
|
|||||||
for pair in key.pairs
|
for pair in key.pairs
|
||||||
]
|
]
|
||||||
|
|
||||||
response = client.transact_get_items(TransactItems=transact_items) # type: ignore
|
r = client.transact_get_items(TransactItems=transact_items) # type: ignore
|
||||||
items = [deserialize(r.get('Item', {})) for r in response.get('Responses', [])]
|
items = [deserialize(r.get('Item', {})) for r in r.get('Responses', [])]
|
||||||
|
|
||||||
if flatten_top:
|
if flatten_top:
|
||||||
head, *tail = items
|
head, *tail = items
|
||||||
@@ -1103,16 +1103,14 @@ class DynamoDBCollection:
|
|||||||
if getattr(sk, 'rename_key', None):
|
if getattr(sk, 'rename_key', None):
|
||||||
return sk.rename_key
|
return sk.rename_key
|
||||||
|
|
||||||
if not isinstance(sk, SortKey):
|
if isinstance(sk, SortKey):
|
||||||
|
return sk.removeprefix(sk.remove_prefix or '')
|
||||||
|
|
||||||
return pk
|
return pk
|
||||||
|
|
||||||
key = pk if pair.retain_key else sk
|
|
||||||
|
|
||||||
return key.removeprefix(sk.remove_prefix or '')
|
|
||||||
|
|
||||||
return head | {
|
return head | {
|
||||||
_map_key(pair): _extract_sk_values(pair, obj)
|
_map_key(pair): _extract_sk_values(pair, obj)
|
||||||
for pair, obj in zip(sortkeys, tail)
|
for pair, obj in zip(pairs, tail)
|
||||||
if obj
|
if obj
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1179,7 +1177,7 @@ class DynamoDBCollection:
|
|||||||
else '#pk = :pk'
|
else '#pk = :pk'
|
||||||
)
|
)
|
||||||
|
|
||||||
response = self.persistence_layer.query(
|
r = self.persistence_layer.query(
|
||||||
key_cond_expr=key_cond_expr,
|
key_cond_expr=key_cond_expr,
|
||||||
expr_attr_name=key.expr_attr_name() | expr_attr_name,
|
expr_attr_name=key.expr_attr_name() | expr_attr_name,
|
||||||
expr_attr_values=key.expr_attr_values() | expr_attr_values,
|
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 {},
|
start_key=_startkey_b64decode(start_key) if start_key else {},
|
||||||
)
|
)
|
||||||
|
|
||||||
items = response['items']
|
items = r['items']
|
||||||
last_key = (
|
last_key = _startkey_b64encode(r['last_key']) if r['last_key'] else None
|
||||||
_startkey_b64encode(response['last_key']) if response['last_key'] else None
|
|
||||||
)
|
|
||||||
|
|
||||||
def _removeprefix(
|
def _removeprefix(
|
||||||
items: list[dict[str, Any]], /, key: str, prefix: str
|
items: list[dict[str, Any]], /, key: str, prefix: str
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "layercake"
|
name = "layercake"
|
||||||
version = "0.10.1"
|
version = "0.11.0"
|
||||||
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 = [
|
||||||
|
|||||||
@@ -178,7 +178,7 @@ def test_collection_get_item_path_spec(
|
|||||||
KeyPair(
|
KeyPair(
|
||||||
pk='5OxmMjL-ujoR5IMGegQz',
|
pk='5OxmMjL-ujoR5IMGegQz',
|
||||||
sk=SortKey(
|
sk=SortKey(
|
||||||
ComposeKey('sergio@somosbeta.com.br', prefix='emails'),
|
'emails#sergio@somosbeta.com.br',
|
||||||
path_spec='mx_record_exists',
|
path_spec='mx_record_exists',
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -193,7 +193,7 @@ def test_collection_put_item(
|
|||||||
assert dynamodb_persistence_layer.collection.put_item(
|
assert dynamodb_persistence_layer.collection.put_item(
|
||||||
KeyPair(
|
KeyPair(
|
||||||
'5OxmMjL-ujoR5IMGegQz',
|
'5OxmMjL-ujoR5IMGegQz',
|
||||||
ComposeKey('6d1044d5-18c5-437c-9219-fc2ace7e5ebc', prefix='orgs'),
|
'orgs#6d1044d5-18c5-437c-9219-fc2ace7e5ebc',
|
||||||
),
|
),
|
||||||
name='Beta Educação',
|
name='Beta Educação',
|
||||||
ttl=ttl(days=3),
|
ttl=ttl(days=3),
|
||||||
@@ -202,7 +202,7 @@ def test_collection_put_item(
|
|||||||
data = dynamodb_persistence_layer.collection.get_item(
|
data = dynamodb_persistence_layer.collection.get_item(
|
||||||
KeyPair(
|
KeyPair(
|
||||||
pk='5OxmMjL-ujoR5IMGegQz',
|
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(
|
assert dynamodb_persistence_layer.collection.delete_item(
|
||||||
KeyPair(
|
KeyPair(
|
||||||
'5OxmMjL-ujoR5IMGegQz',
|
'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(
|
logs = dynamodb_persistence_layer.collection.query(
|
||||||
PartitionKey(
|
PartitionKey(
|
||||||
ComposeKey('5OxmMjL-ujoR5IMGegQz', prefix='logs'),
|
ComposeKey('5OxmMjL-ujoR5IMGegQz', prefix='logs'),
|
||||||
|
# 'logs#5OxmMjL-ujoR5IMGegQz',
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
assert len(logs['items']) == 2
|
assert len(logs['items']) == 2
|
||||||
@@ -280,7 +281,6 @@ def test_collection_get_items(
|
|||||||
'cJtK9SsnJhKPyxESe7g3DG', table_name=dynamodb_persistence_layer.table_name
|
'cJtK9SsnJhKPyxESe7g3DG', table_name=dynamodb_persistence_layer.table_name
|
||||||
)
|
)
|
||||||
+ SortKey('0')
|
+ SortKey('0')
|
||||||
+ SortKey('0')
|
|
||||||
+ SortKey(
|
+ SortKey(
|
||||||
'metadata#billing_policy',
|
'metadata#billing_policy',
|
||||||
path_spec='payment_method',
|
path_spec='payment_method',
|
||||||
@@ -407,7 +407,7 @@ def test_collection_get_items_pair_path_spec(
|
|||||||
+ KeyPair(
|
+ KeyPair(
|
||||||
'email',
|
'email',
|
||||||
SortKey('osergiosiqueira@gmail.com', path_spec='user_id'),
|
SortKey('osergiosiqueira@gmail.com', path_spec='user_id'),
|
||||||
retain_key=True,
|
rename_key='email',
|
||||||
),
|
),
|
||||||
flatten_top=False,
|
flatten_top=False,
|
||||||
)
|
)
|
||||||
|
|||||||
2
layercake/uv.lock
generated
2
layercake/uv.lock
generated
@@ -675,7 +675,7 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "layercake"
|
name = "layercake"
|
||||||
version = "0.10.0"
|
version = "0.10.1"
|
||||||
source = { editable = "." }
|
source = { editable = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "arnparse" },
|
{ name = "arnparse" },
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ order_layer = DynamoDBPersistenceLayer(ORDER_TABLE, dynamodb_client)
|
|||||||
def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool:
|
def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool:
|
||||||
new_image = event.detail['new_image']
|
new_image = event.detail['new_image']
|
||||||
now_ = now()
|
now_ = now()
|
||||||
data = user_layer.collection.get_items(
|
r = user_layer.collection.get_items(
|
||||||
KeyPair(
|
KeyPair(
|
||||||
pk='cnpj',
|
pk='cnpj',
|
||||||
sk=SortKey(new_image['cnpj'], path_spec='user_id'),
|
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,
|
# Sometimes the function executes before the user insertion completes,
|
||||||
# so an exception is raised to trigger a retry.
|
# so an exception is raised to trigger a retry.
|
||||||
if len(data) < 2:
|
if len(r) < 2:
|
||||||
raise ValueError('IDs not found')
|
raise ValueError('IDs not found')
|
||||||
|
|
||||||
logger.info('IDs found', data=data)
|
logger.info('IDs found', result=r)
|
||||||
|
|
||||||
with order_layer.transact_writer() as transact:
|
with order_layer.transact_writer() as transact:
|
||||||
transact.update(
|
transact.update(
|
||||||
@@ -52,7 +52,7 @@ def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool:
|
|||||||
# Post-migration: uncomment the following line
|
# Post-migration: uncomment the following line
|
||||||
# update_expr='SET org_id = :org_id, updated_at = :updated_at',
|
# update_expr='SET org_id = :org_id, updated_at = :updated_at',
|
||||||
expr_attr_values={
|
expr_attr_values={
|
||||||
':org_id': data['org_id'],
|
':org_id': r['org_id'],
|
||||||
':updated_at': now_,
|
':updated_at': now_,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -61,7 +61,7 @@ def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool:
|
|||||||
key=KeyPair(new_image['id'], 'author'),
|
key=KeyPair(new_image['id'], 'author'),
|
||||||
update_expr='SET user_id = :user_id, updated_at = :updated_at',
|
update_expr='SET user_id = :user_id, updated_at = :updated_at',
|
||||||
expr_attr_values={
|
expr_attr_values={
|
||||||
':user_id': data['user_id'],
|
':user_id': r['user_id'],
|
||||||
':updated_at': now_,
|
':updated_at': now_,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -23,10 +23,10 @@ def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool:
|
|||||||
new_image = event.detail['new_image']
|
new_image = event.detail['new_image']
|
||||||
order_id = new_image['id']
|
order_id = new_image['id']
|
||||||
org_id = new_image['tenant_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']
|
# org_id = new_image['org_id']
|
||||||
|
|
||||||
result = enrollment_layer.collection.query(
|
r = enrollment_layer.collection.query(
|
||||||
KeyPair(
|
KeyPair(
|
||||||
# Post-migration: Uncomment the following line
|
# Post-migration: Uncomment the following line
|
||||||
# f'SLOT#ORG#{org_id}',
|
# f'SLOT#ORG#{org_id}',
|
||||||
@@ -38,12 +38,12 @@ def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool:
|
|||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
'Slots found',
|
'Slots found',
|
||||||
total_items=len(result['items']),
|
total_items=len(r['items']),
|
||||||
slots=result['items'],
|
slots=r['items'],
|
||||||
)
|
)
|
||||||
|
|
||||||
with enrollment_layer.batch_writer() as batch:
|
with enrollment_layer.batch_writer() as batch:
|
||||||
for pair in result['items']:
|
for pair in r['items']:
|
||||||
batch.delete_item(
|
batch.delete_item(
|
||||||
Key={
|
Key={
|
||||||
'id': {'S': pair['id']},
|
'id': {'S': pair['id']},
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ Globals:
|
|||||||
Architectures:
|
Architectures:
|
||||||
- x86_64
|
- x86_64
|
||||||
Layers:
|
Layers:
|
||||||
- !Sub arn:aws:lambda:sa-east-1:336641857101:layer:layercake:98
|
- !Sub arn:aws:lambda:sa-east-1:336641857101:layer:layercake:99
|
||||||
Environment:
|
Environment:
|
||||||
Variables:
|
Variables:
|
||||||
TZ: America/Sao_Paulo
|
TZ: America/Sao_Paulo
|
||||||
|
|||||||
2
orders-events/uv.lock
generated
2
orders-events/uv.lock
generated
@@ -576,7 +576,7 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "layercake"
|
name = "layercake"
|
||||||
version = "0.10.1"
|
version = "0.11.0"
|
||||||
source = { directory = "../layercake" }
|
source = { directory = "../layercake" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "arnparse" },
|
{ name = "arnparse" },
|
||||||
|
|||||||
Reference in New Issue
Block a user