add
This commit is contained in:
5
enrollments-events/Makefile
Normal file
5
enrollments-events/Makefile
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
build:
|
||||||
|
sam build --use-container
|
||||||
|
|
||||||
|
deploy: build
|
||||||
|
sam deploy --debug
|
||||||
13
enrollments-events/app/boto3clients.py
Normal file
13
enrollments-events/app/boto3clients.py
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
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()
|
||||||
BIN
enrollments-events/app/certs/fonts/SF-Pro.ttf
Executable file
BIN
enrollments-events/app/certs/fonts/SF-Pro.ttf
Executable file
Binary file not shown.
53
enrollments-events/app/certs/hello.py
Normal file
53
enrollments-events/app/certs/hello.py
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
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')
|
||||||
28
enrollments-events/app/certs/pyproject.toml
Normal file
28
enrollments-events/app/certs/pyproject.toml
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
[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",
|
||||||
|
]
|
||||||
251
enrollments-events/app/certs/sample.html
Normal file
251
enrollments-events/app/certs/sample.html
Normal file
@@ -0,0 +1,251 @@
|
|||||||
|
<!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>
|
||||||
1006
enrollments-events/app/certs/uv.lock
generated
Normal file
1006
enrollments-events/app/certs/uv.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
16
enrollments-events/app/config.py
Normal file
16
enrollments-events/app/config.py
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
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
|
||||||
BIN
enrollments-events/app/courses_export_2025-06-18_110214.db
Normal file
BIN
enrollments-events/app/courses_export_2025-06-18_110214.db
Normal file
Binary file not shown.
0
enrollments-events/app/events/__init__.py
Normal file
0
enrollments-events/app/events/__init__.py
Normal file
25
enrollments-events/app/events/issue_cert.py
Normal file
25
enrollments-events/app/events/issue_cert.py
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
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 boto3clients import dynamodb_client
|
||||||
|
from config import (
|
||||||
|
COURSE_TABLE,
|
||||||
|
ENROLLMENT_TABLE,
|
||||||
|
)
|
||||||
|
|
||||||
|
logger = Logger(__name__)
|
||||||
|
enrollment_layer = DynamoDBPersistenceLayer(ENROLLMENT_TABLE, dynamodb_client)
|
||||||
|
course_layer = DynamoDBPersistenceLayer(COURSE_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']
|
||||||
|
|
||||||
|
return True
|
||||||
4
enrollments-events/app/events/stopgap/__init__.py
Normal file
4
enrollments-events/app/events/stopgap/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
"""
|
||||||
|
Stopgap events. Everything here is a quick fix and should be replaced with
|
||||||
|
proper solutions.
|
||||||
|
"""
|
||||||
59
enrollments-events/app/events/stopgap/enroll.py
Normal file
59
enrollments-events/app/events/stopgap/enroll.py
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
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
|
||||||
BIN
enrollments-events/app/fonts/SF-Pro.ttf
Executable file
BIN
enrollments-events/app/fonts/SF-Pro.ttf
Executable file
Binary file not shown.
33
enrollments-events/pyproject.toml
Normal file
33
enrollments-events/pyproject.toml
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
[project]
|
||||||
|
name = "enrollments-events"
|
||||||
|
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" }
|
||||||
3
enrollments-events/pyrightconfig.json
Normal file
3
enrollments-events/pyrightconfig.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"extraPaths": ["app/"]
|
||||||
|
}
|
||||||
9
enrollments-events/samconfig.toml
Normal file
9
enrollments-events/samconfig.toml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
version = 0.1
|
||||||
|
[default.deploy.parameters]
|
||||||
|
stack_name = "saladeaula-enrollments-events"
|
||||||
|
resolve_s3 = true
|
||||||
|
s3_prefix = "enrollments-events"
|
||||||
|
region = "sa-east-1"
|
||||||
|
confirm_changeset = false
|
||||||
|
capabilities = "CAPABILITY_IAM"
|
||||||
|
image_repositories = []
|
||||||
82
enrollments-events/template.yaml
Normal file
82
enrollments-events/template.yaml
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
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:79
|
||||||
|
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: ["0"]
|
||||||
|
|
||||||
|
EventIssueCertFunction:
|
||||||
|
Type: AWS::Serverless::Function
|
||||||
|
Properties:
|
||||||
|
Handler: events.issue_cert.lambda_handler
|
||||||
|
LoggingConfig:
|
||||||
|
LogGroup: !Ref EventLog
|
||||||
|
Events:
|
||||||
|
DynamoDBEvent:
|
||||||
|
Type: EventBridgeRule
|
||||||
|
Properties:
|
||||||
|
Pattern:
|
||||||
|
resources: [!Ref EnrollmentTable]
|
||||||
|
detail:
|
||||||
|
new_image:
|
||||||
|
sk: ["0"]
|
||||||
|
status: [COMPLETED]
|
||||||
|
old_image:
|
||||||
|
status: [PENDING]
|
||||||
0
enrollments-events/tests/__init__.py
Normal file
0
enrollments-events/tests/__init__.py
Normal file
74
enrollments-events/tests/conftest.py
Normal file
74
enrollments-events/tests/conftest.py
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
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)
|
||||||
0
enrollments-events/tests/events/__init__.py
Normal file
0
enrollments-events/tests/events/__init__.py
Normal file
0
enrollments-events/tests/events/stopgap/__init__.py
Normal file
0
enrollments-events/tests/events/stopgap/__init__.py
Normal file
39
enrollments-events/tests/events/stopgap/test_enroll.py
Normal file
39
enrollments-events/tests/events/stopgap/test_enroll.py
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
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': '0',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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)
|
||||||
251
enrollments-events/tests/sample.html
Normal file
251
enrollments-events/tests/sample.html
Normal file
@@ -0,0 +1,251 @@
|
|||||||
|
<!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, Helvetica, Arial, sans-serif;
|
||||||
|
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>
|
||||||
3
enrollments-events/tests/seeds.jsonl
Normal file
3
enrollments-events/tests/seeds.jsonl
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{"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"}}
|
||||||
1113
enrollments-events/uv.lock
generated
Normal file
1113
enrollments-events/uv.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
0
konviva-events/app/events/__init__.py
Normal file
0
konviva-events/app/events/__init__.py
Normal file
0
konviva-events/app/events/enroll.py
Normal file
0
konviva-events/app/events/enroll.py
Normal file
5
order-events/Makefile
Normal file
5
order-events/Makefile
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
build:
|
||||||
|
sam build --use-container
|
||||||
|
|
||||||
|
deploy: build
|
||||||
|
sam deploy --debug
|
||||||
13
order-events/app/boto3clients.py
Normal file
13
order-events/app/boto3clients.py
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
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()
|
||||||
5
order-events/app/config.py
Normal file
5
order-events/app/config.py
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
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
|
||||||
0
order-events/app/events/__init__.py
Normal file
0
order-events/app/events/__init__.py
Normal file
78
order-events/app/events/assign_tenant_cnpj.py
Normal file
78
order-events/app/events/assign_tenant_cnpj.py
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
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
|
||||||
57
order-events/app/events/remove_slots_on_canceled.py
Normal file
57
order-events/app/events/remove_slots_on_canceled.py
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
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
|
||||||
4
order-events/app/events/stopgap/__init__.py
Normal file
4
order-events/app/events/stopgap/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
"""
|
||||||
|
Stopgap events. Everything here is a quick fix and should be replaced with
|
||||||
|
proper solutions.
|
||||||
|
"""
|
||||||
71
order-events/app/events/stopgap/remove_slots.py
Normal file
71
order-events/app/events/stopgap/remove_slots.py
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
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
|
||||||
46
order-events/app/events/stopgap/set_as_paid.py
Normal file
46
order-events/app/events/stopgap/set_as_paid.py
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
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
|
||||||
33
order-events/pyproject.toml
Normal file
33
order-events/pyproject.toml
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
[project]
|
||||||
|
name = "orders-events"
|
||||||
|
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" }
|
||||||
3
order-events/pyrightconfig.json
Normal file
3
order-events/pyrightconfig.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"extraPaths": ["app/"]
|
||||||
|
}
|
||||||
9
order-events/samconfig.toml
Normal file
9
order-events/samconfig.toml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
version = 0.1
|
||||||
|
[default.deploy.parameters]
|
||||||
|
stack_name = "saladeaula-orders-events"
|
||||||
|
resolve_s3 = true
|
||||||
|
s3_prefix = "orders-events"
|
||||||
|
region = "sa-east-1"
|
||||||
|
confirm_changeset = false
|
||||||
|
capabilities = "CAPABILITY_IAM"
|
||||||
|
image_repositories = []
|
||||||
139
order-events/template.yaml
Normal file
139
order-events/template.yaml
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
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:79
|
||||||
|
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]
|
||||||
0
order-events/tests/__init__.py
Normal file
0
order-events/tests/__init__.py
Normal file
72
order-events/tests/conftest.py
Normal file
72
order-events/tests/conftest.py
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
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)
|
||||||
0
order-events/tests/events/__init__.py
Normal file
0
order-events/tests/events/__init__.py
Normal file
0
order-events/tests/events/stopgap/__init__.py
Normal file
0
order-events/tests/events/stopgap/__init__.py
Normal file
30
order-events/tests/events/stopgap/test_remove_slots.py
Normal file
30
order-events/tests/events/stopgap/test_remove_slots.py
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
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
|
||||||
24
order-events/tests/events/stopgap/test_set_as_paid.py
Normal file
24
order-events/tests/events/stopgap/test_set_as_paid.py
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
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'
|
||||||
28
order-events/tests/events/test_assign_tenant.py
Normal file
28
order-events/tests/events/test_assign_tenant.py
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
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'])
|
||||||
29
order-events/tests/events/test_assign_tenant_cnpj.py
Normal file
29
order-events/tests/events/test_assign_tenant_cnpj.py
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
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'])
|
||||||
27
order-events/tests/events/test_remove_slots_on_canceled.py
Normal file
27
order-events/tests/events/test_remove_slots_on_canceled.py
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
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
|
||||||
10
order-events/tests/seeds.jsonl
Normal file
10
order-events/tests/seeds.jsonl
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{"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-events/uv.lock
generated
Normal file
1113
order-events/uv.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
5
users-events/Makefile
Normal file
5
users-events/Makefile
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
build:
|
||||||
|
sam build --use-container
|
||||||
|
|
||||||
|
deploy: build
|
||||||
|
sam deploy --debug
|
||||||
14
users-events/app/boto3clients.py
Normal file
14
users-events/app/boto3clients.py
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
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')
|
||||||
4
users-events/app/config.py
Normal file
4
users-events/app/config.py
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
import os
|
||||||
|
|
||||||
|
USER_TABLE: str = os.getenv('USER_TABLE') # type: ignore
|
||||||
|
CHUNK_SIZE = 50
|
||||||
83
users-events/app/csv_utils.py
Normal file
83
users-events/app/csv_utils.py
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
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
|
||||||
0
users-events/app/events/__init__.py
Normal file
0
users-events/app/events/__init__.py
Normal file
0
users-events/app/events/batch/__init__.py
Normal file
0
users-events/app/events/batch/__init__.py
Normal file
20
users-events/app/events/batch/csv_into_chunks.py
Normal file
20
users-events/app/events/batch/csv_into_chunks.py
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
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
|
||||||
14
users-events/app/events/batch/excel_to_csv.py
Normal file
14
users-events/app/events/batch/excel_to_csv.py
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
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
|
||||||
55
users-events/app/events/batch/read_csv_chunk.py
Normal file
55
users-events/app/events/batch/read_csv_chunk.py
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
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'))
|
||||||
40
users-events/app/events/email_receiving.py
Normal file
40
users-events/app/events/email_receiving.py
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
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'}
|
||||||
20
users-events/app/ses_utils.py
Normal file
20
users-events/app/ses_utils.py
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
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
|
||||||
62
users-events/cf.py
Normal file
62
users-events/cf.py
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
# /// 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)
|
||||||
33
users-events/pyproject.toml
Normal file
33
users-events/pyproject.toml
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
[project]
|
||||||
|
name = "users-events"
|
||||||
|
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" }
|
||||||
3
users-events/pyrightconfig.json
Normal file
3
users-events/pyrightconfig.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"extraPaths": ["app/"]
|
||||||
|
}
|
||||||
9
users-events/samconfig.toml
Normal file
9
users-events/samconfig.toml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
version = 0.1
|
||||||
|
[default.deploy.parameters]
|
||||||
|
stack_name = "saladeaula-users-events"
|
||||||
|
resolve_s3 = true
|
||||||
|
s3_prefix = "users-events"
|
||||||
|
region = "sa-east-1"
|
||||||
|
confirm_changeset = false
|
||||||
|
capabilities = "CAPABILITY_IAM"
|
||||||
|
image_repositories = []
|
||||||
114
users-events/template.yaml
Normal file
114
users-events/template.yaml
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
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: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
|
||||||
|
|
||||||
|
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
|
||||||
0
users-events/tests/__init__.py
Normal file
0
users-events/tests/__init__.py
Normal file
69
users-events/tests/conftest.py
Normal file
69
users-events/tests/conftest.py
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
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)
|
||||||
0
users-events/tests/events/__init__.py
Normal file
0
users-events/tests/events/__init__.py
Normal file
13
users-events/tests/events/batch/test_csv_into_chunks.py
Normal file
13
users-events/tests/events/batch/test_csv_into_chunks.py
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
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
|
||||||
136
users-events/tests/events/test_email_receiving.py
Normal file
136
users-events/tests/events/test_email_receiving.py
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
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'}
|
||||||
3286
users-events/tests/samples/large_users.csv
Normal file
3286
users-events/tests/samples/large_users.csv
Normal file
File diff suppressed because it is too large
Load Diff
28
users-events/tests/samples/users.csv
Normal file
28
users-events/tests/samples/users.csv
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
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
|
||||||
|
4
users-events/tests/seeds.jsonl
Normal file
4
users-events/tests/seeds.jsonl
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{"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"}}
|
||||||
29
users-events/tests/test_csv_utils.py
Normal file
29
users-events/tests/test_csv_utils.py
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
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
users-events/uv.lock
generated
Normal file
1171
users-events/uv.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user