This commit is contained in:
2025-07-02 19:50:39 -03:00
parent 0a6db8bc48
commit 8901f44ca3
92 changed files with 158 additions and 11387 deletions

Binary file not shown.

View File

Binary file not shown.

View File

@@ -1,53 +0,0 @@
import base64
import io
import locale
from datetime import date
from uuid import uuid4
import qrcode
from jinja2 import Template
from PIL import Image
from weasyprint import HTML
locale.setlocale(locale.LC_TIME, 'pt_BR')
today = date.today()
with open('nr10_complementar_sep.html', encoding='utf-8') as f:
html = f.read()
def cpf_fmt(s: str) -> str:
"""Returns a string as a Brazilian CPF number."""
return '{}.{}.{}-{}'.format(s[:3], s[3:6], s[6:9], s[9:])
qr = qrcode.QRCode(
version=1,
error_correction=qrcode.constants.ERROR_CORRECT_H,
box_size=10,
border=3,
)
qr.add_data('https://eduseg.com.br')
qr.make(fit=True)
img = qr.make_image(fill_color='black', back_color='white')
img = img.resize((120, 120), Image.NEAREST)
buffer = io.BytesIO()
img.save(buffer, format='PNG')
img_str = base64.b64encode(buffer.getvalue()).decode('utf-8')
qrcode_base64 = f'data:image/png;base64,{img_str}'
template = Template(html)
html_rendered = template.render(
id=uuid4(),
name='Sérgio Rafael de Siqueira',
cpf=cpf_fmt('07879819908'),
progress=91.99,
course='NR-10 Complementar (SEP)',
today=today.strftime('%-d de %B de %Y'),
started_date=today.strftime('%d/%m/%Y'),
finished_date=today.strftime('%d/%m/%Y'),
qrcode=qrcode_base64,
)
HTML(string=html_rendered, base_url='').write_pdf('cert.pdf')

View File

@@ -1,251 +0,0 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>NR-10 Complementar (SEP)</title>
<meta name="author" content="EDUSEG® <https://eduseg.com.br>" />
<style>
html,
body,
div,
h1,
h2,
ul,
p,
a {
margin: 0;
padding: 0;
border: 0;
font-size: 100%;
font: inherit;
vertical-align: baseline;
}
@font-face {
font-family: "SF-Pro";
src: url("fonts/SF-Pro.ttf") format("truetype");
}
@page {
size: A4 landscape;
margin: 0;
}
html {
font-family: SF-Pro;
font-size: 13pt;
line-height: 1.4;
}
section {
width: 29.7cm;
height: 21cm;
break-after: page;
box-sizing: border-box;
padding: 5rem;
}
strong {
font-weight: bold;
}
#cover {
background-color: #a7e400;
justify-content: center;
position: relative;
display: flex;
flex-direction: column;
gap: 1rem;
}
#cover h1 {
font-weight: bolder;
font-size: 26pt;
}
#cover .qrcode {
width: 120px;
height: 120px;
background-color: #fff;
position: absolute;
top: 5rem;
right: 5rem;
}
#cover .signatures {
display: flex;
justify-content: space-between;
margin-top: 2.5rem;
}
.sign1 {
width: 250px;
border-top: #000 solid 1px;
}
#back {
background-color: white;
display: flex;
flex-direction: row;
gap: 0.5rem;
}
#back h1,
#back h2 {
font-weight: bold;
font-size: 16pt;
}
#back ul {
padding-left: 1rem;
}
.space-y-0\.5 > :not(:last-child) {
margin-bottom: 0.125rem;
}
.space-y-2\.5 > :not(:last-child) {
margin-bottom: 0.625rem;
}
</style>
</head>
<body>
<section id="cover">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 1072.73 329.6"
style="width: 14rem"
>
<g>
<g>
<path
fill="#8cd366"
d="M152.18,217.62l-68.61-30.91c-1.88-1.2-4.28-1.2-6.16,0l-68.61,30.91c-3.81,2.43-8.8-.3-8.8-4.82V8.98C0,5.82,2.56,3.26,5.72,3.26h149.54c3.16,0,5.72,2.56,5.72,5.72v203.81c0,4.52-5,7.26-8.8,4.82Z"
></path>
<path
fill="#2e3524"
d="M93.97,74.01H26.61v20.16h67.36v-20.16Z"
></path>
<path
fill="#2e3524"
d="M107.44,111.16H26.61v26.8h80.83v-26.8Z"
></path>
<path
fill="#2e3524"
d="M107.44,30.27H26.61v26.8h80.83v-26.8Z"
></path>
<path
fill="#2e3524"
d="M134.38,131.23c0-3.72-3.02-6.73-6.73-6.73s-6.73,3.02-6.73,6.73,3.02,6.73,6.73,6.73,6.73-3.02,6.73-6.73Z"
></path>
</g>
<g>
<path
fill="#f9f7e8"
d="M244.7,3.24h92.33v44.43h-44.15v88.85h39.38v39.62h-39.38v105.77h44.15v44.42h-92.33V3.24Z"
></path>
<path
fill="#f9f7e8"
d="M362.72,3.24h57.79c10.71,0,20.47,2.35,29.29,7.06,8.83,4.7,15.79,11.18,20.87,19.39,5.08,8.21,7.63,17.45,7.63,27.67v214.88c0,10.22-2.48,19.46-7.42,27.67-4.96,8.21-11.83,14.69-20.68,19.39-8.83,4.7-18.73,7.08-29.7,7.08h-57.79V3.24ZM427.55,283.88c1.74-1.87,2.6-4.18,2.6-6.86V52.56c-.26-2.69-1.34-4.97-3.22-6.86-1.88-1.87-4.15-2.83-6.82-2.83h-14v243.85h14.41c2.93,0,5.27-.94,7.01-2.83h.02Z"
></path>
<path
fill="#f9f7e8"
d="M531.5,322.49c-8.71-4.7-15.6-11.16-20.68-19.39-5.08-8.21-7.63-17.42-7.63-27.67V3.24h48.15v279.41c0,2.69.93,4.99,2.82,6.86,1.86,1.9,4.15,2.83,6.82,2.83,2.93,0,5.27-.94,7.01-2.83,1.74-1.87,2.6-4.18,2.6-6.86V3.24h48.16v272.21c0,10.25-2.48,19.46-7.42,27.67-4.96,8.21-11.83,14.69-20.68,19.39-8.85,4.7-18.73,7.08-29.7,7.08s-20.8-2.35-29.5-7.08l.05-.02Z"
></path>
<path
fill="#f9f7e8"
d="M672.79,322.49c-8.7-4.7-15.6-11.16-20.68-19.39-5.08-8.21-7.63-17.42-7.63-27.67v-78.75h48.16v85.95c0,2.69.93,4.99,2.82,6.87,1.86,1.9,4.15,2.83,6.82,2.83,2.93,0,5.27-.94,7.01-2.83,1.74-1.87,2.6-4.18,2.6-6.87v-77.88c0-5.66-2.22-10.3-6.63-13.94-4.41-3.62-11.57-8.02-21.47-13.13-8.3-4.3-15.05-8.14-20.27-11.52-5.22-3.36-9.71-7.87-13.45-13.54-3.75-5.66-5.63-12.24-5.63-19.8V54.12c0-10.22,2.53-19.44,7.63-27.67,5.08-8.21,11.97-14.66,20.68-19.39,8.68-4.7,18.53-7.06,29.5-7.06s20.87,2.35,29.69,7.06c8.83,4.7,15.72,11.18,20.68,19.39,4.96,8.21,7.42,17.45,7.42,27.67v71.09h-48.16V46.92c0-2.69-.88-4.97-2.6-6.86-1.74-1.87-4.08-2.83-7.01-2.83-2.67,0-4.96.94-6.82,2.83-1.89,1.9-2.82,4.18-2.82,6.86v69.79c0,6.19,2.34,11.26,7.04,15.14,4.67,3.91,12.24,8.83,22.68,14.74,8.04,4.32,14.57,8.09,19.68,11.3,5.08,3.24,9.37,7.46,12.83,12.72,3.48,5.26,5.22,11.26,5.22,17.98v86.83c0,10.25-2.48,19.46-7.42,27.67-4.96,8.21-11.83,14.69-20.68,19.39-8.85,4.71-18.72,7.08-29.7,7.08s-20.8-2.35-29.5-7.08Z"
></path>
<path
fill="#f9f7e8"
d="M784.56,3.24h92.33v44.43h-44.15v88.85h39.38v39.62h-39.38v105.77h44.15v44.42h-92.33V3.24Z"
></path>
<path
fill="#f9f7e8"
d="M920.63,322.49c-5.63-4.18-10.11-10.1-13.45-17.76-3.34-7.68-5.01-16.49-5.01-26.45V53.71c0-9.96,2.53-19.06,7.63-27.26,5.08-8.21,12.05-14.66,20.87-19.39,8.83-4.7,18.6-7.06,29.32-7.06s20.54,2.35,29.5,7.06c8.97,4.7,15.91,11.18,20.87,19.39,4.96,8.21,7.42,17.3,7.42,27.26v94.51h-48.16V46.92c0-2.69-.88-4.97-2.6-6.86-1.74-1.87-4.08-2.83-7.01-2.83-2.67,0-4.96.94-6.82,2.83-1.89,1.9-2.82,4.18-2.82,6.86v231.36c0,2.69.93,4.99,2.82,6.87,1.86,1.9,4.15,2.83,6.82,2.83,2.93,0,5.27-.94,7.01-2.83,1.74-1.87,2.6-4.18,2.6-6.87v-46.03h-11.64v-51.29h59.8v145.4h-48.16v-14.14c-2.96,5.4-6.82,9.48-11.64,12.31-4.82,2.83-10.83,4.25-18.06,4.25s-13.64-2.09-19.27-6.26l-.02-.02Z"
></path>
<path
fill="#f9f7e8"
d="M1053.27,25.05h-6.13l-.06-3.69h5.48c.83-.02,1.61-.15,2.33-.4.72-.27,1.3-.64,1.73-1.14.44-.51.65-1.14.65-1.87,0-.93-.16-1.67-.48-2.22-.3-.55-.83-.94-1.59-1.16-.74-.25-1.74-.37-3.01-.37h-3.78v20.42h-4.12V10.54h7.9c1.87,0,3.49.27,4.86.82,1.38.53,2.44,1.34,3.18,2.44.76,1.08,1.14,2.43,1.14,4.06,0,1.02-.24,1.93-.71,2.73-.47.8-1.17,1.49-2.1,2.07-.91.57-2.03,1.03-3.35,1.39-.06,0-.12.07-.2.2-.06.13-.11.2-.17.2-.32.19-.53.33-.63.43-.08.08-.16.12-.25.14-.08.02-.31.03-.68.03ZM1052.99,25.05l.6-2.81c2.95,0,4.97.64,6.05,1.93,1.08,1.27,1.62,2.89,1.62,4.86v1.53c0,.7.03,1.37.08,2.02.08.62.21,1.15.4,1.59v.45h-4.23c-.19-.49-.3-1.19-.34-2.1-.02-.91-.03-1.57-.03-1.99v-1.48c0-1.38-.31-2.39-.94-3.04s-1.69-.97-3.21-.97ZM1035.75,22.92c0,2.52.43,4.87,1.28,7.04.87,2.16,2.08,4.05,3.64,5.68,1.55,1.61,3.34,2.87,5.37,3.78,2.05.89,4.22,1.33,6.53,1.33s4.51-.44,6.53-1.33c2.03-.91,3.8-2.17,5.34-3.78,1.53-1.63,2.74-3.52,3.61-5.68.87-2.18,1.31-4.52,1.31-7.04s-.44-4.86-1.31-7.01c-.87-2.16-2.07-4.04-3.61-5.65-1.53-1.61-3.31-2.86-5.34-3.75-2.03-.91-4.2-1.36-6.53-1.36s-4.49.45-6.53,1.36c-2.03.89-3.81,2.14-5.37,3.75-1.55,1.61-2.77,3.49-3.64,5.65-.85,2.16-1.28,4.5-1.28,7.01ZM1032.4,22.92c0-3.01.52-5.8,1.56-8.38,1.04-2.57,2.49-4.82,4.34-6.73,1.86-1.93,4-3.43,6.42-4.49,2.44-1.08,5.06-1.62,7.84-1.62s5.39.54,7.81,1.62c2.44,1.06,4.58,2.56,6.42,4.49,1.86,1.91,3.31,4.16,4.35,6.73,1.06,2.57,1.59,5.37,1.59,8.38s-.53,5.8-1.59,8.38c-1.04,2.57-2.49,4.84-4.35,6.79-1.83,1.93-3.97,3.44-6.42,4.52-2.42,1.08-5.03,1.62-7.81,1.62s-5.39-.54-7.84-1.62c-2.42-1.08-4.56-2.58-6.42-4.52-1.85-1.95-3.3-4.21-4.34-6.79-1.04-2.57-1.56-5.37-1.56-8.38Z"
></path>
</g>
</g>
</svg>
<p>Certificamos que</p>
<h1>{{ name }}</h1>
<p>
Portador(a) do CPF <strong>{{ cpf }} </strong>, concluiu o curso
de <strong>NR-10 Complementar (SEP)</strong> com aproveitamento
de
<strong>{{ progress }}%</strong>
</p>
<p>Realizado entre {{ started_date }} e {{ finished_date }}</p>
<p>Florianópolis, SC, {{ today }}</p>
<div class="signatures">
<div class="sign1"></div>
<div class="sign2">
<p>Tiago Maciel do Santos</p>
<p>CEO/Diretor</p>
</div>
</div>
<div class="qrcode">
<img src="{{ qrcode }}" />
</div>
</section>
<section id="back">
<div class="space-y-2.5">
<h1>Conteúdo programático ministrado</h1>
<ul>
<li>Organização do sistema elétrico de potência</li>
<li>Organização do trabalho</li>
<li>Aspectos comportamentais</li>
<li>Condições impeditivas para serviços</li>
<li>Riscos típicos no SEP e sua prevenção</li>
<li>Técnicas de análise de riscos no SEP</li>
<li>Procedimentos de trabalho (análise e discussão)</li>
<li>Técnicas de análise de riscos no SEP</li>
<li>Equipamentos e ferramentas de trabalho</li>
<li>Sistemas de proteção coletiva</li>
<li>Equipamentos de proteção individual</li>
<li>Posturas e vestuários de trabalhos</li>
<li>
Segurança com veículos e transporte de pessoas,
materiais e equipamentos
</li>
<li>Sinalização e isolamento de áreas de trabalho</li>
<li>
Liberação de instalação para serviço, operação e uso
</li>
<li>
Treinamento em técnicas de remoção, atendimento e
transporte de acidentados
</li>
<li>Acidentes típicos</li>
<li>Responsabilidades</li>
</ul>
</div>
<div class="space-y-2.5">
<dd class="space-y-0.5">
<h2>Carga horária</h2>
<p>40 horas</p>
</dd>
<dd class="space-y-0.5">
<h2>Instrutor e responsável técnico</h2>
<div>
<p>Francis Ricardo Baretta</p>
<p>CPF 039.539.409-02</p>
<p>Eng. de Segurança no Trabalho Eng. Eletricista</p>
<p>CREA/SC 126693-0</p>
</div>
</dd>
</div>
</section>
</body>
</html>

View File

@@ -1,28 +0,0 @@
[project]
name = "certs"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.13"
dependencies = [
"jinja2>=3.1.6",
"layercake",
"qrcode>=8.2",
]
[tool.uv.sources]
layercake = { path = "../layercake" }
[tool.ruff]
target-version = "py311"
src = ["app"]
[tool.ruff.format]
quote-style = "single"
[tool.ruff.lint]
select = ["E", "F", "I"]
[dependency-groups]
dev = [
"ruff>=0.11.9",
]

1006
certs/uv.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +0,0 @@
build:
sam build --use-container
deploy: build
sam deploy --debug

View File

@@ -1,13 +0,0 @@
import os
import boto3
def get_dynamodb_client():
if os.getenv('AWS_LAMBDA_FUNCTION_NAME'):
return boto3.client('dynamodb')
return boto3.client('dynamodb', endpoint_url='http://127.0.0.1:8000')
dynamodb_client = get_dynamodb_client()

View File

@@ -1,16 +0,0 @@
import os
USER_TABLE: str = os.getenv('USER_TABLE') # type: ignore
ORDER_TABLE: str = os.getenv('ORDER_TABLE') # type: ignore
ENROLLMENT_TABLE: str = os.getenv('ENROLLMENT_TABLE') # type: ignore
COURSE_TABLE: str = os.getenv('COURSE_TABLE') # type: ignore
# Post-migration: remove the lines below
if os.getenv('AWS_LAMBDA_FUNCTION_NAME'):
SQLITE_DATABASE = 'courses_export_2025-06-18_110214.db'
else:
SQLITE_DATABASE = 'app/courses_export_2025-06-18_110214.db'
SQLITE_TABLE = 'courses'
OLD_ENROLLMENT_TABLE: str = os.getenv('OLD_ENROLLMENT_TABLE') # type: ignore

View File

@@ -1,4 +0,0 @@
"""
Stopgap events. Everything here is a quick fix and should be replaced with
proper solutions.
"""

View File

@@ -1,59 +0,0 @@
import json
import sqlite3
from aws_lambda_powertools import Logger
from aws_lambda_powertools.utilities.data_classes import (
EventBridgeEvent,
event_source,
)
from aws_lambda_powertools.utilities.typing import LambdaContext
from layercake.dynamodb import DynamoDBPersistenceLayer
from sqlite_utils import Database
from boto3clients import dynamodb_client
from config import (
COURSE_TABLE,
ENROLLMENT_TABLE,
SQLITE_DATABASE,
SQLITE_TABLE,
)
sqlite3.register_converter('json', json.loads)
logger = Logger(__name__)
enrollment_layer = DynamoDBPersistenceLayer(ENROLLMENT_TABLE, dynamodb_client)
course_layer = DynamoDBPersistenceLayer(COURSE_TABLE, dynamodb_client)
deduplication_window = {'offset_days': 90}
class DeduplicationConflictError(Exception):
def __init__(self, *args):
super().__init__('Enrollment already exists')
@event_source(data_class=EventBridgeEvent)
@logger.inject_lambda_context
def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool:
new_image = event.detail['new_image']
return True
class CourseNotFoundError(Exception):
def __init__(self, *args):
super().__init__('Course not found')
def _get_course(course_id: str) -> dict:
with sqlite3.connect(
database=SQLITE_DATABASE, detect_types=sqlite3.PARSE_DECLTYPES
) as conn:
db = Database(conn)
rows = db[SQLITE_TABLE].rows_where(
"json->>'$.metadata__betaeducacao_id' = ?", [course_id]
)
for row in rows:
return row['json']
raise CourseNotFoundError

View File

@@ -1,33 +0,0 @@
[project]
name = "enrollment-management"
version = "0.1.0"
description = ""
readme = ""
requires-python = ">=3.13"
dependencies = ["layercake"]
[dependency-groups]
dev = [
"jsonlines>=4.0.0",
"pytest>=8.3.4",
"pytest-cov>=6.0.0",
"ruff>=0.9.1",
]
[tool.pytest.ini_options]
pythonpath = ["app/"]
addopts = "--cov --cov-report html -v"
[tool.ruff]
target-version = "py311"
src = ["app"]
[tool.ruff.format]
quote-style = "single"
[tool.ruff.lint]
select = ["E", "F", "I"]
[tool.uv.sources]
layercake = { path = "../layercake" }

View File

@@ -1,3 +0,0 @@
{
"extraPaths": ["app/"]
}

View File

@@ -1,9 +0,0 @@
version = 0.1
[default.deploy.parameters]
stack_name = "saladeaula-enrollment-management"
resolve_s3 = true
s3_prefix = "enrollment_management"
region = "sa-east-1"
confirm_changeset = false
capabilities = "CAPABILITY_IAM"
image_repositories = []

View File

@@ -1,63 +0,0 @@
AWSTemplateFormatVersion: 2010-09-09
Transform: AWS::Serverless-2016-10-31
Parameters:
UserTable:
Type: String
Default: betaeducacao-prod-users_d2o3r5gmm4it7j
EnrollmentTable:
Type: String
Default: betaeducacao-prod-enrollments
CourseTable:
Type: String
Default: saladeaula_courses
OrderTable:
Type: String
Default: betaeducacao-prod-orders
Globals:
Function:
CodeUri: app/
Runtime: python3.13
Tracing: Active
Architectures:
- x86_64
Layers:
- !Sub arn:aws:lambda:sa-east-1:336641857101:layer:layercake:78
Environment:
Variables:
TZ: America/Sao_Paulo
LOG_LEVEL: DEBUG
DYNAMODB_PARTITION_KEY: id
POWERTOOLS_LOGGER_SAMPLE_RATE: 0.1
POWERTOOLS_LOGGER_LOG_EVENT: true
USER_TABLE: !Ref UserTable
ENROLLMENT_TABLE: !Ref EnrollmentTable
ORDER_TABLE: !Ref OrderTable
COURSE_TABLE: !Ref CourseTable
Resources:
EventLog:
Type: AWS::Logs::LogGroup
Properties:
RetentionInDays: 90
EventEnrollFunction:
Type: AWS::Serverless::Function
Properties:
Handler: events.stopgap.enroll.lambda_handler
LoggingConfig:
LogGroup: !Ref EventLog
Policies:
- DynamoDBCrudPolicy:
TableName: !Ref EnrollmentTable
Events:
DynamoDBEvent:
Type: EventBridgeRule
Properties:
Pattern:
resources: [!Ref EnrollmentTable]
detail-type: [INSERT]
detail:
new_image:
sk: ["konviva"]

View File

@@ -1,74 +0,0 @@
import os
from dataclasses import dataclass
import jsonlines
import pytest
PYTEST_TABLE_NAME = 'pytest'
PK = 'id'
SK = 'sk'
# https://docs.pytest.org/en/7.1.x/reference/reference.html#pytest.hookspec.pytest_configure
def pytest_configure():
os.environ['TZ'] = 'America/Sao_Paulo'
os.environ['DYNAMODB_PARTITION_KEY'] = PK
os.environ['DYNAMODB_SORT_KEY'] = SK
os.environ['USER_TABLE'] = PYTEST_TABLE_NAME
os.environ['COURSE_TABLE'] = PYTEST_TABLE_NAME
os.environ['ORDER_TABLE'] = PYTEST_TABLE_NAME
os.environ['ENROLLMENT_TABLE'] = PYTEST_TABLE_NAME
# Post-migration: remove it
os.environ['OLD_ENROLLMENT_TABLE'] = PYTEST_TABLE_NAME
@dataclass
class LambdaContext:
function_name: str = 'test'
memory_limit_in_mb: int = 128
invoked_function_arn: str = 'arn:aws:lambda:eu-west-1:809313241:function:test'
aws_request_id: str = '52fdfc07-2182-154f-163f-5f0f9a621d72'
@pytest.fixture
def lambda_context() -> LambdaContext:
return LambdaContext()
@pytest.fixture
def dynamodb_client():
from boto3clients import dynamodb_client as client
client.create_table(
AttributeDefinitions=[
{'AttributeName': PK, 'AttributeType': 'S'},
{'AttributeName': SK, 'AttributeType': 'S'},
],
TableName=PYTEST_TABLE_NAME,
KeySchema=[
{'AttributeName': PK, 'KeyType': 'HASH'},
{'AttributeName': SK, 'KeyType': 'RANGE'},
],
ProvisionedThroughput={
'ReadCapacityUnits': 123,
'WriteCapacityUnits': 123,
},
)
yield client
client.delete_table(TableName=PYTEST_TABLE_NAME)
@pytest.fixture()
def dynamodb_persistence_layer(dynamodb_client):
from layercake.dynamodb import DynamoDBPersistenceLayer
return DynamoDBPersistenceLayer(PYTEST_TABLE_NAME, dynamodb_client)
@pytest.fixture()
def dynamodb_seeds(dynamodb_client):
with jsonlines.open('tests/seeds.jsonl') as lines:
for line in lines:
dynamodb_client.put_item(TableName=PYTEST_TABLE_NAME, Item=line)

View File

@@ -1,39 +0,0 @@
import pprint
import app.events.stopgap.enroll as app
from aws_lambda_powertools.utilities.typing import LambdaContext
from layercake.dynamodb import (
DynamoDBPersistenceLayer,
SortKey,
TransactKey,
)
def test_enroll(
dynamodb_seeds,
dynamodb_client,
dynamodb_persistence_layer: DynamoDBPersistenceLayer,
lambda_context: LambdaContext,
):
event = {
'detail': {
'new_image': {
'id': '47ZxxcVBjvhDS5TE98tpfQ',
'sk': 'konviva',
}
}
}
assert app.lambda_handler(event, lambda_context) # type: ignore
result = dynamodb_persistence_layer.collection.get_items(
TransactKey('47ZxxcVBjvhDS5TE98tpfQ')
+ SortKey('0')
+ SortKey('metadata#tenant')
+ SortKey('metadata#author')
+ SortKey('metadata#konviva')
+ SortKey('metadata#lock')
+ SortKey('metadata#deduplication_window')
+ SortKey('metadata#cert')
)
pprint.pprint(result)

View File

@@ -1,3 +0,0 @@
{"id": {"S": "47ZxxcVBjvhDS5TE98tpfQ"}, "sk": {"S": "0"}, "course": {"M": {"id": {"S": "42"}, "name": {"S": "NR-35 Segurança nos Trabalhos em Altura (Teórico)"},"time_in_days": {"N": "720"}}},"create_date": {"S": "2025-04-10T11:58:33.303347-03:00"},"konviva:id": {"N": "238662"},"progress": {"N": "16.67"},"score": {"NULL": true},"status": {"S": "IN_PROGRESS"}, "update_date": {"S": "2025-04-10T15:44:03.023054-03:00"}, "user": {"M": {"id": {"S": "5OxmMjL-ujoR5IMGegQz"}, "cpf": {"S": "07879819908"}, "email": {"S": "sergio@somosbeta.com.br"}, "name": {"S": "Sérgio Rafael Siqueira"}}}}
{"id": {"S": "47ZxxcVBjvhDS5TE98tpfQ"}, "sk": {"S": "konviva"}, "create_date": {"S": "2025-04-10T11:58:35.035729-03:00"}, "konviva_id": {"N": "238662"}}
{"id": {"S": "47ZxxcVBjvhDS5TE98tpfQ"}, "sk": {"S": "tenant"}, "create_date": {"S": "2025-04-10T11:58:33.303347-03:00"}, "name": {"S": "Beta Educação"},"org_id": {"S": "cJtK9SsnJhKPyxESe7g3DG"}}

File diff suppressed because it is too large Load Diff

View File

@@ -42,22 +42,27 @@ class LifecycleEvents(str, Enum):
"""Lifecycle events related to scheduling actions.""" """Lifecycle events related to scheduling actions."""
# Reminder if the user does not access within 3 days # Reminder if the user does not access within 3 days
REMINDER_NO_ACCESS_3_DAYS = 'schedules#reminder_no_access_3_days' # REMINDER_NO_ACCESS_3_DAYS = 'schedules#reminder_no_access_3_days'
DOES_NOT_ACCESS = 'schedules#does_not_access'
# When there is no activity 7 days after the first access # When there is no activity 7 days after the first access
NO_ACTIVITY_7_DAYS = 'schedules#no_activity_7_days' # NO_ACTIVITY_7_DAYS = 'schedules#no_activity_7_days'
NO_ACTIVITY = 'schedules#no_activity'
# Reminder 30 days before the access period expires # Reminder 30 days before the access period expires
ACCESS_PERIOD_REMINDER_30_DAYS = 'schedules#access_period_reminder_30_days' # ACCESS_PERIOD_REMINDER_30_DAYS = 'schedules#access_period_reminder_30_days'
ACCESS_PERIOD_ENDS = 'schedules#access_period_ends'
# Reminder for certificate expiration set to 30 days from now # Reminder for certificate expiration set to 30 days from now
CERT_EXPIRATION_REMINDER_30_DAYS = 'schedules#cert_expiration_reminder_30_days' CERT_EXPIRATION_REMINDER_30_DAYS = 'schedules#cert_expiration_reminder_30_days'
# Archive the course after the certificate expires # Archive the course after the certificate expires
COURSE_ARCHIVED = 'schedules#course_archived' # COURSE_ARCHIVED = 'schedules#course_archived'
ARCHIVE_IT = 'schedules#archive_it'
# When the access period ends for a course without a certificate # When the access period ends for a course without a certificate
COURSE_EXPIRED = 'schedules#course_expired' # COURSE_EXPIRED = 'schedules#course_expired'
EXPIRATION = 'schedules#expiration'
def enroll( def enroll(
@@ -106,7 +111,8 @@ def enroll(
transact.put( transact.put(
item={ item={
'id': enrollment.id, 'id': enrollment.id,
'sk': LifecycleEvents.REMINDER_NO_ACCESS_3_DAYS, # 'sk': LifecycleEvents.REMINDER_NO_ACCESS_3_DAYS,
'sk': LifecycleEvents.DOES_NOT_ACCESS,
'name': user.name, 'name': user.name,
'email': user.email, 'email': user.email,
'course': course.name, 'course': course.name,
@@ -120,7 +126,8 @@ def enroll(
transact.put( transact.put(
item={ item={
'id': enrollment.id, 'id': enrollment.id,
'sk': LifecycleEvents.COURSE_EXPIRED, 'sk': LifecycleEvents.EXPIRATION,
# 'sk': LifecycleEvents.COURSE_EXPIRED,
'name': user.name, 'name': user.name,
'email': user.email, 'email': user.email,
'course': course.name, 'course': course.name,
@@ -131,7 +138,8 @@ def enroll(
transact.put( transact.put(
item={ item={
'id': enrollment.id, 'id': enrollment.id,
'sk': LifecycleEvents.ACCESS_PERIOD_REMINDER_30_DAYS, # 'sk': LifecycleEvents.ACCESS_PERIOD_REMINDER_30_DAYS,
'sk': LifecycleEvents.ACCESS_PERIOD_ENDS,
'name': user.name, 'name': user.name,
'email': user.email, 'email': user.email,
'course': course.name, 'course': course.name,
@@ -161,14 +169,14 @@ def enroll(
} }
) )
class VacancyDoesNotExistError(Exception): class SlotDoesNotExistError(Exception):
def __init__(self, *args): def __init__(self, *args):
super().__init__('Vacancy does not exist') super().__init__('Slot does not exist')
transact.delete( transact.delete(
key=KeyPair(vacancy.id, vacancy.sk), key=KeyPair(vacancy.id, vacancy.sk),
cond_expr='attribute_exists(sk)', cond_expr='attribute_exists(sk)',
exc_cls=VacancyDoesNotExistError, exc_cls=SlotDoesNotExistError,
) )
transact.put( transact.put(
item={ item={
@@ -277,10 +285,10 @@ def set_status_as_canceled(
cond_expr='attribute_exists(sk)', cond_expr='attribute_exists(sk)',
) )
# Remove schedules lifecycle events, referencies and locks # Remove schedules lifecycle events, referencies and locks
transact.delete(key=KeyPair(id, 'schedules#archive_it')) transact.delete(key=KeyPair(id, LifecycleEvents.ARCHIVE_IT))
transact.delete(key=KeyPair(id, 'schedules#no_activity')) transact.delete(key=KeyPair(id, LifecycleEvents.NO_ACTIVITY))
transact.delete(key=KeyPair(id, 'schedules#access_period_ends')) transact.delete(key=KeyPair(id, LifecycleEvents.ACCESS_PERIOD_ENDS))
transact.delete(key=KeyPair(id, 'schedules#does_not_access')) transact.delete(key=KeyPair(id, LifecycleEvents.DOES_NOT_ACCESS))
transact.delete(key=KeyPair(id, 'parent_vacancy')) transact.delete(key=KeyPair(id, 'parent_vacancy'))
transact.delete(key=KeyPair(id, 'lock')) transact.delete(key=KeyPair(id, 'lock'))
transact.delete(key=KeyPair('lock', lock_hash)) transact.delete(key=KeyPair('lock', lock_hash))
@@ -305,8 +313,9 @@ def set_status_as_canceled(
}, },
cond_expr='attribute_not_exists(sk)', cond_expr='attribute_not_exists(sk)',
) )
# Post-migration: rename `generated_items` to `slots`.
# Set the status of `generated_items` to `ROLLBACK` to know # Set the status of `generated_items` to `ROLLBACK` to know
# which vacancy is available for reuse # which slot is available for reuse
transact.update( transact.update(
key=KeyPair(order_id, f'generated_items#{enrollment_id}'), key=KeyPair(order_id, f'generated_items#{enrollment_id}'),
update_expr='SET #status = :status, update_date = :update', update_expr='SET #status = :status, update_date = :update',

View File

@@ -26,7 +26,7 @@ Globals:
Architectures: Architectures:
- x86_64 - x86_64
Layers: Layers:
- !Sub arn:aws:lambda:sa-east-1:336641857101:layer:layercake:75 - !Sub arn:aws:lambda:sa-east-1:336641857101:layer:layercake:79
Environment: Environment:
Variables: Variables:
TZ: America/Sao_Paulo TZ: America/Sao_Paulo
@@ -37,7 +37,6 @@ Globals:
USER_TABLE: !Ref UserTable USER_TABLE: !Ref UserTable
ORDER_TABLE: !Ref OrderTable ORDER_TABLE: !Ref OrderTable
ENROLLMENT_TABLE: !Ref EnrollmentTable ENROLLMENT_TABLE: !Ref EnrollmentTable
NEW_ENROLLMENT_TABLE: !Ref NewEnrollmentTable
COURSE_TABLE: !Ref CourseTable COURSE_TABLE: !Ref CourseTable
ELASTIC_CLOUD_ID: "{{resolve:ssm:/betaeducacao/elastic/cloud_id/str}}" ELASTIC_CLOUD_ID: "{{resolve:ssm:/betaeducacao/elastic/cloud_id/str}}"
ELASTIC_AUTH_PASS: "{{resolve:ssm:/betaeducacao/elastic/auth_pass/str}}" ELASTIC_AUTH_PASS: "{{resolve:ssm:/betaeducacao/elastic/auth_pass/str}}"

View File

@@ -1,6 +1,6 @@
[project] [project]
name = "layercake" name = "layercake"
version = "0.6.11" version = "0.6.12"
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 = [
@@ -24,6 +24,8 @@ dependencies = [
"weasyprint>=65.0", "weasyprint>=65.0",
"smart-open[s3]>=7.1.0", "smart-open[s3]>=7.1.0",
"sqlite-utils>=3.38", "sqlite-utils>=3.38",
"jinja2>=3.1.6",
"qrcode>=8.2",
] ]
[dependency-groups] [dependency-groups]

View File

@@ -16,7 +16,7 @@ Resources:
CompatibleRuntimes: CompatibleRuntimes:
- python3.12 - python3.12
- python3.13 - python3.13
RetentionPolicy: Delete RetentionPolicy: Retain
Metadata: Metadata:
BuildMethod: python3.13 BuildMethod: python3.13
BuildArchitecture: x86_64 BuildArchitecture: x86_64

129
layercake/uv.lock generated
View File

@@ -299,6 +299,30 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/0e/f6/65ecc6878a89bb1c23a086ea335ad4bf21a588990c3f535a227b9eea9108/charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85", size = 49767, upload-time = "2024-12-24T18:12:32.852Z" }, { url = "https://files.pythonhosted.org/packages/0e/f6/65ecc6878a89bb1c23a086ea335ad4bf21a588990c3f535a227b9eea9108/charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85", size = 49767, upload-time = "2024-12-24T18:12:32.852Z" },
] ]
[[package]]
name = "click"
version = "8.2.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/60/6c/8ca2efa64cf75a977a0d7fac081354553ebe483345c734fb6b6515d96bbc/click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", size = 286342, upload-time = "2025-05-20T23:19:49.832Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b", size = 102215, upload-time = "2025-05-20T23:19:47.796Z" },
]
[[package]]
name = "click-default-group"
version = "1.2.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "click" },
]
sdist = { url = "https://files.pythonhosted.org/packages/1d/ce/edb087fb53de63dad3b36408ca30368f438738098e668b78c87f93cd41df/click_default_group-1.2.4.tar.gz", hash = "sha256:eb3f3c99ec0d456ca6cd2a7f08f7d4e91771bef51b01bdd9580cc6450fe1251e", size = 3505, upload-time = "2023-08-04T07:54:58.425Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2c/1a/aff8bb287a4b1400f69e09a53bd65de96aa5cee5691925b38731c67fc695/click_default_group-1.2.4-py2.py3-none-any.whl", hash = "sha256:9b60486923720e7fc61731bdb32b617039aba820e22e1c88766b1125592eaa5f", size = 4123, upload-time = "2023-08-04T07:54:56.875Z" },
]
[[package]] [[package]]
name = "colorama" name = "colorama"
version = "0.4.6" version = "0.4.6"
@@ -554,6 +578,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" },
] ]
[[package]]
name = "jinja2"
version = "3.1.6"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "markupsafe" },
]
sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" },
]
[[package]] [[package]]
name = "jmespath" name = "jmespath"
version = "1.0.1" version = "1.0.1"
@@ -589,7 +625,7 @@ wheels = [
[[package]] [[package]]
name = "layercake" name = "layercake"
version = "0.6.5" version = "0.6.11"
source = { editable = "." } source = { editable = "." }
dependencies = [ dependencies = [
{ name = "arnparse" }, { name = "arnparse" },
@@ -598,14 +634,17 @@ dependencies = [
{ name = "elasticsearch-dsl" }, { name = "elasticsearch-dsl" },
{ name = "ftfy" }, { name = "ftfy" },
{ name = "glom" }, { name = "glom" },
{ name = "jinja2" },
{ name = "meilisearch" }, { name = "meilisearch" },
{ name = "orjson" }, { name = "orjson" },
{ name = "pycpfcnpj" }, { name = "pycpfcnpj" },
{ name = "pydantic", extra = ["email"] }, { name = "pydantic", extra = ["email"] },
{ name = "pydantic-extra-types" }, { name = "pydantic-extra-types" },
{ name = "pytz" }, { name = "pytz" },
{ name = "qrcode" },
{ name = "requests" }, { name = "requests" },
{ name = "smart-open", extra = ["s3"] }, { name = "smart-open", extra = ["s3"] },
{ name = "sqlite-utils" },
{ name = "weasyprint" }, { name = "weasyprint" },
] ]
@@ -627,14 +666,17 @@ requires-dist = [
{ name = "elasticsearch-dsl", specifier = ">=8.17.1" }, { name = "elasticsearch-dsl", specifier = ">=8.17.1" },
{ name = "ftfy", specifier = ">=6.3.1" }, { name = "ftfy", specifier = ">=6.3.1" },
{ name = "glom", specifier = ">=24.11.0" }, { name = "glom", specifier = ">=24.11.0" },
{ name = "jinja2", specifier = ">=3.1.6" },
{ name = "meilisearch", specifier = ">=0.34.0" }, { name = "meilisearch", specifier = ">=0.34.0" },
{ name = "orjson", specifier = ">=3.10.15" }, { name = "orjson", specifier = ">=3.10.15" },
{ name = "pycpfcnpj", specifier = ">=1.8" }, { name = "pycpfcnpj", specifier = ">=1.8" },
{ name = "pydantic", extras = ["email"], specifier = ">=2.10.6" }, { name = "pydantic", extras = ["email"], specifier = ">=2.10.6" },
{ name = "pydantic-extra-types", specifier = ">=2.10.3" }, { name = "pydantic-extra-types", specifier = ">=2.10.3" },
{ name = "pytz", specifier = ">=2025.1" }, { name = "pytz", specifier = ">=2025.1" },
{ name = "qrcode", specifier = ">=8.2" },
{ name = "requests", specifier = ">=2.32.3" }, { name = "requests", specifier = ">=2.32.3" },
{ name = "smart-open", extras = ["s3"], specifier = ">=7.1.0" }, { name = "smart-open", extras = ["s3"], specifier = ">=7.1.0" },
{ name = "sqlite-utils", specifier = ">=3.38" },
{ name = "weasyprint", specifier = ">=65.0" }, { name = "weasyprint", specifier = ">=65.0" },
] ]
@@ -648,6 +690,44 @@ dev = [
{ name = "ruff", specifier = ">=0.11.1" }, { name = "ruff", specifier = ">=0.11.1" },
] ]
[[package]]
name = "markupsafe"
version = "3.0.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537, upload-time = "2024-10-18T15:21:54.129Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/22/09/d1f21434c97fc42f09d290cbb6350d44eb12f09cc62c9476effdb33a18aa/MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", size = 14274, upload-time = "2024-10-18T15:21:13.777Z" },
{ url = "https://files.pythonhosted.org/packages/6b/b0/18f76bba336fa5aecf79d45dcd6c806c280ec44538b3c13671d49099fdd0/MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", size = 12348, upload-time = "2024-10-18T15:21:14.822Z" },
{ url = "https://files.pythonhosted.org/packages/e0/25/dd5c0f6ac1311e9b40f4af06c78efde0f3b5cbf02502f8ef9501294c425b/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", size = 24149, upload-time = "2024-10-18T15:21:15.642Z" },
{ url = "https://files.pythonhosted.org/packages/f3/f0/89e7aadfb3749d0f52234a0c8c7867877876e0a20b60e2188e9850794c17/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", size = 23118, upload-time = "2024-10-18T15:21:17.133Z" },
{ url = "https://files.pythonhosted.org/packages/d5/da/f2eeb64c723f5e3777bc081da884b414671982008c47dcc1873d81f625b6/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", size = 22993, upload-time = "2024-10-18T15:21:18.064Z" },
{ url = "https://files.pythonhosted.org/packages/da/0e/1f32af846df486dce7c227fe0f2398dc7e2e51d4a370508281f3c1c5cddc/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", size = 24178, upload-time = "2024-10-18T15:21:18.859Z" },
{ url = "https://files.pythonhosted.org/packages/c4/f6/bb3ca0532de8086cbff5f06d137064c8410d10779c4c127e0e47d17c0b71/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", size = 23319, upload-time = "2024-10-18T15:21:19.671Z" },
{ url = "https://files.pythonhosted.org/packages/a2/82/8be4c96ffee03c5b4a034e60a31294daf481e12c7c43ab8e34a1453ee48b/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", size = 23352, upload-time = "2024-10-18T15:21:20.971Z" },
{ url = "https://files.pythonhosted.org/packages/51/ae/97827349d3fcffee7e184bdf7f41cd6b88d9919c80f0263ba7acd1bbcb18/MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", size = 15097, upload-time = "2024-10-18T15:21:22.646Z" },
{ url = "https://files.pythonhosted.org/packages/c1/80/a61f99dc3a936413c3ee4e1eecac96c0da5ed07ad56fd975f1a9da5bc630/MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", size = 15601, upload-time = "2024-10-18T15:21:23.499Z" },
{ url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274, upload-time = "2024-10-18T15:21:24.577Z" },
{ url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352, upload-time = "2024-10-18T15:21:25.382Z" },
{ url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122, upload-time = "2024-10-18T15:21:26.199Z" },
{ url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085, upload-time = "2024-10-18T15:21:27.029Z" },
{ url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978, upload-time = "2024-10-18T15:21:27.846Z" },
{ url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208, upload-time = "2024-10-18T15:21:28.744Z" },
{ url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357, upload-time = "2024-10-18T15:21:29.545Z" },
{ url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344, upload-time = "2024-10-18T15:21:30.366Z" },
{ url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101, upload-time = "2024-10-18T15:21:31.207Z" },
{ url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603, upload-time = "2024-10-18T15:21:32.032Z" },
{ url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510, upload-time = "2024-10-18T15:21:33.625Z" },
{ url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486, upload-time = "2024-10-18T15:21:34.611Z" },
{ url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480, upload-time = "2024-10-18T15:21:35.398Z" },
{ url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914, upload-time = "2024-10-18T15:21:36.231Z" },
{ url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796, upload-time = "2024-10-18T15:21:37.073Z" },
{ url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473, upload-time = "2024-10-18T15:21:37.932Z" },
{ url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114, upload-time = "2024-10-18T15:21:39.799Z" },
{ url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098, upload-time = "2024-10-18T15:21:40.813Z" },
{ url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208, upload-time = "2024-10-18T15:21:41.814Z" },
{ url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739, upload-time = "2024-10-18T15:21:42.784Z" },
]
[[package]] [[package]]
name = "meilisearch" name = "meilisearch"
version = "0.34.0" version = "0.34.0"
@@ -1001,6 +1081,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/eb/38/ac33370d784287baa1c3d538978b5e2ea064d4c1b93ffbd12826c190dd10/pytz-2025.1-py2.py3-none-any.whl", hash = "sha256:89dd22dca55b46eac6eda23b2d72721bf1bdfef212645d81513ef5d03038de57", size = 507930, upload-time = "2025-01-31T01:54:45.634Z" }, { url = "https://files.pythonhosted.org/packages/eb/38/ac33370d784287baa1c3d538978b5e2ea064d4c1b93ffbd12826c190dd10/pytz-2025.1-py2.py3-none-any.whl", hash = "sha256:89dd22dca55b46eac6eda23b2d72721bf1bdfef212645d81513ef5d03038de57", size = 507930, upload-time = "2025-01-31T01:54:45.634Z" },
] ]
[[package]]
name = "qrcode"
version = "8.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/8f/b2/7fc2931bfae0af02d5f53b174e9cf701adbb35f39d69c2af63d4a39f81a9/qrcode-8.2.tar.gz", hash = "sha256:35c3f2a4172b33136ab9f6b3ef1c00260dd2f66f858f24d88418a015f446506c", size = 43317, upload-time = "2025-05-01T15:44:24.726Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/dd/b8/d2d6d731733f51684bbf76bf34dab3b70a9148e8f2cef2bb544fccec681a/qrcode-8.2-py3-none-any.whl", hash = "sha256:16e64e0716c14960108e85d853062c9e8bba5ca8252c0b4d0231b9df4060ff4f", size = 45986, upload-time = "2025-05-01T15:44:22.781Z" },
]
[[package]] [[package]]
name = "requests" name = "requests"
version = "2.32.3" version = "2.32.3"
@@ -1079,6 +1171,41 @@ s3 = [
{ name = "boto3" }, { name = "boto3" },
] ]
[[package]]
name = "sqlite-fts4"
version = "1.0.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/c2/6d/9dad6c3b433ab8912ace969c66abd595f8e0a2ccccdb73602b1291dbda29/sqlite-fts4-1.0.3.tar.gz", hash = "sha256:78b05eeaf6680e9dbed8986bde011e9c086a06cb0c931b3cf7da94c214e8930c", size = 9718, upload-time = "2022-07-30T01:14:26.943Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/51/29/0096e8b1811aaa78cfb296996f621f41120c21c2f5cd448ae1d54979d9fc/sqlite_fts4-1.0.3-py3-none-any.whl", hash = "sha256:0359edd8dea6fd73c848989e1e2b1f31a50fe5f9d7272299ff0e8dbaa62d035f", size = 9972, upload-time = "2022-07-30T01:14:24.942Z" },
]
[[package]]
name = "sqlite-utils"
version = "3.38"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "click" },
{ name = "click-default-group" },
{ name = "pluggy" },
{ name = "python-dateutil" },
{ name = "sqlite-fts4" },
{ name = "tabulate" },
]
sdist = { url = "https://files.pythonhosted.org/packages/51/43/ce9183a21911e0b73248c8fb83f8b8038515cb80053912c2a009e9765564/sqlite_utils-3.38.tar.gz", hash = "sha256:1ae77b931384052205a15478d429464f6c67a3ac3b4eafd3c674ac900f623aab", size = 214449, upload-time = "2024-11-23T22:49:40.308Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/4d/eb/f8e8e827805f810838efff3311cccd2601238c5fa3fc35c1f878709e161b/sqlite_utils-3.38-py3-none-any.whl", hash = "sha256:8a27441015c3b2ef475f555861f7a2592f73bc60d247af9803a11b65fc605bf9", size = 68183, upload-time = "2024-11-23T22:49:38.289Z" },
]
[[package]]
name = "tabulate"
version = "0.9.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ec/fe/802052aecb21e3797b8f7902564ab6ea0d60ff8ca23952079064155d1ae1/tabulate-0.9.0.tar.gz", hash = "sha256:0095b12bf5966de529c0feb1fa08671671b3368eec77d7ef7ab114be2c068b3c", size = 81090, upload-time = "2022-10-06T17:21:48.54Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/40/44/4a5f08c96eb108af5cb50b41f76142f0afa346dfa99d5296fe7202a11854/tabulate-0.9.0-py3-none-any.whl", hash = "sha256:024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f", size = 35252, upload-time = "2022-10-06T17:21:44.262Z" },
]
[[package]] [[package]]
name = "tinycss2" name = "tinycss2"
version = "1.4.0" version = "1.4.0"

View File

@@ -1,5 +0,0 @@
build:
sam build --use-container
deploy: build
sam deploy --debug

View File

@@ -1,13 +0,0 @@
import os
import boto3
def get_dynamodb_client():
if os.getenv('AWS_LAMBDA_FUNCTION_NAME'):
return boto3.client('dynamodb')
return boto3.client('dynamodb', endpoint_url='http://localhost:8000')
dynamodb_client = get_dynamodb_client()

View File

@@ -1,5 +0,0 @@
import os
USER_TABLE: str = os.getenv('USER_TABLE') # type: ignore
ORDER_TABLE: str = os.getenv('ORDER_TABLE') # type: ignore
ENROLLMENT_TABLE: str = os.getenv('ENROLLMENT_TABLE') # type: ignore

View File

@@ -1,78 +0,0 @@
from aws_lambda_powertools import Logger
from aws_lambda_powertools.utilities.data_classes import (
EventBridgeEvent,
event_source,
)
from aws_lambda_powertools.utilities.typing import LambdaContext
from layercake.dateutils import now
from layercake.dynamodb import (
DynamoDBPersistenceLayer,
KeyPair,
SortKey,
)
from boto3clients import dynamodb_client
from config import ORDER_TABLE, USER_TABLE
logger = Logger(__name__)
user_layer = DynamoDBPersistenceLayer(USER_TABLE, dynamodb_client)
order_layer = DynamoDBPersistenceLayer(ORDER_TABLE, dynamodb_client)
@event_source(data_class=EventBridgeEvent)
@logger.inject_lambda_context
def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool:
new_image = event.detail['new_image']
now_ = now()
ids = user_layer.collection.get_items(
KeyPair(
pk='cnpj',
sk=SortKey(new_image['cnpj'], path_spec='user_id'),
rename_key='org_id',
)
+ KeyPair(
pk='email',
sk=SortKey(new_image['email'], path_spec='user_id'),
rename_key='user_id',
),
flatten_top=False,
)
# Sometimes the function executes before the user insertion completes,
# so an exception is raised to trigger a retry.
if len(ids) < 2:
raise ValueError('IDs not found.')
with order_layer.transact_writer() as transact:
transact.update(
key=KeyPair(new_image['id'], '0'),
update_expr='SET metadata__tenant_id = :tenant_id, \
metadata__related_ids = :related_ids, \
update_date = :update_date',
expr_attr_values={
':tenant_id': ids['org_id'],
':related_ids': set(ids.values()),
':update_date': now_,
},
)
transact.put(
item={
'id': new_image['id'],
'sk': 'metadata#tenant',
'tenant_id': f'ORG#{ids["org_id"]}',
'create_date': now_,
}
)
for k, v in ids.items():
kind = k.removesuffix('_id')
transact.put(
item={
'id': new_image['id'],
'sk': f'related_ids#{kind}', # e.g. related_ids#user
'create_date': now_,
k: v,
}
)
return True

View File

@@ -1,57 +0,0 @@
from aws_lambda_powertools import Logger
from aws_lambda_powertools.utilities.data_classes import (
EventBridgeEvent,
event_source,
)
from aws_lambda_powertools.utilities.typing import LambdaContext
from layercake.dynamodb import (
ComposeKey,
DynamoDBPersistenceLayer,
KeyPair,
SortKey,
)
from boto3clients import dynamodb_client
from config import ENROLLMENT_TABLE, ORDER_TABLE
logger = Logger(__name__)
enrollment_layer = DynamoDBPersistenceLayer(ENROLLMENT_TABLE, dynamodb_client)
order_layer = DynamoDBPersistenceLayer(ORDER_TABLE, dynamodb_client)
class TenantDoesNotExistError(Exception):
def __init__(self, *args):
super().__init__('Tenant does not exist')
@event_source(data_class=EventBridgeEvent)
@logger.inject_lambda_context
def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool:
new_image = event.detail['new_image']
order_id = new_image['id']
tenant_id = order_layer.collection.get_item(
KeyPair(
order_id,
SortKey('metadata#tenant', path_spec='tenant_id'),
),
exc_cls=TenantDoesNotExistError,
)
result = enrollment_layer.collection.query(
KeyPair(
# Post-migration: rename `vacancies` to `slots`
ComposeKey(tenant_id, prefix='vacancies'),
order_id,
)
)
with enrollment_layer.batch_writer() as batch:
for pair in result['items']:
batch.delete_item(
Key={
# Post-migration: rename `vacancies` to `slots`
'id': {'S': ComposeKey(pair['id'], prefix='vacancies')},
'sk': {'S': pair['sk']},
}
)
return True

View File

@@ -1,4 +0,0 @@
"""
Stopgap events. Everything here is a quick fix and should be replaced with
proper solutions.
"""

View File

@@ -1,71 +0,0 @@
from aws_lambda_powertools import Logger
from aws_lambda_powertools.utilities.data_classes import (
EventBridgeEvent,
event_source,
)
from aws_lambda_powertools.utilities.typing import LambdaContext
from layercake.dynamodb import (
ComposeKey,
DynamoDBPersistenceLayer,
KeyPair,
SortKey,
TransactKey,
)
from boto3clients import dynamodb_client
from config import ENROLLMENT_TABLE, ORDER_TABLE, USER_TABLE
logger = Logger(__name__)
user_layer = DynamoDBPersistenceLayer(USER_TABLE, dynamodb_client)
order_layer = DynamoDBPersistenceLayer(ORDER_TABLE, dynamodb_client)
enrollment_layer = DynamoDBPersistenceLayer(ENROLLMENT_TABLE, dynamodb_client)
@event_source(data_class=EventBridgeEvent)
@logger.inject_lambda_context
def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool:
"""Remove slots if the Tenant has a `metadata#billing_policy` and
the order is positive."""
new_image = event.detail['new_image']
order_id = new_image['id']
data = order_layer.collection.get_items(
TransactKey(order_id)
+ SortKey('0')
+ KeyPair(
pk=order_id,
sk=SortKey(
sk='metadata#tenant',
path_spec='tenant_id',
remove_prefix='metadata#',
),
rename_key='tenant_id',
)
)
tenant_id = data['tenant_id'].removeprefix('ORG#')
policy = user_layer.collection.get_item(
KeyPair(pk=tenant_id, sk='metadata#billing_policy'),
raise_on_error=False,
default=False,
)
# Skip if missing billing policy or order is zero/negative
if not policy or data['total'] <= 0:
return False
result = enrollment_layer.collection.query(
KeyPair(
ComposeKey(tenant_id, prefix='vacancies'),
order_id,
)
)
with enrollment_layer.batch_writer() as batch:
for pair in result['items']:
batch.delete_item(
Key={
'id': {'S': ComposeKey(pair['id'], prefix='vacancies')},
'sk': {'S': pair['sk']},
}
)
return True

View File

@@ -1,46 +0,0 @@
from aws_lambda_powertools import Logger
from aws_lambda_powertools.utilities.data_classes import (
EventBridgeEvent,
event_source,
)
from aws_lambda_powertools.utilities.typing import LambdaContext
from layercake.dateutils import now
from layercake.dynamodb import (
DynamoDBPersistenceLayer,
KeyPair,
)
from boto3clients import dynamodb_client
from config import ORDER_TABLE
logger = Logger(__name__)
order_layer = DynamoDBPersistenceLayer(ORDER_TABLE, dynamodb_client)
@event_source(data_class=EventBridgeEvent)
@logger.inject_lambda_context
def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool:
new_image = event.detail['new_image']
now_ = now()
with order_layer.transact_writer() as transact:
transact.update(
key=KeyPair(new_image['id'], '0'),
update_expr='SET #status = :status, update_date = :update_date',
expr_attr_names={
'#status': 'status',
},
expr_attr_values={
':status': 'PAID',
':update_date': now_,
},
)
transact.put(
item={
'id': new_image['id'],
'sk': 'paid_date',
'create_date': now_,
}
)
return True

View File

@@ -1,33 +0,0 @@
[project]
name = "order-management"
version = "0.1.0"
description = ""
readme = ""
requires-python = ">=3.13"
dependencies = ["layercake"]
[dependency-groups]
dev = [
"jsonlines>=4.0.0",
"pytest>=8.3.4",
"pytest-cov>=6.0.0",
"ruff>=0.9.1",
]
[tool.pytest.ini_options]
pythonpath = ["app/"]
addopts = "--cov --cov-report html -v"
[tool.ruff]
target-version = "py311"
src = ["app"]
[tool.ruff.format]
quote-style = "single"
[tool.ruff.lint]
select = ["E", "F", "I"]
[tool.uv.sources]
layercake = { path = "../layercake" }

View File

@@ -1,3 +0,0 @@
{
"extraPaths": ["app/"]
}

View File

@@ -1,9 +0,0 @@
version = 0.1
[default.deploy.parameters]
stack_name = "saladeaula-order-management"
resolve_s3 = true
s3_prefix = "order_management"
region = "sa-east-1"
confirm_changeset = false
capabilities = "CAPABILITY_IAM"
image_repositories = []

View File

@@ -1,139 +0,0 @@
AWSTemplateFormatVersion: 2010-09-09
Transform: AWS::Serverless-2016-10-31
Parameters:
UserTable:
Type: String
Default: betaeducacao-prod-users_d2o3r5gmm4it7j
EnrollmentTable:
Type: String
Default: betaeducacao-prod-enrollments
OrderTable:
Type: String
Default: betaeducacao-prod-orders
Globals:
Function:
CodeUri: app/
Runtime: python3.13
Tracing: Active
Architectures:
- x86_64
Layers:
- !Sub arn:aws:lambda:sa-east-1:336641857101:layer:layercake:78
Environment:
Variables:
TZ: America/Sao_Paulo
LOG_LEVEL: DEBUG
DYNAMODB_PARTITION_KEY: id
POWERTOOLS_LOGGER_SAMPLE_RATE: 0.1
POWERTOOLS_LOGGER_LOG_EVENT: true
USER_TABLE: !Ref UserTable
ORDER_TABLE: !Ref OrderTable
ENROLLMENT_TABLE: !Ref EnrollmentTable
Resources:
EventLog:
Type: AWS::Logs::LogGroup
Properties:
RetentionInDays: 90
EventAssignTenantCnpjFunction:
Type: AWS::Serverless::Function
Properties:
Handler: events.assign_tenant_cnpj.lambda_handler
LoggingConfig:
LogGroup: !Ref EventLog
Policies:
- DynamoDBCrudPolicy:
TableName: !Ref UserTable
- DynamoDBCrudPolicy:
TableName: !Ref OrderTable
Events:
Event:
Type: EventBridgeRule
Properties:
Pattern:
resources: [!Ref OrderTable]
detail-type: [INSERT]
detail:
new_image:
sk: ["0"]
cnpj:
- exists: true
metadata__tenant_id:
- exists: false
EventRemoveSlotsOnCanceledFunction:
Type: AWS::Serverless::Function
Properties:
Handler: events.delete_slots_on_canceled.lambda_handler
LoggingConfig:
LogGroup: !Ref EventLog
Policies:
- DynamoDBWritePolicy:
TableName: !Ref OrderTable
- DynamoDBWritePolicy:
TableName: !Ref EnrollmentTable
Events:
Event:
Type: EventBridgeRule
Properties:
Pattern:
resources: [!Ref OrderTable]
detail-type: [MODIFY]
detail:
new_image:
sk: ["0"]
cnpj:
- exists: true
status: [CANCELED, EXPIRED]
EventSetAsPaidFunction:
Type: AWS::Serverless::Function
Properties:
Handler: events.stopgap.set_as_paid.lambda_handler
LoggingConfig:
LogGroup: !Ref EventLog
Policies:
- DynamoDBWritePolicy:
TableName: !Ref OrderTable
Events:
Event:
Type: EventBridgeRule
Properties:
Pattern:
resources: [!Ref OrderTable]
detail-type: [INSERT]
detail:
new_image:
sk: ["0"]
cnpj:
- exists: true
total: [0]
status: [CREATING, PENDING]
payment_method: [MANUAL]
EventRemoveSlotsFunction:
Type: AWS::Serverless::Function
Properties:
Handler: events.stopgap.remove_slots.lambda_handler
LoggingConfig:
LogGroup: !Ref EventLog
Policies:
- DynamoDBReadPolicy:
TableName: !Ref UserTable
- DynamoDBReadPolicy:
TableName: !Ref OrderTable
- DynamoDBCrudPolicy:
TableName: !Ref EnrollmentTable
Events:
DynamoDBEvent:
Type: EventBridgeRule
Properties:
Pattern:
resources: [!Ref OrderTable]
detail:
new_image:
sk: [generated_items]
status: [SUCCESS]

View File

@@ -1,72 +0,0 @@
import os
from dataclasses import dataclass
import jsonlines
import pytest
PYTEST_TABLE_NAME = 'pytest'
PK = 'id'
SK = 'sk'
# https://docs.pytest.org/en/7.1.x/reference/reference.html#pytest.hookspec.pytest_configure
def pytest_configure():
os.environ['TZ'] = 'America/Sao_Paulo'
os.environ['DYNAMODB_PARTITION_KEY'] = PK
os.environ['DYNAMODB_SORT_KEY'] = SK
os.environ['USER_TABLE'] = PYTEST_TABLE_NAME
os.environ['COURSE_TABLE'] = PYTEST_TABLE_NAME
os.environ['ENROLLMENT_TABLE'] = PYTEST_TABLE_NAME
os.environ['ORDER_TABLE'] = PYTEST_TABLE_NAME
@dataclass
class LambdaContext:
function_name: str = 'test'
memory_limit_in_mb: int = 128
invoked_function_arn: str = 'arn:aws:lambda:eu-west-1:809313241:function:test'
aws_request_id: str = '52fdfc07-2182-154f-163f-5f0f9a621d72'
@pytest.fixture
def lambda_context() -> LambdaContext:
return LambdaContext()
@pytest.fixture
def dynamodb_client():
from boto3clients import dynamodb_client as client
client.create_table(
AttributeDefinitions=[
{'AttributeName': PK, 'AttributeType': 'S'},
{'AttributeName': SK, 'AttributeType': 'S'},
],
TableName=PYTEST_TABLE_NAME,
KeySchema=[
{'AttributeName': PK, 'KeyType': 'HASH'},
{'AttributeName': SK, 'KeyType': 'RANGE'},
],
ProvisionedThroughput={
'ReadCapacityUnits': 123,
'WriteCapacityUnits': 123,
},
)
yield client
client.delete_table(TableName=PYTEST_TABLE_NAME)
@pytest.fixture()
def dynamodb_persistence_layer(dynamodb_client):
from layercake.dynamodb import DynamoDBPersistenceLayer
return DynamoDBPersistenceLayer(PYTEST_TABLE_NAME, dynamodb_client)
@pytest.fixture()
def dynamodb_seeds(dynamodb_client):
with jsonlines.open('tests/seeds.jsonl') as lines:
for line in lines:
dynamodb_client.put_item(TableName=PYTEST_TABLE_NAME, Item=line)

View File

@@ -1,30 +0,0 @@
from layercake.dynamodb import PartitionKey
import events.stopgap.remove_slots as app
from ...conftest import LambdaContext
def test_remove_slots(
dynamodb_seeds,
dynamodb_persistence_layer,
lambda_context: LambdaContext,
):
event = {
'detail': {
'new_image': {
'id': '9omWNKymwU5U4aeun6mWzZ',
'sk': 'generated_items',
'create_date': '2024-07-23T20:43:37.303418-03:00',
'status': 'SUCCESS',
'scope': 'MILTI_USER',
}
},
}
assert app.lambda_handler(event, lambda_context) # type: ignore
result = dynamodb_persistence_layer.collection.query(
PartitionKey('vacancies#cJtK9SsnJhKPyxESe7g3DG')
)
assert len(result['items']) == 0

View File

@@ -1,24 +0,0 @@
from aws_lambda_powertools.utilities.typing import LambdaContext
from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair
import events.stopgap.set_as_paid as app
def test_set_as_paid(
dynamodb_seeds,
dynamodb_persistence_layer: DynamoDBPersistenceLayer,
lambda_context: LambdaContext,
):
event = {
'detail': {
'new_image': {
'id': '9omWNKymwU5U4aeun6mWzZ',
}
}
}
assert app.lambda_handler(event, lambda_context) # type: ignore
doc = dynamodb_persistence_layer.get_item(
key=KeyPair('9omWNKymwU5U4aeun6mWzZ', '0'),
)
assert doc['status'] == 'PAID'

View File

@@ -1,28 +0,0 @@
from aws_lambda_powertools.utilities.typing import LambdaContext
from layercake.dynamodb import DynamoDBPersistenceLayer, PartitionKey
import events.assign_tenant_cnpj as app
def test_assign_tenant_cnpj(
dynamodb_seeds,
dynamodb_persistence_layer: DynamoDBPersistenceLayer,
lambda_context: LambdaContext,
):
event = {
'detail': {
'new_image': {
'id': '9omWNKymwU5U4aeun6mWzZ',
'cnpj': '15608435000190',
'email': 'sergio@somosbeta.com.br',
}
}
}
assert app.lambda_handler(event, lambda_context) # type: ignore
result = dynamodb_persistence_layer.collection.query(
PartitionKey('9omWNKymwU5U4aeun6mWzZ')
)
assert 4 == len(result['items'])

View File

@@ -1,29 +0,0 @@
from aws_lambda_powertools.utilities.typing import LambdaContext
from layercake.dynamodb import DynamoDBPersistenceLayer, PartitionKey
import events.assign_tenant_cnpj as app
def test_assign_tenant_cnpj(
dynamodb_seeds,
dynamodb_persistence_layer: DynamoDBPersistenceLayer,
lambda_context: LambdaContext,
):
event = {
'detail': {
'new_image': {
'id': '9omWNKymwU5U4aeun6mWzZ',
'cnpj': '15608435000190',
'email': 'sergio@somosbeta.com.br',
}
}
}
assert app.lambda_handler(event, lambda_context) # type: ignore
result = dynamodb_persistence_layer.collection.query(
PartitionKey('9omWNKymwU5U4aeun6mWzZ')
)
assert 4 == len(result['items'])
print(result['items'])

View File

@@ -1,27 +0,0 @@
from aws_lambda_powertools.utilities.typing import LambdaContext
from layercake.dynamodb import DynamoDBPersistenceLayer, PartitionKey
import events.remove_slots_on_canceled as app
def test_delete_slots_on_canceled(
dynamodb_seeds,
dynamodb_persistence_layer: DynamoDBPersistenceLayer,
lambda_context: LambdaContext,
):
event = {
'detail': {
'new_image': {
'id': '9omWNKymwU5U4aeun6mWzZ',
'status': 'CANCELED',
}
}
}
assert app.lambda_handler(event, lambda_context) # type: ignore
result = dynamodb_persistence_layer.collection.query(
PartitionKey('vacancies#cJtK9SsnJhKPyxESe7g3DG')
)
assert len(result['items']) == 0

View File

@@ -1,10 +0,0 @@
{"id": {"S": "cJtK9SsnJhKPyxESe7g3DG"}, "sk": {"S": "metadata#payment_policy"}, "due_days": {"N": "90"}}
{"id": {"S": "cJtK9SsnJhKPyxESe7g3DG"}, "sk": {"S": "metadata#billing_policy"}, "billing_day": {"N": "1"}, "payment_method": {"S": "PIX"}}
{"id": {"S": "9omWNKymwU5U4aeun6mWzZ"}, "sk": {"S": "0"}, "total": {"N": "398"}, "status": {"S": "PENDING"}}
{"id": {"S": "9omWNKymwU5U4aeun6mWzZ"}, "sk": {"S": "metadata#tenant"}, "tenant_id": {"S": "cJtK9SsnJhKPyxESe7g3DG"}}
{"id": {"S": "cnpj"}, "sk": {"S": "15608435000190"}, "user_id": {"S": "cJtK9SsnJhKPyxESe7g3DG"}}
{"id": {"S": "email"}, "sk": {"S": "sergio@somosbeta.com.br"}, "user_id": {"S": "5OxmMjL-ujoR5IMGegQz"}}
{"id": {"S": "5OxmMjL-ujoR5IMGegQz"}, "sk": {"S": "0"}, "name": {"S": "Sérgio R Siqueira"}}
{"id": {"S": "vacancies#cJtK9SsnJhKPyxESe7g3DG"}, "sk": {"S": "9omWNKymwU5U4aeun6mWzZ#1"}}
{"id": {"S": "vacancies#cJtK9SsnJhKPyxESe7g3DG"}, "sk": {"S": "9omWNKymwU5U4aeun6mWzZ#2"}}
{"id": {"S": "vacancies#cJtK9SsnJhKPyxESe7g3DG"}, "sk": {"S": "9omWNKymwU5U4aeun6mWzZ#3"}}

1113
order-management/uv.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,11 +0,0 @@
build:
sam build --use-container
deploy: build
sam deploy --debug
pytest:
uv run pytest
htmlcov: pytest
uv run python -m http.server 80 -d htmlcov

View File

@@ -1,4 +0,0 @@
import os
MEILISEARCH_HOST: str = os.getenv('MEILISEARCH_HOST') # type: ignore
MEILISEARCH_API_KEY: str = os.getenv('MEILISEARCH_API_KEY') # type: ignore

View File

@@ -1,32 +0,0 @@
from arnparse import arnparse
from aws_lambda_powertools.utilities.data_classes import (
DynamoDBStreamEvent,
event_source,
)
from aws_lambda_powertools.utilities.typing import LambdaContext
from meilisearch import Client as Meilisearch
from config import MEILISEARCH_API_KEY, MEILISEARCH_HOST
from meili import Op
meili_client = Meilisearch(MEILISEARCH_HOST, MEILISEARCH_API_KEY)
@event_source(data_class=DynamoDBStreamEvent)
def lambda_handler(event: DynamoDBStreamEvent, context: LambdaContext):
with Op(meili_client) as op:
for record in event.records:
pk = record.dynamodb.keys['id'] # type: ignore
new_image = record.dynamodb.new_image # type: ignore
index = table_from_arn(record.event_source_arn) # type: ignore
op.append(
index,
op=record.event_name, # type: ignore
data=new_image or pk,
)
def table_from_arn(arn: str) -> str:
arn_ = arnparse(arn)
return arn_.resource.split('/')[0]

View File

@@ -1,56 +0,0 @@
from typing import Self
from aws_lambda_powertools.shared.json_encoder import Encoder
from aws_lambda_powertools.utilities.data_classes.dynamo_db_stream_event import (
DynamoDBRecordEventName,
)
from meilisearch import Client
class Op:
def __init__(self, client: Client) -> None:
self.op = {}
self.client = client
def __enter__(self) -> Self:
return self
def __exit__(self, exc_type, exc_val, exc_tb):
op = self.op
client = self.client
for index_, ops in op.items():
index = client.index(index_)
for op, doc in ops.items():
match op:
case DynamoDBRecordEventName.INSERT:
index.add_documents(doc, serializer=JSONEncoder)
case DynamoDBRecordEventName.MODIFY:
index.update_documents(doc, serializer=JSONEncoder)
case DynamoDBRecordEventName.REMOVE:
index.delete_documents(doc)
self.op = {}
def append(
self,
index: str,
/,
op: DynamoDBRecordEventName,
data: dict | str,
) -> bool:
if index not in self.op:
self.op[index] = {}
if op not in self.op[index]:
self.op[index][op] = []
return self.op[index][op].append(data)
class JSONEncoder(Encoder):
def default(self, obj):
if isinstance(obj, set):
return list(obj)
return super(__class__, self).default(obj)

View File

@@ -1,23 +0,0 @@
[project]
name = "streams"
version = "0.1.0"
description = "Streaming DynamoDB events to Meilisearch and EventBridge."
readme = ""
requires-python = ">=3.12"
dependencies = ["layercake"]
[dependency-groups]
dev = [
"pytest>=8.3.4",
"pytest-cov>=6.0.0",
"ruff>=0.9.1",
]
[tool.pytest.ini_options]
addopts = "--cov --cov-report html -v"
[tool.ruff.format]
quote-style = "single"
[tool.uv.sources]
layercake = { path = "../layercake" }

View File

@@ -1,9 +0,0 @@
version = 0.1
[default.deploy.parameters]
stack_name = "saladeaula-streams"
resolve_s3 = true
s3_prefix = "streams"
region = "sa-east-1"
confirm_changeset = false
capabilities = "CAPABILITY_IAM"
image_repositories = []

View File

@@ -1,54 +0,0 @@
AWSTemplateFormatVersion: 2010-09-09
Transform: AWS::Serverless-2016-10-31
Globals:
Function:
CodeUri: .
Runtime: python3.13
Architectures:
- x86_64
Layers:
- !Sub arn:aws:lambda:sa-east-1:336641857101:layer:layercake:75
Environment:
Variables:
LOG_LEVEL: DEBUG
TZ: America/Sao_Paulo
POWERTOOLS_LOGGER_SAMPLE_RATE: 0.1
POWERTOOLS_LOGGER_LOG_EVENT: true
MEILISEARCH_HOST: https://meili.eduseg.com.br
MEILISEARCH_API_KEY: "{{resolve:ssm:/saladeaula/meili_api_key}}"
Resources:
MeilisearchLog:
Type: AWS::Logs::LogGroup
Properties:
RetentionInDays: 90
EventIndexDocsFunction:
Type: AWS::Serverless::Function
Properties:
Handler: events.index_docs.lambda_handler
LoggingConfig:
LogGroup: !Ref MeilisearchLog
Events:
Enrollments:
Type: DynamoDB
Properties:
Stream: !Sub arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/saladeaula_enrollments/stream/2025-06-04T16:44:42.524
StartingPosition: LATEST
MaximumRetryAttempts: 5
BatchSize: 25
FilterCriteria:
Filters:
- Pattern: '{ "dynamodb" : { "Keys" : { "sk" : { "S" : [ "0" ] } } } }'
Courses:
Type: DynamoDB
Properties:
Stream: !Sub arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/saladeaula_courses/stream/2025-03-12T20:42:46.706
StartingPosition: LATEST
MaximumRetryAttempts: 5
BatchSize: 25
FilterCriteria:
Filters:
- Pattern: '{ "dynamodb" : { "Keys" : { "sk" : { "S" : [ "0" ] } } } }'

View File

@@ -1,34 +0,0 @@
import json
import os
from dataclasses import dataclass
import pytest
# 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['MEILISEARCH_HOST'] = 'http://127.0.0.1:7700'
def load_jsonfile(path: str) -> dict:
with open(path) as fp:
return json.load(fp)
@dataclass
class LambdaContext:
function_name: str = 'test'
memory_limit_in_mb: int = 128
invoked_function_arn: str = 'arn:aws:lambda:eu-west-1:809313241:function:test'
aws_request_id: str = '52fdfc07-2182-154f-163f-5f0f9a621d72'
@pytest.fixture
def lambda_context() -> LambdaContext:
return LambdaContext()
@pytest.fixture
def dynamodb_stream_event():
return load_jsonfile('tests/samples/dynamodb_stream_event.json')

View File

@@ -1,212 +0,0 @@
{
"Records": [
{
"eventID": "c4ca4238a0b923820dcc509a6f75849b",
"eventName": "INSERT",
"eventVersion": "1.1",
"eventSource": "aws:dynamodb",
"awsRegion": "us-east-1",
"dynamodb": {
"Keys": {
"id": {
"S": "102"
}
},
"NewImage": {
"message": {
"S": "New item!!"
},
"id": {
"S": "102"
},
"cpf": {
"NULL": true
},
"tenant:org_id": {
"SS": ["5OxmMjL-ujoR5IMGegQz"]
}
},
"ApproximateCreationDateTime": 1428537600,
"SequenceNumber": "4421584500000000017450439091",
"SizeBytes": 26,
"StreamViewType": "NEW_AND_OLD_IMAGES"
},
"eventSourceARN": "arn:aws:dynamodb:us-east-1:123456789012:table/example_table_with_stream/stream/2015-06-27T00:48:05.899"
},
{
"eventID": "c4ca4238a0b923820dcc509a6f75849b",
"eventName": "INSERT",
"eventVersion": "1.1",
"eventSource": "aws:dynamodb",
"awsRegion": "us-east-1",
"dynamodb": {
"Keys": {
"id": {
"S": "102"
}
},
"NewImage": {
"message": {
"S": "New item!"
},
"id": {
"S": "101"
},
"cpf": {
"NULL": true
}
},
"ApproximateCreationDateTime": 1428537600,
"SequenceNumber": "4421584500000000017450439091",
"SizeBytes": 26,
"StreamViewType": "NEW_AND_OLD_IMAGES"
},
"eventSourceARN": "arn:aws:dynamodb:us-east-1:123456789012:table/example_table_with_stream/stream/2015-06-27T00:48:05.899"
},
{
"eventID": "c81e728d9d4c2f636f067f89cc14862c",
"eventName": "MODIFY",
"eventVersion": "1.1",
"eventSource": "aws:dynamodb",
"awsRegion": "us-east-1",
"dynamodb": {
"Keys": {
"id": {
"S": "101"
}
},
"NewImage": {
"message": {
"S": "This item has changed"
},
"id": {
"S": "101"
},
"assignee": {
"M": {
"name": {
"S": "Sérgio R Siqueira"
}
}
},
"cpf": {
"S": "07879819908"
}
},
"OldImage": {
"message": {
"S": "New item!"
},
"id": {
"S": "101"
},
"assignee": {
"M": {
"name": {
"S": "Sérgio R Siqueira"
}
}
}
},
"ApproximateCreationDateTime": 1428537600,
"SequenceNumber": "4421584500000000017450439092",
"SizeBytes": 59,
"StreamViewType": "NEW_AND_OLD_IMAGES"
},
"eventSourceARN": "arn:aws:dynamodb:us-east-1:123456789012:table/example_table_with_stream/stream/2015-06-27T00:48:05.899"
},
{
"eventID": "eccbc87e4b5ce2fe28308fd9f2a7baf3",
"eventName": "REMOVE",
"eventVersion": "1.1",
"eventSource": "aws:dynamodb",
"awsRegion": "us-east-1",
"dynamodb": {
"Keys": {
"id": {
"S": "101"
}
},
"OldImage": {
"message": {
"S": "This item has changed"
},
"id": {
"S": "101"
},
"ttl": {
"N": "1710532240"
}
},
"ApproximateCreationDateTime": 1428537600,
"SequenceNumber": "4421584500000000017450439093",
"SizeBytes": 38,
"StreamViewType": "NEW_AND_OLD_IMAGES"
},
"eventSourceARN": "arn:aws:dynamodb:us-east-1:123456789012:table/example_table_with_stream/stream/2015-06-27T00:48:05.899"
},
{
"eventID": "eccbc87e4b5ce2fe28308fd9f2a7baf3",
"eventName": "REMOVE",
"eventVersion": "1.1",
"eventSource": "aws:dynamodb",
"awsRegion": "us-east-1",
"dynamodb": {
"Keys": {
"id": {
"S": "102"
}
},
"OldImage": {
"message": {
"S": "This item has changed"
},
"id": {
"S": "102"
},
"ttl": {
"N": "2530997445"
}
},
"ApproximateCreationDateTime": 1428537600,
"SequenceNumber": "4421584500000000017450439093",
"SizeBytes": 38,
"StreamViewType": "NEW_AND_OLD_IMAGES"
},
"eventSourceARN": "arn:aws:dynamodb:us-east-1:123456789012:table/example_table_with_stream/stream/2015-06-27T00:48:05.899"
},
{
"eventID": "bbb152116867ab05f3abfcadd4873bee",
"eventName": "REMOVE",
"eventVersion": "1.1",
"eventSource": "aws:dynamodb",
"awsRegion": "sa-east-1",
"dynamodb": {
"ApproximateCreationDateTime": 1710529909,
"Keys": {
"sk": {
"S": "0"
},
"id": {
"S": "DwHRXCm5bE64rcu5VA6ai6"
}
},
"OldImage": {
"sk": {
"S": "0"
},
"id": {
"S": "DwHRXCm5bE64rcu5VA6ai6"
},
"createDate": {
"S": "2024-03-15T15:44:30.374640-03:00"
}
},
"SequenceNumber": "3173521300000000009361288070",
"SizeBytes": 156,
"StreamViewType": "NEW_AND_OLD_IMAGES"
},
"eventSourceARN": "arn:aws:dynamodb:sa-east-1:336641857101:table/betaeducacao-prod-users_d2o3r5gmm4it7j/stream/2022-06-12T21:33:25.634"
}
]
}

View File

@@ -1,5 +0,0 @@
import events.index_docs as app
def test_record_handler(monkeypatch, dynamodb_stream_event, lambda_context):
app.lambda_handler(dynamodb_stream_event, lambda_context)

1147
streams/uv.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +0,0 @@
build:
sam build --use-container
deploy: build
sam deploy --debug

View File

@@ -1,14 +0,0 @@
import os
import boto3
def get_dynamodb_client():
if os.getenv('AWS_LAMBDA_FUNCTION_NAME'):
return boto3.client('dynamodb')
return boto3.client('dynamodb', endpoint_url='http://127.0.0.1:8000')
dynamodb_client = get_dynamodb_client()
s3_client = boto3.client('s3')

View File

@@ -1,4 +0,0 @@
import os
USER_TABLE: str = os.getenv('USER_TABLE') # type: ignore
CHUNK_SIZE = 50

View File

@@ -1,83 +0,0 @@
import csv
from typing import TextIO
from smart_open import open
def byte_ranges(
csvfile: str,
chunk_size: int = 100,
**kwargs,
) -> list[tuple[int, int]]:
"""Compute byte ranges for reading a CSV file in fixed-size line chunks.
Returns pairs (start_byte, end_byte) for each fixed-size group of lines.
Parameters
----------
csvfile : str
Path to the CSV file, opened in binary mode internally.
chunk_size : int, optional
Number of lines per chunk. Default is 100.
**kwargs :
Extra options passed to `open()`, e.g., buffering.
Returns
-------
list of tuple[int, int]
Byte ranges covering each chunk of lines.
Example
-------
>>> byte_ranges("users.csv", chunk_size=500)
[(0, 3125), (3126, 6150), (6151, 9124)]
"""
line_offsets = [0]
with open(csvfile, 'rb', **kwargs) as fp:
while True:
if not fp.readline():
break
line_offsets.append(fp.tell())
total_lines = len(line_offsets) - 1
byte_ranges = []
for start_line in range(1, total_lines + 1, chunk_size):
# Calculate the end line index, bounded by total lines
end_line = min(start_line + chunk_size - 1, total_lines)
# Get byte range for this chunk
start_byte = line_offsets[start_line - 1]
end_byte = line_offsets[end_line] - 1
byte_ranges.append((start_byte, end_byte))
return byte_ranges
def detect_delimiter(sample: TextIO) -> str:
"""Detect the delimiter character used in a CSV file.
Parameters
----------
sample : TextIO
A file-like object opened in text mode (e.g., from `open('file.csv')`).
Must be readable and at position 0.
Returns
-------
str
The detected delimiter character (e.g., ',', ';', '\\t').
Raises
------
csv.Error
If the file cannot be parsed as CSV or delimiter detection fails.
ValueError
If the file is empty or contains no detectable delimiter.
"""
sniffer = csv.Sniffer()
dialect = sniffer.sniff(sample.read())
sample.seek(0)
return dialect.delimiter

View File

@@ -1,20 +0,0 @@
from aws_lambda_powertools.utilities.data_classes import (
EventBridgeEvent,
event_source,
)
from aws_lambda_powertools.utilities.typing import LambdaContext
from boto3clients import s3_client
from config import CHUNK_SIZE
from csv_utils import byte_ranges
transport_params = {'client': s3_client}
@event_source(data_class=EventBridgeEvent)
def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool:
new_image = event.detail['new_image']
csvfile = new_image['s3uri']
pairs = byte_ranges(csvfile, CHUNK_SIZE, transport_params=transport_params)
return True

View File

@@ -1,14 +0,0 @@
from aws_lambda_powertools.utilities.data_classes import (
EventBridgeEvent,
event_source,
)
from aws_lambda_powertools.utilities.typing import LambdaContext
from boto3clients import s3_client
transport_params = {'client': s3_client}
@event_source(data_class=EventBridgeEvent)
def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool:
return True

View File

@@ -1,55 +0,0 @@
import csv
from io import StringIO
from typing import TYPE_CHECKING
from aws_lambda_powertools.utilities.data_classes import (
EventBridgeEvent,
event_source,
)
from aws_lambda_powertools.utilities.typing import LambdaContext
from boto3clients import s3_client
if TYPE_CHECKING:
from mypy_boto3_s3.client import S3Client
else:
S3Client = object
transport_params = {'client': s3_client}
@event_source(data_class=EventBridgeEvent)
def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool:
new_image = event.detail['new_image']
csvfile = new_image['s3_uri']
data = _get_s3_object_range(
csvfile,
start_byte=new_image['start_byte'],
end_byte=new_image['end_byte'],
s3_client=s3_client,
)
reader = csv.reader(data)
for x in reader:
print(x)
return True
def _get_s3_object_range(
s3_uri: str,
*,
start_byte: int,
end_byte: int,
s3_client: S3Client,
) -> StringIO:
bucket, key = s3_uri.replace('s3://', '').split('/', 1)
response = s3_client.get_object(
Bucket=bucket,
Key=key,
Range=f'bytes={start_byte}-{end_byte}',
)
return StringIO(response['Body'].read().decode('utf-8'))

View File

@@ -1,40 +0,0 @@
import urllib.parse as urllib_parse
from email.utils import parseaddr
from aws_lambda_powertools import Logger
from aws_lambda_powertools.utilities.data_classes import SESEvent, event_source
from aws_lambda_powertools.utilities.typing import LambdaContext
from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair, SortKey
from boto3clients import dynamodb_client
from config import USER_TABLE
from ses_utils import get_header_value
logger = Logger(__name__)
user_layer = DynamoDBPersistenceLayer(USER_TABLE, dynamodb_client)
@logger.inject_lambda_context
@event_source(data_class=SESEvent)
def lambda_handler(event: SESEvent, context: LambdaContext) -> dict:
ses = event.record.ses
to = urllib_parse.unquote(ses.receipt.recipients[0]).lower()
name, email_from = parseaddr(get_header_value(ses.mail.headers, 'from'))
org_id = user_layer.collection.get_item(
KeyPair('email', SortKey(to, path_spec='user_id')),
raise_on_error=False,
default={},
)
if not org_id:
return {'disposition': 'STOP_RULE_SET'}
print(
{
'id': f'mailbox#{org_id}',
'sk': ses.mail.message_id,
}
)
return {'disposition': 'CONTINUE'}

View File

@@ -1,20 +0,0 @@
from typing import Any, Iterator
from aws_lambda_powertools.utilities.data_classes.ses_event import SESMailHeader
def get_header_value(
headers: Iterator[SESMailHeader],
header_name: str,
*,
default: Any = None,
raise_on_missing: bool = True,
) -> str:
for header in headers:
if header.name.lower() == header_name:
return header.value
if raise_on_missing:
raise ValueError(f'{header_name} not found.')
return default

View File

@@ -1,62 +0,0 @@
# /// script
# dependencies = [
# "cloudflare"
# ]
# ///
from cloudflare import Cloudflare
CLOUDFLARE_ACCOUNT_ID = '5436b62470020c04b434ad31c3e4cf4e'
CLOUDFLARE_API_TOKEN = 'gFndkBJCzH4pRX7mKXokdWfw1xhm8-9FHfvLfhwa'
client = Cloudflare(api_token=CLOUDFLARE_API_TOKEN)
assistant = """
You are a data analysis assistant specialized in identifying Brazilian
personal data from CSV files.
These CSV files may or may not include headers.
Your task is to analyze the content and identify only three possible
data types: 'name', 'cpf', and 'email'.
Ignore all other fields.
"""
csv_content = """
,RICARDO GALLES BONET,ricardo.bonet@fanucamerica.com,424.430.528-93,NR-10 (RECICLAGEM)
,RULIO SIEFERT SERA,rulio.sera@fanucamerica.com,063.916.859-08,NR-10 (RECICLAGEM)
,MACIEL FERREIRA BOMFIM,maciel.bomfim@fanucamerica.com,334.547.088-85,NR-10 (RECICLAGEM)
,JAIME EDUARDO GALVEZ AVILES,jaime.galvez@fanucamerica.com,280.238.818-50,NR-12
,JAIME EDUARDO GALVEZ AVILES,jaime.galvez@fanucamerica.com,280.238.818-50,NR-35 (RECICLAGEM)
,HIGOR MACHADO SILVA,higor.silva@fanucamerica.com,419.879.878-88,NR-12
,LÁZARO SOUZA DIAS,lazaro.dias@fanucamerica.com,067.179.825-19,NR-12
,JOÃO PEDRO AGUIAR GALASSO,joao.pedro@fanucamerica.com,570.403.588-40,NR-12
"""
prompt = f"""
Here is a CSV sample:
{csv_content}
Your task is to:
- Detect which columns most likely contain "name", "cpf", or "email".
- Skip any category that is not present in the data.
- Return ONLY a valid Python list of tuples, like:
[('name', index), ('cpf', index), ('email', index)]
- Use the column index that most likely matches each data type,
based on frequency and data format.
- Don't include explanations, code, or any additional text.
"""
r = client.ai.run(
model_name='@cf/meta/llama-3-8b-instruct',
account_id=CLOUDFLARE_ACCOUNT_ID,
messages=[
{'role': 'system', 'content': assistant},
{'role': 'user', 'content': prompt},
],
)
print(r)

View File

@@ -1,33 +0,0 @@
[project]
name = "user-management"
version = "0.1.0"
description = ""
readme = ""
requires-python = ">=3.13"
dependencies = ["layercake"]
[dependency-groups]
dev = [
"boto3-stubs[essential]>=1.38.26",
"jsonlines>=4.0.0",
"pytest>=8.3.4",
"pytest-cov>=6.0.0",
"ruff>=0.9.1",
]
[tool.pytest.ini_options]
pythonpath = ["app/"]
addopts = "--cov --cov-report html -v"
[tool.ruff]
target-version = "py311"
src = ["app"]
[tool.ruff.format]
quote-style = "single"
[tool.ruff.lint]
select = ["E", "F", "I"]
[tool.uv.sources]
layercake = { path = "../layercake" }

View File

@@ -1,3 +0,0 @@
{
"extraPaths": ["app/"]
}

View File

@@ -1,9 +0,0 @@
version = 0.1
[default.deploy.parameters]
stack_name = "saladeaula-user-management"
resolve_s3 = true
s3_prefix = "user_management"
region = "sa-east-1"
confirm_changeset = false
capabilities = "CAPABILITY_IAM"
image_repositories = []

View File

@@ -1,113 +0,0 @@
AWSTemplateFormatVersion: 2010-09-09
Transform: AWS::Serverless-2016-10-31
Parameters:
BucketName:
Type: String
Default: saladeaula.digital
UserTable:
Type: String
Default: betaeducacao-prod-users_d2o3r5gmm4it7j
Globals:
Function:
CodeUri: app/
Runtime: python3.13
Tracing: Active
Architectures:
- x86_64
Layers:
- !Sub arn:aws:lambda:sa-east-1:336641857101:layer:layercake:72
Environment:
Variables:
TZ: America/Sao_Paulo
LOG_LEVEL: DEBUG
POWERTOOLS_LOGGER_SAMPLE_RATE: 0.1
POWERTOOLS_LOGGER_LOG_EVENT: true
USER_TABLE: !Ref UserTable
Resources:
EventLog:
Type: AWS::Logs::LogGroup
Properties:
RetentionInDays: 90
EventCsvChunksFunction:
Type: AWS::Serverless::Function
Properties:
Handler: events.batch.csv_chunks.lambda_handler
LoggingConfig:
LogGroup: !Ref EventLog
Policies:
- S3CrudPolicy:
BucketName: !Ref BucketName
Events:
DynamoDBEvent:
Type: EventBridgeRule
Properties:
Pattern:
resources: [betaeducacao-prod-users_d2o3r5gmm4it7j]
detail:
new_image:
sk:
- prefix: batch_jobs#
EventEmailReceivingFunction:
Type: AWS::Serverless::Function
Properties:
Handler: events.email_receiving.lambda_handler
LoggingConfig:
LogGroup: !Ref EventLog
Policies:
- DynamoDBCrudPolicy:
TableName: !Ref UserTable
LambdaInvokePermission:
Type: AWS::Lambda::Permission
Properties:
FunctionName: !GetAtt EventEmailReceivingFunction.Arn
Action: lambda:InvokeFunction
Principal: ses.amazonaws.com
SourceArn: !Sub arn:aws:ses:${AWS::Region}:${AWS::AccountId}:receipt-rule-set/*
BucketPolicy:
Type: AWS::S3::BucketPolicy
Properties:
Bucket: !Ref BucketName
PolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Principal:
Service: ses.amazonaws.com
Action: s3:PutObject
Resource: !Sub arn:aws:s3:::${BucketName}/*
Condition:
StringEquals:
aws:SourceAccount: !Ref AWS::AccountId
StringLike:
aws:SourceArn: !Sub arn:aws:ses:${AWS::Region}:${AWS::AccountId}:receipt-rule-set/*
EmailReceiptRuleSet:
Type: AWS::SES::ReceiptRuleSet
Properties:
RuleSetName: users.noreply.saladeaula.digital
EmailReceiptRule:
Type: AWS::SES::ReceiptRule
DependsOn:
- LambdaInvokePermission
- BucketPolicy
Properties:
RuleSetName: !Ref EmailReceiptRuleSet
Rule:
Name: lambda
Enabled: true
Actions:
- LambdaAction:
FunctionArn: !GetAtt EventEmailReceivingFunction.Arn
InvocationType: RequestResponse
- S3Action:
BucketName: !Ref BucketName
ObjectKeyPrefix: "mailbox"
ScanEnabled: true

View File

@@ -1,69 +0,0 @@
import os
from dataclasses import dataclass
import jsonlines
import pytest
PYTEST_TABLE_NAME = 'pytest'
PK = 'id'
SK = 'sk'
# https://docs.pytest.org/en/7.1.x/reference/reference.html#pytest.hookspec.pytest_configure
def pytest_configure():
os.environ['TZ'] = 'America/Sao_Paulo'
os.environ['DYNAMODB_PARTITION_KEY'] = PK
os.environ['DYNAMODB_SORT_KEY'] = SK
os.environ['USER_TABLE'] = PYTEST_TABLE_NAME
@dataclass
class LambdaContext:
function_name: str = 'test'
memory_limit_in_mb: int = 128
invoked_function_arn: str = 'arn:aws:lambda:eu-west-1:809313241:function:test'
aws_request_id: str = '52fdfc07-2182-154f-163f-5f0f9a621d72'
@pytest.fixture
def lambda_context() -> LambdaContext:
return LambdaContext()
@pytest.fixture
def dynamodb_client():
from boto3clients import dynamodb_client as client
client.create_table(
AttributeDefinitions=[
{'AttributeName': PK, 'AttributeType': 'S'},
{'AttributeName': SK, 'AttributeType': 'S'},
],
TableName=PYTEST_TABLE_NAME,
KeySchema=[
{'AttributeName': PK, 'KeyType': 'HASH'},
{'AttributeName': SK, 'KeyType': 'RANGE'},
],
ProvisionedThroughput={
'ReadCapacityUnits': 123,
'WriteCapacityUnits': 123,
},
)
yield client
client.delete_table(TableName=PYTEST_TABLE_NAME)
@pytest.fixture()
def dynamodb_persistence_layer(dynamodb_client):
from layercake.dynamodb import DynamoDBPersistenceLayer
return DynamoDBPersistenceLayer(PYTEST_TABLE_NAME, dynamodb_client)
@pytest.fixture()
def dynamodb_seeds(dynamodb_client):
with jsonlines.open('tests/seeds.jsonl') as lines:
for line in lines:
dynamodb_client.put_item(TableName=PYTEST_TABLE_NAME, Item=line)

View File

@@ -1,13 +0,0 @@
import events.batch.csv_into_chunks as app
def test_chunk_csv(lambda_context):
event = {
'detail': {
'new_image': {
's3uri': 's3://saladeaula.digital/samples/large_users.csv',
},
},
}
app.lambda_handler(event, lambda_context) # type: ignore

View File

@@ -1,136 +0,0 @@
from aws_lambda_powertools.utilities.typing import LambdaContext
import events.email_receiving as app
event = {
'Records': [
{
'eventSource': 'aws:ses',
'eventVersion': '1.0',
'ses': {
'mail': {
'timestamp': '2025-05-29T15:50:41.604Z',
'source': 'sergio@somosbeta.com.br',
'messageId': '2994higq3tr7efijr3lj65etntffapgg1q7hea81',
'destination': [
'org+15608435000190@users.noreply.saladeaula.digital'
],
'headersTruncated': False,
'headers': [
{'name': 'Return-Path', 'value': '<sergio@somosbeta.com.br>'},
{
'name': 'Received',
'value': 'from mail-lf1-f54.google.com (mail-lf1-f54.google.com [209.85.167.54]) by inbound-smtp.sa-east-1.amazonaws.com with SMTP id 2994higq3tr7efijr3lj65etntffapgg1q7hea81 for org+35980592000130@users.noreply.saladeaula.digital; Thu, 29 May 2025 15:50:41 +0000 (UTC)',
},
{'name': 'X-SES-Spam-Verdict', 'value': 'PASS'},
{'name': 'X-SES-Virus-Verdict', 'value': 'PASS'},
{
'name': 'Received-SPF',
'value': 'pass (spfCheck: domain of somosbeta.com.br designates 209.85.167.54 as permitted sender) client-ip=209.85.167.54; envelope-from=sergio@somosbeta.com.br; helo=mail-lf1-f54.google.com;',
},
{
'name': 'Authentication-Results',
'value': 'amazonses.com; spf=pass (spfCheck: domain of somosbeta.com.br designates 209.85.167.54 as permitted sender) client-ip=209.85.167.54; envelope-from=sergio@somosbeta.com.br; helo=mail-lf1-f54.google.com; dkim=pass header.i=@somosbeta.com.br; dmarc=none header.from=somosbeta.com.br;',
},
{
'name': 'X-SES-RECEIPT',
'value': 'AEFBQUFBQUFBQUFHVWpuODdPY2tGUlordE5YWkVEUlZNWWZFYkpDMU5MUURyaHNVSldnTGhEVWhCQzd5UGpzWHI4LzJoS1VaN0lOU0FkMzJFU0h6MjVuUzk2c09KUXlzbUJQdHd6T0d0Y2ptZXhRVk1KY3RkOXpRamZMb3hwSGJIVlFla2tBcmZvRmYwQS9WU3hBVlBqcUpDYm00eTdiRnRqNW45ek9ld0ZyTGJKV3k2TXRpc0J6aGhBdmFvZDFDQ000Zm9QTng3VHljNXArM0hjT2ZsYkhtM3RCZnpRV1NOczU2RDdmL0RKclJOcDNvY2ZxV1hmajNYMkczVHpsWEZCMm40Z2pQM29udkMyb01vN3JwU0p2TUI1WGorN2JPd2RPYW5lUDN3T3RMRlhsdEpGbGNCa3c9PQ==',
},
{
'name': 'X-SES-DKIM-SIGNATURE',
'value': 'a=rsa-sha256; q=dns/txt; b=KPtFiBwsOTBl1YVLRTSfaZ+X6h7uSSOu/i1Cw6Pd+wvMBHRWy9EYcWUjyDjsLG/uYHShLW4+LHsSg9HiqrAP2jVJSAawrIwZr1wPQo7ovQvWuZfHQN/StgXIgBU+L7Bp6GSR26LRufxjj7q9YBmEeirjJ3d0G8E/rF2QqeITlpo=; c=relaxed/simple; s=bm3ypaoivbtdzmy3b6w37fzb5voa2uru; d=amazonses.com; t=1748533842; v=1; bh=kTUCV1DQAazu4FsUi1MrelD2QvSfHGArZ/c6A79t3/E=; h=From:To:Cc:Bcc:Subject:Date:Message-ID:MIME-Version:Content-Type:X-SES-RECEIPT;',
},
{
'name': 'Received',
'value': 'by mail-lf1-f54.google.com with SMTP id 2adb3069b0e04-54b0d638e86so1570269e87.1 for <org+35980592000130@users.noreply.saladeaula.digital>; Thu, 29 May 2025 08:50:40 -0700 (PDT)',
},
{
'name': 'DKIM-Signature',
'value': 'v=1; a=rsa-sha256; c=relaxed/relaxed; d=somosbeta.com.br; s=google; t=1748533838; x=1749138638; darn=users.noreply.saladeaula.digital; h=to:subject:message-id:date:from:in-reply-to:references:mime-version:from:to:cc:subject:date:message-id:reply-to; bh=kTUCV1DQAazu4FsUi1MrelD2QvSfHGArZ/c6A79t3/E=; b=Qi8gk/kTpwXCLDM7FPS7ULTy+9gO/4WsGL9zY1xEDw0Rp38f4rVR8L95hIhwK2daA27mq3pv9TdrK3XKQQIuSvRVvaM0b/evkZD8QhaT9tCmL0eKEBB4czGB0OSS3Q4qP34GFWMmXIaxoKIo1td76JnXbto9ZQvjUTBr3GGlF3Lm/MPTaAHs1b3dalv2diTvyj1tzoeb4wGePKsqLh5LKGOxbbWsxPeHEJ8sLM4LyJjxoqSOO0wgKdH5S/ZNpHWcJtXBntjiDUZNeQ5ucEn8ZLbADCObZZV/gH9i/cB1BmlSvJP3D07uJTAEBqyepd+W9fIW2mox/+fmOb3OEHRthQ==',
},
{
'name': 'X-Google-DKIM-Signature',
'value': 'v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20230601; t=1748533838; x=1749138638; h=to:subject:message-id:date:from:in-reply-to:references:mime-version :x-gm-message-state:from:to:cc:subject:date:message-id:reply-to; bh=kTUCV1DQAazu4FsUi1MrelD2QvSfHGArZ/c6A79t3/E=; b=UnL/uRXRahnH5uStZ276LH4kqpHigngw4iql9GHKmFaIKxJ8hLGn/wu7ie4ljnw8m/ I4CvhDKH4TVtIWPS81fm06PMgeqQYRX3jLhHvIltROCNVX6ZFzbAXgiAlk0NS1npvDYJ evVgSaPPco4D/8pMWZX4fUjU+8me32ChKxHsklEAts/LiD+MvTuHCHZovSEj1aXAz91b yGZe1bx2+phuqzUZyIOeheKjl4TNjEBx83omOzf9HtClKhjzCwHjfZ8uk2lhJ10ogKZ0 GNQ5OlnPkgdAg0/+HsifvGR6xfkFsiunIDyinBWoOhMU1o0+DiicxOIjY8QEayF3MLUt REoA==',
},
{
'name': 'X-Gm-Message-State',
'value': 'AOJu0Yxw0icQkFV090vn5hx/hKp0ePH78Wr0iqi4V3x4mpVXrRX8te2o 30aBYeZRPwn8SRSrq/kbn4bLcs5mPDB+iRP9IGFxS7KLSQi+KG4PQeDHyW3R/AgOPHACUUXUUyz Vcwny029WGY5PVhxlikAYdDfhNdO8GM2DKMV1+Oxy/a+qmt5LZeuy',
},
{
'name': 'X-Gm-Gg',
'value': 'ASbGncslCMPPU/pax0+RNy/cQR/Y/wUroSJMvI2DCCMq6Qld+Ih1jG4+HnhQPqn3nTK EEW6/99tqazq+SKy+31AB77ajVczvJQTElRSW/+bhd42l7by2hicTKElcR3GWivlrqd1TywUZOB DkB9J/vupSV0PDCJfZVi+7Tb9Pb61nnxaU+SQ=',
},
{
'name': 'X-Google-Smtp-Source',
'value': 'AGHT+IFYi41KmJjGcfQmUvWJDdTAzGIv2JlL9XAwBpAb53mMOOm3tttzkhbvfuiKh/DI9NjITHuO3xuEPqnPI9lpum8=',
},
{
'name': 'X-Received',
'value': 'by 2002:a05:6512:1392:b0:553:2f61:58f1 with SMTP id 2adb3069b0e04-5532f615a8dmr2268707e87.53.1748533837647; Thu, 29 May 2025 08:50:37 -0700 (PDT)',
},
{'name': 'MIME-Version', 'value': '1.0'},
{
'name': 'References',
'value': '<CAMThe4mV9=1-BLiOi9MU3fAS=C6uYE9+3hKUjibrwxxngYNn2Q@mail.gmail.com>',
},
{
'name': 'In-Reply-To',
'value': '<CAMThe4mV9=1-BLiOi9MU3fAS=C6uYE9+3hKUjibrwxxngYNn2Q@mail.gmail.com>',
},
{
'name': 'From',
'value': 'Sérgio Rafael Siqueira <sergio@somosbeta.com.br>',
},
{'name': 'Date', 'value': 'Thu, 29 May 2025 12:50:26 -0300'},
{
'name': 'X-Gm-Features',
'value': 'AX0GCFvofROqzf21KTgiIJq_AULCNljEuNFUJBk2xQGwVKmPjim_4slYIOP0WRw',
},
{
'name': 'Message-ID',
'value': '<CAMThe4=yMRJg4YOcACYAR509N1RyWyQgAghyVmr=NuSJnbondg@mail.gmail.com>',
},
{'name': 'Subject', 'value': 'Re: test'},
{
'name': 'To',
'value': 'org+15608435000190@users.noreply.saladeaula.digital',
},
{
'name': 'Content-Type',
'value': 'multipart/alternative; boundary="00000000000045b8c206364842b3"',
},
],
'commonHeaders': {
'returnPath': 'sergio@somosbeta.com.br',
'from': ['"Sérgio Rafael Siqueira" <sergio@somosbeta.com.br>'],
'date': 'Thu, 29 May 2025 12:50:26 -0300',
'to': ['org+15608435000190@users.noreply.saladeaula.digital'],
'messageId': '<CAMThe4=yMRJg4YOcACYAR509N1RyWyQgAghyVmr=NuSJnbondg@mail.gmail.com>',
'subject': 'Re: test',
},
},
'receipt': {
'timestamp': '2025-05-29T15:50:41.604Z',
'processingTimeMillis': 1105,
'recipients': [
'org+15608435000190@users.noreply.saladeaula.digital'
],
'spamVerdict': {'status': 'PASS'},
'virusVerdict': {'status': 'PASS'},
'spfVerdict': {'status': 'PASS'},
'dkimVerdict': {'status': 'PASS'},
'dmarcVerdict': {'status': 'GRAY'},
'action': {
'type': 'Lambda',
'functionArn': 'arn:aws:lambda:sa-east-1:336641857101:function:saladeaula-user-managemen-EventEmailReceivingFunct-LmnnEfi9tL2O',
'invocationType': 'Event',
},
},
},
}
]
}
def test_email_receiving(dynamodb_seeds, lambda_context: LambdaContext):
assert app.lambda_handler(event, lambda_context) == {'disposition': 'CONTINUE'}

File diff suppressed because it is too large Load Diff

View File

@@ -1,28 +0,0 @@
CADASTRO DE COLABORADOR,,,,
,NOME COMPLETO,EMAIL (letra minúscula),CPF,TREINAMENTO
,ANDRE HENRIQUE LOPES ZAFALON,henrique.zafalon@fanucamerica.com,261.955.138-22,NR-35 (RECICLAGEM)
,SERGIO DA SILVA CUPERTINO,sergio.cupertino@fanucamerica.com,066.945.708-64,NR-10 (RECICLAGEM)
,SERGIO DA SILVA CUPERTINO,sergio.cupertino@fanucamerica.com,066.945.708-64,NR-35 (RECICLAGEM)
,ROVANE CAMPOS,rovane.campos@fanucamerica.com,095.958.578-82,NR-10 (RECICLAGEM)
,ROVANE CAMPOS,rovane.campos@fanucamerica.com,095.958.578-82,NR-35 (RECICLAGEM)
,MARCIO ATSUSHI KANEKO MASUDA,marcio.masuda@fanucamerica.com,293.042.798-10,NR-10 (RECICLAGEM)
,FABIO AKIRA HARAGUCHI,fabio.haraguchi@fanucamerica.com,287.018.428-03,NR-10 (RECICLAGEM)
,EMIDIO YOITI MOCHIZUKI,emidio.mochizuki@fanucamerica.com,268.579.208-26,NR-10 (RECICLAGEM)
,EMIDIO YOITI MOCHIZUKI,emidio.mochizuki@fanucamerica.com,268.579.208-26,NR-35 (RECICLAGEM)
,ERIC HIDEKI MORIKIO,eric.morikio@fanucamerica.com,417.359.838-61,NR-10 (RECICLAGEM)
,HENRIQUE DE FIGUEIREDO BASTOS FERRAZ,henrique.ferraz@fanucamerica.com,417.059.788-51,NR-10 (RECICLAGEM)
,LAYS MORETTI DA SILVA,lays.silva@fanucamerica.com,013.107.662-07,NR-10 (RECICLAGEM)
,LAYS MORETTI DA SILVA,lays.silva@fanucamerica.com,013.107.662-07,NR-12
,ANDRE DE SOUZA,andre.souza@fanucamerica.com,290.688.648-31,NR-10 (RECICLAGEM)
,ANDRE DE SOUZA,andre.souza@fanucamerica.com,290.688.648-31,NR-12
,RAFAEL TOSHIO BURATO MAEDA,rafael.maeda@fanucamerica.com,394.153.268-59,NR-10 (RECICLAGEM)
,RAFAEL TOSHIO BURATO MAEDA,rafael.maeda@fanucamerica.com,394.153.268-59,NR-12
,RAFAEL TOSHIO BURATO MAEDA,rafael.maeda@fanucamerica.com,394.153.268-59,NR-35 (RECICLAGEM)
,RICARDO GALLES BONET,ricardo.bonet@fanucamerica.com,424.430.528-93,NR-10 (RECICLAGEM)
,RULIO SIEFERT SERA,rulio.sera@fanucamerica.com,063.916.859-08,NR-10 (RECICLAGEM)
,MACIEL FERREIRA BOMFIM,maciel.bomfim@fanucamerica.com,334.547.088-85,NR-10 (RECICLAGEM)
,JAIME EDUARDO GALVEZ AVILES,jaime.galvez@fanucamerica.com,280.238.818-50,NR-12
,JAIME EDUARDO GALVEZ AVILES,jaime.galvez@fanucamerica.com,280.238.818-50,NR-35 (RECICLAGEM)
,HIGOR MACHADO SILVA,higor.silva@fanucamerica.com,419.879.878-88,NR-12
,LÁZARO SOUZA DIAS,lazaro.dias@fanucamerica.com,067.179.825-19,NR-12
,JOÃO PEDRO AGUIAR GALASSO,joao.pedro@fanucamerica.com,570.403.588-40,NR-12
1 CADASTRO DE COLABORADOR
2 NOME COMPLETO EMAIL (letra minúscula) CPF TREINAMENTO
3 ANDRE HENRIQUE LOPES ZAFALON henrique.zafalon@fanucamerica.com 261.955.138-22 NR-35 (RECICLAGEM)
4 SERGIO DA SILVA CUPERTINO sergio.cupertino@fanucamerica.com 066.945.708-64 NR-10 (RECICLAGEM)
5 SERGIO DA SILVA CUPERTINO sergio.cupertino@fanucamerica.com 066.945.708-64 NR-35 (RECICLAGEM)
6 ROVANE CAMPOS rovane.campos@fanucamerica.com 095.958.578-82 NR-10 (RECICLAGEM)
7 ROVANE CAMPOS rovane.campos@fanucamerica.com 095.958.578-82 NR-35 (RECICLAGEM)
8 MARCIO ATSUSHI KANEKO MASUDA marcio.masuda@fanucamerica.com 293.042.798-10 NR-10 (RECICLAGEM)
9 FABIO AKIRA HARAGUCHI fabio.haraguchi@fanucamerica.com 287.018.428-03 NR-10 (RECICLAGEM)
10 EMIDIO YOITI MOCHIZUKI emidio.mochizuki@fanucamerica.com 268.579.208-26 NR-10 (RECICLAGEM)
11 EMIDIO YOITI MOCHIZUKI emidio.mochizuki@fanucamerica.com 268.579.208-26 NR-35 (RECICLAGEM)
12 ERIC HIDEKI MORIKIO eric.morikio@fanucamerica.com 417.359.838-61 NR-10 (RECICLAGEM)
13 HENRIQUE DE FIGUEIREDO BASTOS FERRAZ henrique.ferraz@fanucamerica.com 417.059.788-51 NR-10 (RECICLAGEM)
14 LAYS MORETTI DA SILVA lays.silva@fanucamerica.com 013.107.662-07 NR-10 (RECICLAGEM)
15 LAYS MORETTI DA SILVA lays.silva@fanucamerica.com 013.107.662-07 NR-12
16 ANDRE DE SOUZA andre.souza@fanucamerica.com 290.688.648-31 NR-10 (RECICLAGEM)
17 ANDRE DE SOUZA andre.souza@fanucamerica.com 290.688.648-31 NR-12
18 RAFAEL TOSHIO BURATO MAEDA rafael.maeda@fanucamerica.com 394.153.268-59 NR-10 (RECICLAGEM)
19 RAFAEL TOSHIO BURATO MAEDA rafael.maeda@fanucamerica.com 394.153.268-59 NR-12
20 RAFAEL TOSHIO BURATO MAEDA rafael.maeda@fanucamerica.com 394.153.268-59 NR-35 (RECICLAGEM)
21 RICARDO GALLES BONET ricardo.bonet@fanucamerica.com 424.430.528-93 NR-10 (RECICLAGEM)
22 RULIO SIEFERT SERA rulio.sera@fanucamerica.com 063.916.859-08 NR-10 (RECICLAGEM)
23 MACIEL FERREIRA BOMFIM maciel.bomfim@fanucamerica.com 334.547.088-85 NR-10 (RECICLAGEM)
24 JAIME EDUARDO GALVEZ AVILES jaime.galvez@fanucamerica.com 280.238.818-50 NR-12
25 JAIME EDUARDO GALVEZ AVILES jaime.galvez@fanucamerica.com 280.238.818-50 NR-35 (RECICLAGEM)
26 HIGOR MACHADO SILVA higor.silva@fanucamerica.com 419.879.878-88 NR-12
27 LÁZARO SOUZA DIAS lazaro.dias@fanucamerica.com 067.179.825-19 NR-12
28 JOÃO PEDRO AGUIAR GALASSO joao.pedro@fanucamerica.com 570.403.588-40 NR-12

View File

@@ -1,4 +0,0 @@
{"id": {"S": "cJtK9SsnJhKPyxESe7g3DG"}, "sk": {"S": "0"}, "name": {"S": "EDUSEG"}}
{"id": {"S": "cJtK9SsnJhKPyxESe7g3DG"}, "sk": {"S": "admins#5OxmMjL-ujoR5IMGegQz"}, "name": {"S": "Sérgio R Siqueira"}, "email": {"S": "sergio@somosbeta.com.br"}}
{"id": {"S": "cnpj"}, "sk": {"S": "15608435000190"}, "user_id": {"S": "cJtK9SsnJhKPyxESe7g3DG"}}
{"id": {"S": "email"}, "sk": {"S": "org+15608435000190@users.noreply.saladeaula.digital"}, "user_id": {"S": "cJtK9SsnJhKPyxESe7g3DG"}}

View File

@@ -1,29 +0,0 @@
from csv_utils import byte_ranges, detect_delimiter
def test_detect_delimiter():
with open('tests/samples/users.csv') as fp:
assert detect_delimiter(fp) == ','
def test_byte_ranges():
csvpath = 'tests/samples/users.csv'
ranges = byte_ranges(csvpath, 10)
*_, pair = ranges
start_byte, end_byte = pair
assert ranges == [(0, 808), (809, 1655), (1656, 2303)]
expected = """,RICARDO GALLES BONET,ricardo.bonet@fanucamerica.com,424.430.528-93,NR-10 (RECICLAGEM)
,RULIO SIEFERT SERA,rulio.sera@fanucamerica.com,063.916.859-08,NR-10 (RECICLAGEM)
,MACIEL FERREIRA BOMFIM,maciel.bomfim@fanucamerica.com,334.547.088-85,NR-10 (RECICLAGEM)
,JAIME EDUARDO GALVEZ AVILES,jaime.galvez@fanucamerica.com,280.238.818-50,NR-12
,JAIME EDUARDO GALVEZ AVILES,jaime.galvez@fanucamerica.com,280.238.818-50,NR-35 (RECICLAGEM)
,HIGOR MACHADO SILVA,higor.silva@fanucamerica.com,419.879.878-88,NR-12
,LÁZARO SOUZA DIAS,lazaro.dias@fanucamerica.com,067.179.825-19,NR-12
,JOÃO PEDRO AGUIAR GALASSO,joao.pedro@fanucamerica.com,570.403.588-40,NR-12"""
with open(csvpath, 'rb') as f:
f.seek(start_byte)
data = f.read(end_byte - start_byte + 1)
assert data.decode('utf-8') == expected

1171
user-management/uv.lock generated

File diff suppressed because it is too large Load Diff