fix retain key

This commit is contained in:
2025-05-23 10:30:54 -03:00
parent 812470aae4
commit a7ee787378
26 changed files with 117 additions and 166 deletions

View File

@@ -2,7 +2,7 @@ version = 0.1
[default.deploy.parameters]
stack_name = "saladeaula-batch-jobs"
resolve_s3 = true
s3_prefix = "batchjobs"
s3_prefix = "batch_jobs"
region = "sa-east-1"
confirm_changeset = false
capabilities = "CAPABILITY_IAM"

View File

@@ -9,7 +9,7 @@ Globals:
Architectures:
- x86_64
Layers:
- !Sub arn:aws:lambda:sa-east-1:336641857101:layer:layercake:53
- !Sub arn:aws:lambda:sa-east-1:336641857101:layer:layercake:55
Environment:
Variables:
TZ: America/Sao_Paulo

View File

@@ -16,13 +16,11 @@
<h1>{{ name }}</h1>
<p>
Portador(a) do CPF <strong>{{ cpf }} </strong>, concluiu o curso
de <strong>{{ course }}</strong> com aproveitamento de
de <strong>NR-10 Complementar (SEP)</strong> com aproveitamento
de
<strong>{{ progress }}%</strong>
</p>
<p>
Realizado entre {{ start_date }} e {{ finish_date }}, com
validade até {{ due_date }}
</p>
<p>Realizado entre {{ start_date }} e {{ finish_date }}</p>
<p>Florianópolis, SC, {{ today }}</p>
</section>

View File

@@ -5,10 +5,6 @@
html,
body,
div,
span,
applet,
object,
iframe,
h1,
h2,
h3,
@@ -16,73 +12,7 @@ h4,
h5,
h6,
p,
blockquote,
pre,
a,
abbr,
acronym,
address,
big,
cite,
code,
del,
dfn,
em,
img,
ins,
kbd,
q,
s,
samp,
small,
strike,
strong,
sub,
sup,
tt,
var,
b,
u,
i,
center,
dl,
dt,
dd,
ol,
ul,
li,
fieldset,
form,
label,
legend,
table,
caption,
tbody,
tfoot,
thead,
tr,
th,
td,
article,
aside,
canvas,
details,
embed,
figure,
figcaption,
footer,
header,
hgroup,
menu,
nav,
output,
ruby,
section,
summary,
time,
mark,
audio,
video {
a {
margin: 0;
padding: 0;
border: 0;

View File

@@ -1,9 +0,0 @@
# If UV does not load this file, try running `export UV_ENV_FILE=.env`
# See more details at https://docs.astral.sh/uv/configuration/files/#env
KONVIVA_API_URL=https://saladeaula.digital
KONVIVA_SECRET_KEY=
MEILISEARCH_HOST=
MEILISEARCH_API_KEY=
DYNAMODB_PARTITION_KEY=id
DYNAMODB_SORT_KEY=sk

View File

@@ -26,7 +26,6 @@ Example
from dataclasses import asdict, dataclass
from typing import Any
import boto3
from aws_lambda_powertools import Logger, Tracer
from aws_lambda_powertools.utilities.data_classes import event_source
from aws_lambda_powertools.utilities.data_classes.api_gateway_authorizer_event import (
@@ -38,7 +37,7 @@ from botocore.endpoint_provider import Enum
from layercake.dynamodb import DynamoDBCollection, DynamoDBPersistenceLayer, KeyPair
from layercake.funcs import pick
from boto3clients import dynamodb_client
from boto3clients import dynamodb_client, idp_client
from cognito import get_user
from conf import USER_TABLE
@@ -46,7 +45,6 @@ APIKEY_PREFIX = 'sk-'
tracer = Tracer()
logger = Logger(__name__)
idp_client = boto3.client('cognito-idp')
user_layer = DynamoDBPersistenceLayer(USER_TABLE, dynamodb_client)
user_collect = DynamoDBCollection(user_layer)

View File

@@ -2,15 +2,16 @@ import os
import boto3
DYNAMODB_ENDPOINT_URL: str | None = None
# Only when running `sam local start-api`
if 'AWS_SAM_LOCAL' in os.environ:
DYNAMODB_ENDPOINT_URL = 'http://host.docker.internal:8000'
def get_dynamodb_client():
sam_local = os.getenv('AWS_SAM_LOCAL')
# Only when running `pytest`
if 'PYTEST_VERSION' in os.environ:
DYNAMODB_ENDPOINT_URL = 'http://127.0.0.1:8000'
if os.getenv('AWS_LAMBDA_FUNCTION_NAME') and not sam_local:
return boto3.client('dynamodb')
dynamodb_client = boto3.client('dynamodb', endpoint_url=DYNAMODB_ENDPOINT_URL)
url = 'host.docker.internal' if sam_local else 'localhost'
return boto3.client('dynamodb', endpoint_url=f'http://{url}:8000')
dynamodb_client = get_dynamodb_client()
idp_client = boto3.client('cognito-idp')

View File

@@ -2,9 +2,9 @@ from dataclasses import asdict, dataclass
from urllib.parse import quote as urlquote
from urllib.parse import urlencode, urlparse
import requests
from aws_lambda_powertools.event_handler.exceptions import BadRequestError
from glom import glom
import requests
from conf import KONVIVA_API_URL, KONVIVA_SECRET_KEY

View File

@@ -119,12 +119,12 @@ def _tenant(
# Ensure user has ACL
collect.get_item(
KeyPair(user.id, ComposeKey(tenant_id, prefix='acls')),
exception_cls=ForbiddenError,
exc_cls=ForbiddenError,
)
# For root tenant, provide the default Tenant
if tenant_id == '*':
return Tenant(id=tenant_id, name='default')
obj = collect.get_item(KeyPair(tenant_id, '0'), exception_cls=NotFoundError)
return Tenant.parse_obj(obj)
obj = collect.get_item(KeyPair(tenant_id, '0'), exc_cls=NotFoundError)
return Tenant.model_validate(obj)

View File

@@ -76,7 +76,7 @@ def post_course(payload: Course):
def get_course(id: str):
return course_collect.get_item(
KeyPair(id, '0'),
exception_cls=NotFoundError,
exc_cls=NotFoundError,
)

View File

@@ -17,7 +17,7 @@ from conf import ELASTIC_CONN, ORDER_TABLE
router = Router()
order_layer = DynamoDBPersistenceLayer(ORDER_TABLE, dynamodb_client)
order_collect = DynamoDBCollection(order_layer, exception_cls=BadRequestError)
order_collect = DynamoDBCollection(order_layer, exc_cls=BadRequestError)
elastic_client = Elasticsearch(**ELASTIC_CONN)

View File

@@ -20,7 +20,7 @@ from rules.org import update_policies
router = Router()
org_layer = DynamoDBPersistenceLayer(USER_TABLE, dynamodb_client)
org_collect = DynamoDBCollection(org_layer, exception_cls=BadRequestError)
org_collect = DynamoDBCollection(org_layer, exc_cls=BadRequestError)
@router.get(
@@ -31,7 +31,9 @@ org_collect = DynamoDBCollection(org_layer, exception_cls=BadRequestError)
)
def get_policies(id: str):
return org_collect.get_items(
TransactKey(id) + SortKey('billing_policy') + SortKey('payment_policy'),
TransactKey(id)
+ SortKey('metadata#billing_policy', remove_prefix='metadata#')
+ SortKey('metadata#payment_policy', remove_prefix='metadata#'),
flatten_top=False,
)

View File

@@ -39,7 +39,7 @@ class BadRequestError(MissingError, PowertoolsBadRequestError):
router = Router()
user_layer = DynamoDBPersistenceLayer(USER_TABLE, dynamodb_client)
user_collect = DynamoDBCollection(user_layer, exception_cls=BadRequestError)
user_collect = DynamoDBCollection(user_layer, exc_cls=BadRequestError)
elastic_client = Elasticsearch(**ELASTIC_CONN)

View File

@@ -25,7 +25,7 @@ class BadRequestError(MissingError, PowertoolsBadRequestError): ...
router = Router()
user_layer = DynamoDBPersistenceLayer(USER_TABLE, dynamodb_client)
user_collect = DynamoDBCollection(user_layer, exception_cls=BadRequestError)
user_collect = DynamoDBCollection(user_layer, exc_cls=BadRequestError)
@router.get(

View File

@@ -23,7 +23,7 @@ class BadRequestError(MissingError, PowertoolsBadRequestError): ...
router = Router()
user_layer = DynamoDBPersistenceLayer(USER_TABLE, dynamodb_client)
user_collect = DynamoDBCollection(user_layer, exception_cls=BadRequestError)
user_collect = DynamoDBCollection(user_layer, exc_cls=BadRequestError)
@router.get(

View File

@@ -26,7 +26,7 @@ class BadRequestError(MissingError, PowertoolsBadRequestError): ...
router = Router()
user_layer = DynamoDBPersistenceLayer(USER_TABLE, dynamodb_client)
user_collect = DynamoDBCollection(user_layer, exception_cls=BadRequestError)
user_collect = DynamoDBCollection(user_layer, exc_cls=BadRequestError)
@router.get(

View File

@@ -23,7 +23,7 @@ def create_course(
transact.put(
item={
'id': course.id,
'sk': 'tenant',
'sk': 'metadata#tenant',
'org_id': org.id,
'name': org.name,
'create_date': now_,
@@ -42,7 +42,8 @@ def update_course(
transact = TransactItems(persistence_layer.table_name)
transact.update(
key=KeyPair(id, '0'),
update_expr='SET #name = :name, access_period = :access_period, cert = :cert, update_date = :update_date',
update_expr='SET #name = :name, access_period = :access_period, \
cert = :cert, update_date = :update_date',
expr_attr_names={
'#name': 'name',
},

View File

@@ -17,24 +17,24 @@ def update_policies(
transact.put(
item={
'id': id,
'sk': 'payment_policy',
'sk': 'metadata#payment_policy',
'create_date': now_,
}
| payment_policy
)
else:
transact.delete(key=KeyPair(id, 'payment_policy'))
transact.delete(key=KeyPair(id, 'metadata#payment_policy'))
if billing_policy:
transact.put(
item={
'id': id,
'sk': 'billing_policy',
'sk': 'metadata#billing_policy',
'create_date': now_,
}
| billing_policy
)
else:
transact.delete(key=KeyPair(id, 'billing_policy'))
transact.delete(key=KeyPair(id, 'metadata#billing_policy'))
return persistence_layer.transact_write_items(transact)

View File

@@ -4,7 +4,6 @@ from typing import TypedDict
from aws_lambda_powertools.event_handler.exceptions import (
BadRequestError,
)
from botocore.exceptions import ClientError
from botocore.tokens import timedelta
from layercake.dateutils import now, ttl
from layercake.dynamodb import (
@@ -14,11 +13,6 @@ from layercake.dynamodb import (
TransactItems,
)
class CPFConflictError(BadRequestError):
pass
User = TypedDict('User', {'id': str, 'name': str, 'cpf': str})
@@ -60,6 +54,10 @@ def update_user(
cond_expr='attribute_not_exists(sk)',
)
class CPFConflictError(BadRequestError):
def __init__(self, msg: str):
super().__init__('Cpf already exists')
if user.cpf != old_cpf:
transact.put(
item={
@@ -69,18 +67,14 @@ def update_user(
'create_date': now_,
},
cond_expr='attribute_not_exists(sk)',
exc_cls=CPFConflictError,
)
# Ensures that the old CPF is discarded
if old_cpf:
transact.delete(key=KeyPair('cpf', old_cpf))
try:
persistence_layer.transact_write_items(transact)
except ClientError:
raise CPFConflictError('CPF is already in use.')
else:
return True
return persistence_layer.transact_write_items(transact)
def add_email(
@@ -109,6 +103,11 @@ def add_email(
},
cond_expr='attribute_not_exists(sk)',
)
class EmailConflictError(BadRequestError):
def __init__(self, msg: str):
super().__init__('Email already exists')
transact.put(
item={
'id': 'email',
@@ -117,12 +116,10 @@ def add_email(
'create_date': now_,
},
cond_expr='attribute_not_exists(sk)',
exc_cls=EmailConflictError,
)
try:
return persistence_layer.transact_write_items(transact)
except ClientError:
raise BadRequestError('Email already exists.')
return persistence_layer.transact_write_items(transact)
def del_email(
@@ -141,6 +138,7 @@ def del_email(
key=KeyPair(id, ComposeKey(email, prefix='emails')),
cond_expr='email_primary <> :primary',
expr_attr_values={':primary': True},
exc_cls=BadRequestError,
)
transact.update(
key=KeyPair(id, '0'),
@@ -150,10 +148,7 @@ def del_email(
},
)
try:
return persistence_layer.transact_write_items(transact)
except ClientError:
raise BadRequestError('Cannot remove the primary email.')
return persistence_layer.transact_write_items(transact)
def set_email_as_primary(
@@ -188,10 +183,8 @@ def set_email_as_primary(
)
transact.update(
key=KeyPair(id, '0'),
update_expr=(
'SET email = :email, email_verified = :email_verified, '
'update_date = :update_date'
),
update_expr='SET email = :email, email_verified = :email_verified, \
update_date = :update_date',
expr_attr_values={
':email': new_email,
':email_verified': email_verified,

View File

@@ -1,9 +1,9 @@
{
"Parameters": {
"KONVIVA_API_KEY": "",
"USER_TABLE": "test-users",
"ORDER_TABLE": "test-orders",
"ENROLLMENT_TABLE": "test-enrollments"
"COURSE_TABLE": "test-courses"
"USER_TABLE": "",
"ORDER_TABLE": "",
"ENROLLMENT_TABLE": ""
"COURSE_TABLE": ""
}
}

View File

@@ -23,7 +23,7 @@ Globals:
Architectures:
- x86_64
Layers:
- !Sub arn:aws:lambda:sa-east-1:336641857101:layer:layercake:48
- !Sub arn:aws:lambda:sa-east-1:336641857101:layer:layercake:59
Environment:
Variables:
TZ: America/Sao_Paulo
@@ -39,7 +39,7 @@ Globals:
ELASTIC_AUTH_PASS: "{{resolve:ssm:/betaeducacao/elastic/auth_pass/str}}"
KONVIVA_API_URL: https://saladeaula.digital
KONVIVA_SECRET_KEY: "{{resolve:ssm:/betaeducacao/konviva/secret_key/str}}"
MEILISEARCH_HOST: https://meili.vps.eduseg.com.br
MEILISEARCH_HOST: https://meili.eduseg.com.br
MEILISEARCH_API_KEY: "{{resolve:ssm:/saladeaula/meili_api_key}}"
Resources:

View File

@@ -6,17 +6,21 @@ from http import HTTPMethod
import jsonlines
import pytest
from layercake.dynamodb import DynamoDBPersistenceLayer
PYTEST_TABLE_NAME = 'pytest'
PK = os.getenv('DYNAMODB_PARTITION_KEY')
SK = os.getenv('DYNAMODB_SORT_KEY')
PK = 'id'
SK = 'sk'
patch = pytest.MonkeyPatch()
patch.setenv('USER_TABLE', PYTEST_TABLE_NAME)
patch.setenv('COURSE_TABLE', PYTEST_TABLE_NAME)
patch.setenv('ENROLLMENT_TABLE', PYTEST_TABLE_NAME)
# https://docs.pytest.org/en/7.1.x/reference/reference.html#pytest.hookspec.pytest_configure
def pytest_configure():
os.environ['TZ'] = 'America/Sao_Paulo'
os.environ['KONVIVA_API_URL'] = 'https://saladeaula.digital'
os.environ['DYNAMODB_PARTITION_KEY'] = PK
os.environ['DYNAMODB_SORT_KEY'] = SK
os.environ['USER_TABLE'] = PYTEST_TABLE_NAME
os.environ['COURSE_TABLE'] = PYTEST_TABLE_NAME
os.environ['ENROLLMENT_TABLE'] = PYTEST_TABLE_NAME
@dataclass
@@ -132,7 +136,9 @@ def dynamodb_client():
@pytest.fixture()
def dynamodb_persistence_layer(dynamodb_client) -> DynamoDBPersistenceLayer:
def dynamodb_persistence_layer(dynamodb_client):
from layercake.dynamodb import DynamoDBPersistenceLayer
return DynamoDBPersistenceLayer(PYTEST_TABLE_NAME, dynamodb_client)

2
http-api/uv.lock generated
View File

@@ -522,7 +522,7 @@ wheels = [
[[package]]
name = "layercake"
version = "0.2.16"
version = "0.3.0"
source = { directory = "../layercake" }
dependencies = [
{ name = "arnparse" },

View File

@@ -146,11 +146,15 @@ if TYPE_CHECKING:
Optional specification for nested data extraction.
remove_prefix: str, optional
Optional prefix to remove from the key when forming the result dict.
retain_key: bool, optional
Use the key itself as value if True; otherwise, use the extracted value.
"""
sk: str
path_spec: str | None = None
remove_prefix: str | None = None
retain_key: bool = False
else:
class SortKey(str):
@@ -166,6 +170,8 @@ else:
Optional specification for nested data extraction.
remove_prefix: str, optional
Optional prefix to remove from the key when forming the result dict.
retain_key: bool, optional
Use the key itself as value if True; otherwise, use the extracted value.
"""
def __new__(
@@ -174,6 +180,7 @@ else:
*,
path_spec: str | None = None,
remove_prefix: str | None = None,
retain_key: bool = False,
) -> str:
return super().__new__(cls, sk)
@@ -183,12 +190,14 @@ else:
*,
path_spec: str | None = None,
remove_prefix: str | None = None,
retain_key: bool = False,
) -> None:
# __init__ is used to store the parameters for later reference.
# For immutable types like str, __init__ cannot change the instance's value.
self.sk = sk
self.path_spec = path_spec
self.remove_prefix = remove_prefix
self.retain_key = retain_key
class Key(ABC, dict):
@@ -929,29 +938,32 @@ class DynamoDBCollection:
else:
head, tail = {}, items
def _getin(pair: KeyPair, v: dict) -> dict:
v = omit((PK, SK), v)
def _getin(pair: KeyPair, obj: dict) -> dict:
obj = omit((PK, SK), obj)
sk = pair[SK]
path_spec = getattr(sk, 'path_spec', None)
if path_spec:
from glom import glom
return glom(v, path_spec)
return v
return glom(obj, path_spec)
return obj
def _removeprefix(pair: KeyPair) -> str:
pk = pair[PK]
sk = pair[SK]
if not isinstance(sk, SortKey):
return pair[PK]
return pk
return sk.removeprefix(sk.remove_prefix or '')
key = pk if sk.retain_key else sk
return key.removeprefix(sk.remove_prefix or '')
return head | {
_removeprefix(pair): _getin(pair, item)
for pair, item in zip(sortkeys, tail)
if item
_removeprefix(pair): _getin(pair, obj)
for pair, obj in zip(sortkeys, tail)
if obj
}
def query(

View File

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

View File

@@ -3,7 +3,6 @@ from decimal import Decimal
from ipaddress import IPv4Address
import pytest
from layercake.dateutils import ttl
from layercake.dynamodb import (
ComposeKey,
@@ -371,3 +370,23 @@ def test_collection_get_items_pair_unflatten(
'cpf': {'user_id': '5OxmMjL-ujoR5IMGegQz'},
'email': {'user_id': '5OxmMjL-ujoR5IMGegQz'},
}
def test_collection_get_items_pair_path_spec(
dynamodb_seeds,
dynamodb_persistence_layer: DynamoDBPersistenceLayer,
):
collect = DynamoDBCollection(dynamodb_persistence_layer)
doc = collect.get_items(
KeyPair('cpf', SortKey('07879819908', path_spec='user_id', retain_key=True))
+ KeyPair(
'email',
SortKey('osergiosiqueira@gmail.com', path_spec='user_id', retain_key=True),
),
flatten_top=False,
)
assert doc == {
'cpf': '5OxmMjL-ujoR5IMGegQz',
'email': '5OxmMjL-ujoR5IMGegQz',
}