This commit is contained in:
2025-04-12 21:04:02 -03:00
parent 1b2ebcfb99
commit 86bdb41216
19 changed files with 259 additions and 60 deletions

View File

@@ -13,3 +13,4 @@ if 'PYTEST_VERSION' in os.environ:
DYNAMODB_ENDPOINT_URL = 'http://127.0.0.1:8000'
dynamodb_client = boto3.client('dynamodb', endpoint_url=DYNAMODB_ENDPOINT_URL)
idp_client = boto3.client('cognito-idp')

View File

@@ -57,10 +57,11 @@ class Elastic:
index: str,
doc: dict,
):
return self.client.index(
return self.client.update(
index=index,
id=id,
document=_serialize_to_basic_types(doc),
doc=_serialize_to_basic_types(doc),
doc_as_upsert=True,
)
def delete_index(self, index: str) -> bool:
@@ -90,12 +91,12 @@ if __name__ == '__main__':
# Populate DynamoDB tables with data from JSONL files
for file in tqdm(jsonl_files, desc='Processing files'):
with jsonlines.open(f'seeds/{file}') as lines:
with open(f'seeds/{file}') as fp:
table_name = file.removesuffix('.jsonl')
reader = jsonlines.Reader(fp)
reader = jsonlines.Reader(fp).iter(skip_invalid=True)
for line in tqdm(lines, desc=f'Processing lines in {file}'):
put_item(line, table_name, dynamodb_client)
for line in tqdm(reader, desc=f'Processing lines in {file}'):
put_item(line, table_name, dynamodb_client) # type: ignore
# Scan DynamoDB tables and index the data into Elasticsearch
for file in tqdm(jsonl_files, desc='Scanning tables'):

View File

@@ -1,3 +1,8 @@
from aws_lambda_powertools import Logger
logger = Logger(__name__)
class UnauthorizedError(Exception):
pass
@@ -10,3 +15,24 @@ def get_user(access_token: str, /, idp_client) -> dict[str, str]:
raise UnauthorizedError()
else:
return {attr['Name']: attr['Value'] for attr in user['UserAttributes']}
def admin_get_user(
sub: str,
user_pool_id: str,
*,
idp_client,
) -> dict[str, str] | None:
"""Gets the specified user by user name in a user pool as an administrator.
Works on any user.
- https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/cognito-idp/client/admin_get_user.html
- https://docs.aws.amazon.com/cognito-user-identity-pools/latest/APIReference/API_AdminGetUser.html
"""
try:
user = idp_client.admin_get_user(Username=sub, UserPoolId=user_pool_id)
except idp_client.exceptions as err:
logger.exception(err)
return None
else:
return user

View File

@@ -10,13 +10,13 @@ def create_course(
/,
persistence_layer: DynamoDBPersistenceLayer,
):
current_time = now()
now_ = now()
transact = TransactItems(persistence_layer.table_name)
transact.put(
item={
'sk': '0',
'tenant__org_id': {org.id},
'create_date': current_time,
'create_date': now_,
**course.model_dump(),
}
)
@@ -26,7 +26,7 @@ def create_course(
'sk': 'tenant',
'org_id': org.id,
'name': org.name,
'create_date': current_time,
'create_date': now_,
}
)
return persistence_layer.transact_write_items(transact)
@@ -38,7 +38,7 @@ def update_course(
/,
persistence_layer: DynamoDBPersistenceLayer,
):
current_time = now()
now_ = now()
transact = TransactItems(persistence_layer.table_name)
transact.update(
key=KeyPair(id, '0'),
@@ -50,7 +50,7 @@ def update_course(
':name': course.name,
':cert': course.cert.model_dump() if course.cert else None,
':access_period': course.access_period,
':update_date': current_time,
':update_date': now_,
},
cond_expr='attribute_exists(sk)',
)

View File

@@ -1,15 +1,28 @@
import json
from typing import Literal
from typing import TypedDict
from aws_lambda_powertools.event_handler.api_gateway import Router
from elasticsearch import Elasticsearch
from pydantic import BaseModel
from layercake.dynamodb import (
DynamoDBCollection,
DynamoDBPersistenceLayer,
SortKey,
TransactKey,
)
from pydantic import UUID4, BaseModel
import elastic
from settings import ELASTIC_CONN, ENROLLMENT_TABLE
from boto3clients import dynamodb_client
from middlewares.audit_log_middleware import AuditLogMiddleware
from middlewares.authorizer_middleware import User
from settings import ELASTIC_CONN, ENROLLMENT_TABLE, USER_TABLE
router = Router()
elastic_client = Elasticsearch(**ELASTIC_CONN)
enrollment_layer = DynamoDBPersistenceLayer(ENROLLMENT_TABLE, dynamodb_client)
user_layer = DynamoDBPersistenceLayer(USER_TABLE, dynamodb_client)
enrollment_collect = DynamoDBCollection(enrollment_layer)
user_collect = DynamoDBCollection(user_layer)
@router.get('/', compress=True, tags=['Enrollment'])
@@ -28,16 +41,44 @@ def get_enrollments():
@router.get('/<id>', compress=True, tags=['Enrollment'])
def get_enrollment(id: str):
return {}
return enrollment_collect.get_items(
TransactKey(id)
+ SortKey('0')
+ SortKey('started_date')
+ SortKey('finished_date')
+ SortKey('failed_date')
+ SortKey('canceled_date')
+ SortKey('archived_date')
+ SortKey('cancel_policy')
+ SortKey('parent_vacancy')
+ SortKey('lock', path_spec='hash')
+ SortKey('author')
+ SortKey('tenant')
+ SortKey('cert')
)
class CancelPayload(BaseModel):
status: Literal['CANCELED'] = 'CANCELED'
class Course(TypedDict):
id: str
name: str
@router.patch('/<id>', compress=True, tags=['Enrollment'])
def cancel(id: str, payload: CancelPayload):
return {}
class Cancel(BaseModel):
id: UUID4 | str
course: Course
@router.patch(
'/<id>/cancel',
compress=True,
tags=['Enrollment'],
middlewares=[
AuditLogMiddleware('ENROLLMENT_CANCEL', user_collect, ('id', 'course'))
],
)
def cancel(id: str, payload: Cancel):
user: User = router.context['user']
return payload
@router.post('/', compress=True, tags=['Enrollment'])

View File

@@ -22,11 +22,11 @@ LIMIT = 25
@router.get('/', include_in_schema=False)
def settings():
user: User = router.context['user']
acls = user_collect.get_items(
acls = user_collect.query(
KeyPair(user.id, PrefixKey('acls')),
limit=LIMIT,
)
tenants = user_collect.get_items(
tenants = user_collect.query(
KeyPair(user.id, PrefixKey('orgs')),
limit=LIMIT,
)

View File

@@ -21,11 +21,12 @@ from layercake.dynamodb import (
)
from pydantic import UUID4, BaseModel, StringConstraints
import cognito
import elastic
from boto3clients import dynamodb_client
from boto3clients import dynamodb_client, idp_client
from middlewares import AuditLogMiddleware
from models import User
from settings import ELASTIC_CONN, USER_TABLE
from settings import ELASTIC_CONN, USER_POOOL_ID, USER_TABLE
class BadRequestError(MissingError, PowertoolsBadRequestError): ...
@@ -37,12 +38,7 @@ user_collect = DynamoDBCollection(user_layer, exception_cls=BadRequestError)
elastic_client = Elasticsearch(**ELASTIC_CONN)
@router.get(
'/',
compress=True,
tags=['User'],
summary='Get users',
)
@router.get('/', compress=True, tags=['User'], summary='Get users')
def get_users():
event = router.current_event
query = event.get_query_string_value('query', '{}')
@@ -67,29 +63,28 @@ def post_user(payload: User):
return Response(status_code=HTTPStatus.CREATED)
class NewPasswordPayload(BaseModel):
class NewPassword(BaseModel):
cognito_sub: UUID4
new_password: Annotated[str, StringConstraints(min_length=6)]
@router.patch('/<id>', compress=True, tags=['User'])
def patch_reset(id: str, payload: NewPasswordPayload):
def patch_newpassword(id: str, payload: NewPassword):
return Response(status_code=HTTPStatus.OK)
@router.get(
'/<id>',
compress=True,
tags=['User'],
summary='Get user',
)
@router.get('/<id>', compress=True, tags=['User'], summary='Get user')
def get_user(id: str):
return user_collect.get_item(KeyPair(id, '0'))
@router.get('/<id>/idp', compress=True, include_in_schema=False)
def get_idp(id: str):
return []
return cognito.admin_get_user(
sub=id,
user_pool_id=USER_POOOL_ID,
idp_client=idp_client,
)
@router.get(
@@ -99,7 +94,7 @@ def get_idp(id: str):
summary='Get user emails',
)
def get_emails(id: str):
return user_collect.get_items(
return user_collect.query(
KeyPair(id, PrefixKey('emails')),
start_key=router.current_event.get_query_string_value('start_key', None),
)
@@ -112,7 +107,7 @@ def get_emails(id: str):
summary='Get user logs',
)
def get_logs(id: str):
return user_collect.get_items(
return user_collect.query(
# Post-migration: uncomment to enable PartitionKey with a composite key (id with `logs` prefix).
# PartitionKey(ComposeKey(id, prefix='logs')),
PartitionKey(ComposeKey(id, prefix='log', delimiter=':')),
@@ -127,7 +122,7 @@ def get_logs(id: str):
summary='Get user orgs',
)
def get_orgs(id: str):
return user_collect.get_items(
return user_collect.query(
KeyPair(id, PrefixKey('orgs')),
start_key=router.current_event.get_query_string_value('start_key', None),
)

View File

@@ -13,6 +13,8 @@
{"id": {"S": "cJtK9SsnJhKPyxESe7g3DG"}, "sk": {"S": "tags#MqiRQWy9cSBEci93aKBvTd"}, "tag": {"S": "Test"}, "create_date": {"S": "2023-09-22T18:31:11.475449-03:00"}}
{"id": {"S": "cJtK9SsnJhKPyxESe7g3DG"}, "sk": {"S": "tags#NQ3YmmTesfoSyHCkPKGKEF"}, "tag": {"S": "F\u00e1brica"}, "create_date": {"S": "2023-09-22T18:31:11.475449-03:00"}}
{"id": {"S": "cJtK9SsnJhKPyxESe7g3DG"}, "sk": {"S": "tags#bg45io8igarjsA4BzPQyrz"}, "tag": {"S": "Escrit\u00f3rio"}, "create_date": {"S": "2023-09-22T18:31:11.475449-03:00"}}
{"id": {"S": "cJtK9SsnJhKPyxESe7g3DG"}, "sk": {"S": "payment_policy"}, "due_days": {"N": "90"}}
{"id": {"S": "cJtK9SsnJhKPyxESe7g3DG"}, "sk": {"S": "billing_policy"}, "billing_day": {"N": "2"}, "payment_method": {"S": "PIX"}}
{"sk": {"S": "admin#18606"}, "create_date": {"S": "2023-09-22T18:31:11.475449-03:00"}, "id": {"S": "cJtK9SsnJhKPyxESe7g3DG"}, "email": {"S": "mateushickmann@petrobras.com.br"}, "name": {"S": "MATEUS LATTIK HICKMANN"}}
{"sk": {"S": "admin#21425"}, "create_date": {"S": "2023-09-22T18:34:06.480230-03:00"}, "id": {"S": "cJtK9SsnJhKPyxESe7g3DG"}, "email": {"S": "sergio@inbep.com.br"}, "name": {"S": "S\u00e9rgio Siqueira"}}
{"sk": {"S": "admin#2338"}, "create_date": {"S": "2023-09-22T18:28:10.121068-03:00"}, "id": {"S": "cJtK9SsnJhKPyxESe7g3DG"}, "email": {"S": "empresa@inbep.com.br"}, "name": {"S": "Empresa Teste"}}
@@ -68,3 +70,6 @@
{"id": {"S": "webhook_requests#*#0e7f4d2e62ec525fc94465a6dd7299d2"}, "sk": {"S": "2025-03-03T15:47:42.039256-03:00"}, "request": {"M": {"id": {"S": "cJtK9SsnJhKPyxESe7g3DG"}, "headers": {"M": {"Accept": {"S": "*/*"}, "Accept-Encoding": {"S": "gzip, deflate"}, "Connection": {"S": "keep-alive"}, "Content-Length": {"S": "108"}, "Content-Type": {"S": "application/json"}, "User-Agent": {"S": "eduseg/python-requests/2.32.3"}}}, "payload": {"M": {"id": {"S": "5OxmMjL-ujoR5IMGegQz"}, "email": {"S": "sergio@somosbeta.com.br"}, "name": {"S": "S\u00e9rgio Rafael de Siqueira"}}}}}, "response": {"M": {"body": {"S": "{\"message\":\"Workflow was started\"}"}, "elapsed_time": {"S": "0.14"}, "headers": {"M": {"alt-svc": {"S": "h3=\":443\"; ma=86400"}, "cf-cache-status": {"S": "DYNAMIC"}, "CF-RAY": {"S": "8fc528b57b62a3f0-GRU"}, "Connection": {"S": "keep-alive"}, "Content-Length": {"S": "34"}, "Content-Type": {"S": "application/json; charset=utf-8"}, "Date": {"S": "Fri, 03 Jan 2025 18:47:44 GMT"}, "etag": {"S": "W/\"22-6OS7cK0FzqnV2NeDHdOSGS1bVUs\""}, "NEL": {"S": "{\"success_fraction\":0,\"report_to\":\"cf-nel\",\"max_age\":604800}"}, "Report-To": {"S": "{\"endpoints\":[{\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=FMkgKCTOnFvNgtE30yiYnM4XtK8q99O62s7Ep57KDNc3YGiy3W1j%2BzfS0vqJNylSAn5viU3MMGJvTIwPsYQ6jQ298t1p0hYd1KPwURhxchME4hc%2BsO0NNAv%2FFEpXv1ZYWw%3D%3D\"}],\"group\":\"cf-nel\",\"max_age\":604800}"}, "Server": {"S": "cloudflare"}, "server-timing": {"S": "cfL4;desc=\"?proto=TCP&rtt=1016&min_rtt=998&rtt_var=411&sent=4&recv=7&lost=0&retrans=0&sent_bytes=2836&recv_bytes=999&delivery_rate=2522648&cwnd=251&unsent_bytes=0&cid=1b26053952909423&ts=93&x=0\""}, "Strict-Transport-Security": {"S": "max-age=0; includeSubDomains"}, "vary": {"S": "Accept-Encoding"}}}, "status_code": {"S": "200"}}}, "update_date": {"S": "2025-01-03T15:47:44.537597-03:00"}, "url": {"S": "https://n8n.sergio.run/webhook/56bb43b8-533c-4e8b-bdaa-3f7c2b0e548f"}}
{"id": {"S": "webhooks#*"}, "sk": {"S": "1e3759c9c4cfa7aaf86ac281bdb8fd6f"}, "author": {"M": {"id": {"S": "5OxmMjL-ujoR5IMGegQz"}, "name": {"S": "S\u00e9rgio Rafael de Siqueira"}}}, "create_date": {"S": "2025-01-06T15:00:39.363207-03:00"}, "event_type": {"S": "insert"}, "resource": {"S": "users"}, "url": {"S": "https://hook.us2.make.com/hgkc5oj5dbpfld5qvu1xmsu22ond93l9"}}
{"id": {"S": "log:5OxmMjL-ujoR5IMGegQz"},"sk": {"S": "2024-02-26T11:48:17.605911-03:00"},"action": {"S": "OPEN_EMAIL"},"data": {"M": {"ip_address": {"S": "66.249.83.73"},"message_id": {"S": "0103018de5de75cf-670bf01f-7ccf-4ce0-88b4-7c85cd0d06d0-000000"},"recipients": {"L": [{"S": "sergio@somosbeta.com.br"}]},"subject": {"S": "Re: (sem assunto)"},"timestamp": {"S": "2024-02-26T14:48:16.976Z"},"user_agent": {"S": "Mozilla/5.0 (Windows NT 5.1; rv:11.0) Gecko Firefox/11.0 (via ggpht.com GoogleImageProxy)"}}},"ttl": {"N": "1772030897"}}
{"id": {"S": "7zf52CWrTS3csRBFWU5rkq"},"sk": {"S": "0"},"cognito:sub": {"S": "58efed8d-d276-41a8-8502-4ab8b5a6415e"},"cpf": {"S": "04330965275"},"createDate": {"S": "2025-04-08T10:24:46.493980-03:00"},"email": {"S": "barbara.gomes@sinobras.com.br"},"email_verified": {"BOOL": true},"konviva:id": {"N": "199205"},"lastLogin": {"S": "2025-04-10T12:01:48.380215-03:00"},"name": {"S": "Barbara Kamyla Vasconcelos Gomes"},"tenant__org_id": {"SS": ["EkvQwpmmL6vzWtJunM5dCJ"]},"update_date": {"S": "2025-04-10T08:50:22.530758-03:00"}}
{"id": {"S": "7zf52CWrTS3csRBFWU5rkq"},"sk": {"S": "acls#EkvQwpmmL6vzWtJunM5dCJ"},"create_date": {"S": "2025-04-10T09:36:41.133157-03:00"},"roles": {"L": [{"S": "ADMIN"}]}}
{"id": {"S": "7zf52CWrTS3csRBFWU5rkq"},"sk": {"S": "orgs#EkvQwpmmL6vzWtJunM5dCJ"},"cnpj": {"S": "07933914000154"},"create_date": {"S": "2025-04-08T10:24:46.493980-03:00"},"name": {"S": "SIDERURGICA NORTE BRASIL S.A"}}

View File

@@ -32,3 +32,5 @@ match os.getenv('AWS_SAM_LOCAL'), os.getenv('PYTEST_VERSION'):
'cloud_id': ELASTIC_CLOUD_ID,
'basic_auth': ('elastic', ELASTIC_AUTH_PASS),
}
USER_POOOL_ID = 'sa-east-1_s6YmVSfXj'

View File

@@ -23,7 +23,7 @@ Globals:
Architectures:
- x86_64
Layers:
- !Sub arn:aws:lambda:sa-east-1:336641857101:layer:layercake:35
- !Sub arn:aws:lambda:sa-east-1:336641857101:layer:layercake:42
Environment:
Variables:
TZ: America/Sao_Paulo
@@ -82,6 +82,15 @@ Resources:
TableName: !Ref CourseTable
- DynamoDBCrudPolicy:
TableName: !Ref OrderTable
- DynamoDBCrudPolicy:
TableName: !Ref EnrollmentTable
- Version: 2012-10-17
Statement:
- Effect: Allow
Action:
- cognito-idp:AdminGetUser
- cognito-idp:AdminSetUserPassword
Resource: !Sub arn:aws:cognito-idp:${AWS::Region}:${AWS::AccountId}:userpool/*
Events:
Preflight:
Type: HttpApi

View File

@@ -59,7 +59,7 @@ def test_post_course(
assert r['statusCode'] == HTTPStatus.CREATED
collect = DynamoDBCollection(dynamodb_persistence_layer)
logs = collect.get_items(
logs = collect.query(
PartitionKey(
ComposeKey('5OxmMjL-ujoR5IMGegQz', prefix='log', delimiter=':'),
)

View File

@@ -8,5 +8,8 @@
{"id": {"S": "log:5OxmMjL-ujoR5IMGegQz"}, "sk": {"S": "2024-02-08T16:42:33.776409-03:00"}, "action": {"S": "OPEN_EMAIL"}}
{"id": {"S": "log:5OxmMjL-ujoR5IMGegQz"}, "sk": {"S": "2019-03-25T00:00:00-03:00"}, "action": {"S": "CLICK_EMAIL"}}
{"id": {"S": "cJtK9SsnJhKPyxESe7g3DG"}, "sk": {"S": "0"}, "name": {"S": "EDUSEG"}, "cnpj": {"S": "15608435000190"}, "email": {"S": "org+15608435000190@users.noreply.betaeducacao.com.br"}}
{"id": {"S": "cJtK9SsnJhKPyxESe7g3DG"}, "sk": {"S": "payment_policies"}, "due_days": {"N": "90"}}
{"id": {"S": "cJtK9SsnJhKPyxESe7g3DG"}, "sk": {"S": "payment_policy"}, "due_days": {"N": "90"}}
{"id": {"S": "cJtK9SsnJhKPyxESe7g3DG"}, "sk": {"S": "billing_policy"}, "billing_day": {"N": "1"}, "payment_method": {"S": "PIX"}}
{"id": {"S": "90d7f0d2-d9a4-4467-a31c-f9a7955964cf"}, "sk": {"S": "0"}, "access_period": {"N": "720"}, "create_date": {"S": "2024-12-30T00:00:33.088916-03:00"},"konviva__class_id": {"N": "266"},"name": {"S": "Reciclagem em NR-18 Básico"},"tenant__org_id": {"SS": ["cJtK9SsnJhKPyxESe7g3DG"]}}
{"id": {"S": "43ea4475-c369-4f90-b576-135b7df5106b"}, "sk": {"S": "0"}, "status": {"S": "PENDING"}}
{"id": {"S": "43ea4475-c369-4f90-b576-135b7df5106b"}, "sk": {"S": "cancel_policy"}}

24
http-api/uv.lock generated
View File

@@ -521,7 +521,7 @@ wheels = [
[[package]]
name = "layercake"
version = "0.1.16"
version = "0.2.5"
source = { directory = "../layercake" }
dependencies = [
{ name = "arnparse" },
@@ -538,6 +538,7 @@ dependencies = [
{ name = "pydantic-extra-types" },
{ name = "pytz" },
{ name = "requests" },
{ name = "uuid-utils" },
{ name = "weasyprint" },
]
@@ -557,6 +558,7 @@ requires-dist = [
{ name = "pydantic-extra-types", specifier = ">=2.10.3" },
{ name = "pytz", specifier = ">=2025.1" },
{ name = "requests", specifier = ">=2.32.3" },
{ name = "uuid-utils", specifier = ">=0.10.0" },
{ name = "weasyprint", specifier = ">=65.0" },
]
@@ -938,6 +940,26 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/c8/19/4ec628951a74043532ca2cf5d97b7b14863931476d117c471e8e2b1eb39f/urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df", size = 128369 },
]
[[package]]
name = "uuid-utils"
version = "0.10.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/66/0a/cbdb2eb4845dafeb632d02a18f47b02f87f2ce4f25266f5e3c017976ce89/uuid_utils-0.10.0.tar.gz", hash = "sha256:5db0e1890e8f008657ffe6ded4d9459af724ab114cfe82af1557c87545301539", size = 18828 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/44/54/9d22fa16b19e5d1676eba510f08a9c458d96e2a62ff2c8ebad64251afb18/uuid_utils-0.10.0-cp39-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:8d5a4508feefec62456cd6a41bcdde458d56827d908f226803b886d22a3d5e63", size = 573006 },
{ url = "https://files.pythonhosted.org/packages/08/8e/f895c6e52aa603e521fbc13b8626ba5dd99b6e2f5a55aa96ba5b232f4c53/uuid_utils-0.10.0-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:dbefc2b9113f9dfe56bdae58301a2b3c53792221410d422826f3d1e3e6555fe7", size = 292543 },
{ url = "https://files.pythonhosted.org/packages/b6/58/cc4834f377a5e97d6e184408ad96d13042308de56643b6e24afe1f6f34df/uuid_utils-0.10.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffc49c33edf87d1ec8112a9b43e4cf55326877716f929c165a2cc307d31c73d5", size = 323340 },
{ url = "https://files.pythonhosted.org/packages/37/e3/6aeddf148f6a7dd7759621b000e8c85382ec83f52ae79b60842d1dc3ab6b/uuid_utils-0.10.0-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0636b6208f69d5a4e629707ad2a89a04dfa8d1023e1999181f6830646ca048a1", size = 329653 },
{ url = "https://files.pythonhosted.org/packages/0c/00/dd6c2164ace70b7b1671d9129267df331481d7d1e5f9c5e6a564f07953f6/uuid_utils-0.10.0-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7bc06452856b724df9dedfc161c3582199547da54aeb81915ec2ed54f92d19b0", size = 365471 },
{ url = "https://files.pythonhosted.org/packages/b4/e7/0ab8080fcae5462a7b5e555c1cef3d63457baffb97a59b9bc7b005a3ecb1/uuid_utils-0.10.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:263b2589111c61decdd74a762e8f850c9e4386fb78d2cf7cb4dfc537054cda1b", size = 325844 },
{ url = "https://files.pythonhosted.org/packages/73/39/52d94e9ef75b03f44b39ffc6ac3167e93e74ef4d010a93d25589d9f48540/uuid_utils-0.10.0-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a558db48b7096de6b4d2d2210d82bba8586a6d55f99106b03bb7d01dc5c5bcd6", size = 344389 },
{ url = "https://files.pythonhosted.org/packages/7c/29/4824566f62666238290d99c62a58e4ab2a8b9cf2eccf94cebd9b3359131e/uuid_utils-0.10.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:807465067f3c892514230326ac71a79b28a8dfe2c88ecd2d5675fc844f3c76b5", size = 510078 },
{ url = "https://files.pythonhosted.org/packages/5e/8f/bbcc7130d652462c685f0d3bd26bb214b754215b476340885a4cb50fb89a/uuid_utils-0.10.0-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:57423d4a2b9d7b916de6dbd75ba85465a28f9578a89a97f7d3e098d9aa4e5d4a", size = 515937 },
{ url = "https://files.pythonhosted.org/packages/23/f8/34e0c00f5f188604d336713e6a020fcf53b10998e8ab24735a39ab076740/uuid_utils-0.10.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:76d8d660f18ff6b767e319b1b5f927350cd92eafa4831d7ef5b57fdd1d91f974", size = 494111 },
{ url = "https://files.pythonhosted.org/packages/1a/52/b7f0066cc90a7a9c28d54061ed195cd617fde822e5d6ac3ccc88509c3c44/uuid_utils-0.10.0-cp39-abi3-win32.whl", hash = "sha256:6c11a71489338837db0b902b75e1ba7618d5d29f05fde4f68b3f909177dbc226", size = 173520 },
{ url = "https://files.pythonhosted.org/packages/8b/15/f04f58094674d333974243fb45d2c740cf4b79186fb707168e57943c84a3/uuid_utils-0.10.0-cp39-abi3-win_amd64.whl", hash = "sha256:11c55ae64f6c0a7a0c741deae8ca2a4eaa11e9c09dbb7bec2099635696034cf7", size = 182965 },
]
[[package]]
name = "wcwidth"
version = "0.2.13"

View File

@@ -1 +0,0 @@
::: layercake.dynamodb

View File

@@ -12,7 +12,6 @@ from uuid import UUID
from aws_lambda_powertools import Logger
from boto3.dynamodb.types import TypeDeserializer, TypeSerializer
from botocore.exceptions import ClientError
from glom import glom
from .dateutils import now, timestamp
from .funcs import omit
@@ -129,12 +128,40 @@ if TYPE_CHECKING:
@dataclass
class SortKey(str):
"""
SortKey encapsulates the sort key value and optionally stores additional attributes
for nested data extraction.
Parameters
----------
sk: str
The sort key value.
table_name: str, optional
Optional name of the table associated with the sort key.
path_spec: str, optional
Optional specification for nested data extraction.
"""
sk: str
table_name: str | None = None
path_spec: str | None = None
else:
class SortKey(str):
"""
SortKey encapsulates the sort key value and optionally stores additional attributes
for nested data extraction.
Parameters
----------
sk: str
The sort key value.
table_name: str, optional
Optional name of the table associated with the sort key.
path_spec: str, optional
Optional specification for nested data extraction.
"""
def __new__(
cls,
sk: str,
@@ -574,6 +601,27 @@ class PaginatedResult(TypedDict):
class DynamoDBCollection:
"""
DynamoDBCollection provides a high-level abstraction for performing common CRUD operations
and queries on a DynamoDB table. It leverages an underlying persistence layer to handle
serialization and deserialization of data, key composition, transaction operations, and TTL management.
This collection class simplifies interaction with DynamoDB items, allowing users to:
- Retrieve a single item or multiple items via transactions.
- Insert (put) items with optional TTL (time-to-live) settings.
- Delete items based on keys and conditions.
- Query items using partition keys or composite key pairs with optional filtering and pagination.
Parameters
----------
persistence_layer: DynamoDBPersistenceLayer
The persistence layer instance responsible for direct DynamoDB operations.
exception_cls: Type[Exception], optional
The exception class to be raised when a requested item is not found.
tz: str, optional
The timezone identifier used for date/time operations.
"""
def __init__(
self,
persistence_layer: DynamoDBPersistenceLayer,
@@ -781,6 +829,8 @@ class DynamoDBCollection:
head, tail = {}, items
def _getin(sk: SortKey, v: dict) -> dict:
from glom import glom
v = omit((PK, SK), v)
return glom(v, sk.path_spec) if sk.path_spec else v

View File

@@ -165,9 +165,11 @@ if TYPE_CHECKING:
CnpjStr = Annotated[str, ...]
else:
class CpfStr(CpfCnpj): ...
class CpfStr(CpfCnpj):
pass
class CnpjStr(CpfCnpj): ...
class CnpjStr(CpfCnpj):
pass
if __name__ == '__main__':

View File

@@ -7,14 +7,56 @@ def pick(
exclude_none: bool = True,
default: Any = None,
) -> dict[str, Any]:
"""Returns a partial copy of an object containing only the keys specified."""
return {
k: dct.get(k, default) for k in keys if k in dct or not exclude_none
}
"""
Return a partial dict with only the specified keys.
Parameters
----------
keys: list[str] or tuple[str, ...]
Keys to select.
dct: dict[str, Any]
Source dict.
exclude_none: bool, optional
If True, omit keys not in dct; if False, include them with `default`.
default: Any, optional
Value for keys not in dct when exclude_none is False.
Returns
-------
dict[str, Any]
A dict with the picked key/value pairs.
Examples
--------
>>> d = {'a': 1, 'b': 2, 'c': 3}
>>> pick(['a', 'c'], d)
{'a': 1, 'c': 3}
>>> pick(['a', 'd'], d, exclude_none=False, default='missing')
{'a': 1, 'd': 'missing'}
"""
return {k: dct.get(k, default) for k in keys if k in dct or not exclude_none}
def omit(
keys: list[str] | tuple[str, ...], dct: dict[str, Any]
) -> dict[str, Any]:
"""Returns a partial copy of an object omitting the keys specified."""
def omit(keys: list[str] | tuple[str, ...], dct: dict[str, Any]) -> dict[str, Any]:
"""
Return a partial dict omitting the specified keys.
Parameters
----------
keys: list[str] or tuple[str, ...]
Keys to omit.
dct: dict[str, Any]
Source dict.
Returns
-------
dict[str, Any]
A dict without the omitted key/value pairs.
Examples
--------
>>> d = {'a': 1, 'b': 2, 'c': 3}
>>> omit(['b'], d)
{'a': 1, 'c': 3}
"""
return {k: dct[k] for k in dct.keys() if k not in keys}

View File

@@ -11,4 +11,5 @@ plugins:
docstring_style: numpy
nav:
- index.md
- API reference: reference.md
- DynamoDB: dynamodb.md
- API: api.md

2
layercake/uv.lock generated
View File

@@ -600,7 +600,7 @@ wheels = [
[[package]]
name = "layercake"
version = "0.2.4"
version = "0.2.5"
source = { editable = "." }
dependencies = [
{ name = "arnparse" },