rename
This commit is contained in:
BIN
certs/cert.pdf
BIN
certs/cert.pdf
Binary file not shown.
Binary file not shown.
@@ -1,53 +0,0 @@
|
||||
import base64
|
||||
import io
|
||||
import locale
|
||||
from datetime import date
|
||||
from uuid import uuid4
|
||||
|
||||
import qrcode
|
||||
from jinja2 import Template
|
||||
from PIL import Image
|
||||
from weasyprint import HTML
|
||||
|
||||
locale.setlocale(locale.LC_TIME, 'pt_BR')
|
||||
today = date.today()
|
||||
|
||||
with open('nr10_complementar_sep.html', encoding='utf-8') as f:
|
||||
html = f.read()
|
||||
|
||||
|
||||
def cpf_fmt(s: str) -> str:
|
||||
"""Returns a string as a Brazilian CPF number."""
|
||||
return '{}.{}.{}-{}'.format(s[:3], s[3:6], s[6:9], s[9:])
|
||||
|
||||
|
||||
qr = qrcode.QRCode(
|
||||
version=1,
|
||||
error_correction=qrcode.constants.ERROR_CORRECT_H,
|
||||
box_size=10,
|
||||
border=3,
|
||||
)
|
||||
qr.add_data('https://eduseg.com.br')
|
||||
qr.make(fit=True)
|
||||
img = qr.make_image(fill_color='black', back_color='white')
|
||||
img = img.resize((120, 120), Image.NEAREST)
|
||||
buffer = io.BytesIO()
|
||||
img.save(buffer, format='PNG')
|
||||
img_str = base64.b64encode(buffer.getvalue()).decode('utf-8')
|
||||
qrcode_base64 = f'data:image/png;base64,{img_str}'
|
||||
|
||||
|
||||
template = Template(html)
|
||||
html_rendered = template.render(
|
||||
id=uuid4(),
|
||||
name='Sérgio Rafael de Siqueira',
|
||||
cpf=cpf_fmt('07879819908'),
|
||||
progress=91.99,
|
||||
course='NR-10 Complementar (SEP)',
|
||||
today=today.strftime('%-d de %B de %Y'),
|
||||
started_date=today.strftime('%d/%m/%Y'),
|
||||
finished_date=today.strftime('%d/%m/%Y'),
|
||||
qrcode=qrcode_base64,
|
||||
)
|
||||
|
||||
HTML(string=html_rendered, base_url='').write_pdf('cert.pdf')
|
||||
@@ -1,251 +0,0 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>NR-10 Complementar (SEP)</title>
|
||||
<meta name="author" content="EDUSEG® <https://eduseg.com.br>" />
|
||||
<style>
|
||||
html,
|
||||
body,
|
||||
div,
|
||||
h1,
|
||||
h2,
|
||||
ul,
|
||||
p,
|
||||
a {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
font-size: 100%;
|
||||
font: inherit;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "SF-Pro";
|
||||
src: url("fonts/SF-Pro.ttf") format("truetype");
|
||||
}
|
||||
|
||||
@page {
|
||||
size: A4 landscape;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
html {
|
||||
font-family: SF-Pro;
|
||||
font-size: 13pt;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
section {
|
||||
width: 29.7cm;
|
||||
height: 21cm;
|
||||
break-after: page;
|
||||
box-sizing: border-box;
|
||||
padding: 5rem;
|
||||
}
|
||||
|
||||
strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#cover {
|
||||
background-color: #a7e400;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
#cover h1 {
|
||||
font-weight: bolder;
|
||||
font-size: 26pt;
|
||||
}
|
||||
|
||||
#cover .qrcode {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
background-color: #fff;
|
||||
position: absolute;
|
||||
top: 5rem;
|
||||
right: 5rem;
|
||||
}
|
||||
|
||||
#cover .signatures {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-top: 2.5rem;
|
||||
}
|
||||
|
||||
.sign1 {
|
||||
width: 250px;
|
||||
border-top: #000 solid 1px;
|
||||
}
|
||||
|
||||
#back {
|
||||
background-color: white;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
#back h1,
|
||||
#back h2 {
|
||||
font-weight: bold;
|
||||
font-size: 16pt;
|
||||
}
|
||||
|
||||
#back ul {
|
||||
padding-left: 1rem;
|
||||
}
|
||||
|
||||
.space-y-0\.5 > :not(:last-child) {
|
||||
margin-bottom: 0.125rem;
|
||||
}
|
||||
|
||||
.space-y-2\.5 > :not(:last-child) {
|
||||
margin-bottom: 0.625rem;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<section id="cover">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 1072.73 329.6"
|
||||
style="width: 14rem"
|
||||
>
|
||||
<g>
|
||||
<g>
|
||||
<path
|
||||
fill="#8cd366"
|
||||
d="M152.18,217.62l-68.61-30.91c-1.88-1.2-4.28-1.2-6.16,0l-68.61,30.91c-3.81,2.43-8.8-.3-8.8-4.82V8.98C0,5.82,2.56,3.26,5.72,3.26h149.54c3.16,0,5.72,2.56,5.72,5.72v203.81c0,4.52-5,7.26-8.8,4.82Z"
|
||||
></path>
|
||||
<path
|
||||
fill="#2e3524"
|
||||
d="M93.97,74.01H26.61v20.16h67.36v-20.16Z"
|
||||
></path>
|
||||
<path
|
||||
fill="#2e3524"
|
||||
d="M107.44,111.16H26.61v26.8h80.83v-26.8Z"
|
||||
></path>
|
||||
<path
|
||||
fill="#2e3524"
|
||||
d="M107.44,30.27H26.61v26.8h80.83v-26.8Z"
|
||||
></path>
|
||||
<path
|
||||
fill="#2e3524"
|
||||
d="M134.38,131.23c0-3.72-3.02-6.73-6.73-6.73s-6.73,3.02-6.73,6.73,3.02,6.73,6.73,6.73,6.73-3.02,6.73-6.73Z"
|
||||
></path>
|
||||
</g>
|
||||
<g>
|
||||
<path
|
||||
fill="#f9f7e8"
|
||||
d="M244.7,3.24h92.33v44.43h-44.15v88.85h39.38v39.62h-39.38v105.77h44.15v44.42h-92.33V3.24Z"
|
||||
></path>
|
||||
<path
|
||||
fill="#f9f7e8"
|
||||
d="M362.72,3.24h57.79c10.71,0,20.47,2.35,29.29,7.06,8.83,4.7,15.79,11.18,20.87,19.39,5.08,8.21,7.63,17.45,7.63,27.67v214.88c0,10.22-2.48,19.46-7.42,27.67-4.96,8.21-11.83,14.69-20.68,19.39-8.83,4.7-18.73,7.08-29.7,7.08h-57.79V3.24ZM427.55,283.88c1.74-1.87,2.6-4.18,2.6-6.86V52.56c-.26-2.69-1.34-4.97-3.22-6.86-1.88-1.87-4.15-2.83-6.82-2.83h-14v243.85h14.41c2.93,0,5.27-.94,7.01-2.83h.02Z"
|
||||
></path>
|
||||
<path
|
||||
fill="#f9f7e8"
|
||||
d="M531.5,322.49c-8.71-4.7-15.6-11.16-20.68-19.39-5.08-8.21-7.63-17.42-7.63-27.67V3.24h48.15v279.41c0,2.69.93,4.99,2.82,6.86,1.86,1.9,4.15,2.83,6.82,2.83,2.93,0,5.27-.94,7.01-2.83,1.74-1.87,2.6-4.18,2.6-6.86V3.24h48.16v272.21c0,10.25-2.48,19.46-7.42,27.67-4.96,8.21-11.83,14.69-20.68,19.39-8.85,4.7-18.73,7.08-29.7,7.08s-20.8-2.35-29.5-7.08l.05-.02Z"
|
||||
></path>
|
||||
<path
|
||||
fill="#f9f7e8"
|
||||
d="M672.79,322.49c-8.7-4.7-15.6-11.16-20.68-19.39-5.08-8.21-7.63-17.42-7.63-27.67v-78.75h48.16v85.95c0,2.69.93,4.99,2.82,6.87,1.86,1.9,4.15,2.83,6.82,2.83,2.93,0,5.27-.94,7.01-2.83,1.74-1.87,2.6-4.18,2.6-6.87v-77.88c0-5.66-2.22-10.3-6.63-13.94-4.41-3.62-11.57-8.02-21.47-13.13-8.3-4.3-15.05-8.14-20.27-11.52-5.22-3.36-9.71-7.87-13.45-13.54-3.75-5.66-5.63-12.24-5.63-19.8V54.12c0-10.22,2.53-19.44,7.63-27.67,5.08-8.21,11.97-14.66,20.68-19.39,8.68-4.7,18.53-7.06,29.5-7.06s20.87,2.35,29.69,7.06c8.83,4.7,15.72,11.18,20.68,19.39,4.96,8.21,7.42,17.45,7.42,27.67v71.09h-48.16V46.92c0-2.69-.88-4.97-2.6-6.86-1.74-1.87-4.08-2.83-7.01-2.83-2.67,0-4.96.94-6.82,2.83-1.89,1.9-2.82,4.18-2.82,6.86v69.79c0,6.19,2.34,11.26,7.04,15.14,4.67,3.91,12.24,8.83,22.68,14.74,8.04,4.32,14.57,8.09,19.68,11.3,5.08,3.24,9.37,7.46,12.83,12.72,3.48,5.26,5.22,11.26,5.22,17.98v86.83c0,10.25-2.48,19.46-7.42,27.67-4.96,8.21-11.83,14.69-20.68,19.39-8.85,4.71-18.72,7.08-29.7,7.08s-20.8-2.35-29.5-7.08Z"
|
||||
></path>
|
||||
<path
|
||||
fill="#f9f7e8"
|
||||
d="M784.56,3.24h92.33v44.43h-44.15v88.85h39.38v39.62h-39.38v105.77h44.15v44.42h-92.33V3.24Z"
|
||||
></path>
|
||||
<path
|
||||
fill="#f9f7e8"
|
||||
d="M920.63,322.49c-5.63-4.18-10.11-10.1-13.45-17.76-3.34-7.68-5.01-16.49-5.01-26.45V53.71c0-9.96,2.53-19.06,7.63-27.26,5.08-8.21,12.05-14.66,20.87-19.39,8.83-4.7,18.6-7.06,29.32-7.06s20.54,2.35,29.5,7.06c8.97,4.7,15.91,11.18,20.87,19.39,4.96,8.21,7.42,17.3,7.42,27.26v94.51h-48.16V46.92c0-2.69-.88-4.97-2.6-6.86-1.74-1.87-4.08-2.83-7.01-2.83-2.67,0-4.96.94-6.82,2.83-1.89,1.9-2.82,4.18-2.82,6.86v231.36c0,2.69.93,4.99,2.82,6.87,1.86,1.9,4.15,2.83,6.82,2.83,2.93,0,5.27-.94,7.01-2.83,1.74-1.87,2.6-4.18,2.6-6.87v-46.03h-11.64v-51.29h59.8v145.4h-48.16v-14.14c-2.96,5.4-6.82,9.48-11.64,12.31-4.82,2.83-10.83,4.25-18.06,4.25s-13.64-2.09-19.27-6.26l-.02-.02Z"
|
||||
></path>
|
||||
<path
|
||||
fill="#f9f7e8"
|
||||
d="M1053.27,25.05h-6.13l-.06-3.69h5.48c.83-.02,1.61-.15,2.33-.4.72-.27,1.3-.64,1.73-1.14.44-.51.65-1.14.65-1.87,0-.93-.16-1.67-.48-2.22-.3-.55-.83-.94-1.59-1.16-.74-.25-1.74-.37-3.01-.37h-3.78v20.42h-4.12V10.54h7.9c1.87,0,3.49.27,4.86.82,1.38.53,2.44,1.34,3.18,2.44.76,1.08,1.14,2.43,1.14,4.06,0,1.02-.24,1.93-.71,2.73-.47.8-1.17,1.49-2.1,2.07-.91.57-2.03,1.03-3.35,1.39-.06,0-.12.07-.2.2-.06.13-.11.2-.17.2-.32.19-.53.33-.63.43-.08.08-.16.12-.25.14-.08.02-.31.03-.68.03ZM1052.99,25.05l.6-2.81c2.95,0,4.97.64,6.05,1.93,1.08,1.27,1.62,2.89,1.62,4.86v1.53c0,.7.03,1.37.08,2.02.08.62.21,1.15.4,1.59v.45h-4.23c-.19-.49-.3-1.19-.34-2.1-.02-.91-.03-1.57-.03-1.99v-1.48c0-1.38-.31-2.39-.94-3.04s-1.69-.97-3.21-.97ZM1035.75,22.92c0,2.52.43,4.87,1.28,7.04.87,2.16,2.08,4.05,3.64,5.68,1.55,1.61,3.34,2.87,5.37,3.78,2.05.89,4.22,1.33,6.53,1.33s4.51-.44,6.53-1.33c2.03-.91,3.8-2.17,5.34-3.78,1.53-1.63,2.74-3.52,3.61-5.68.87-2.18,1.31-4.52,1.31-7.04s-.44-4.86-1.31-7.01c-.87-2.16-2.07-4.04-3.61-5.65-1.53-1.61-3.31-2.86-5.34-3.75-2.03-.91-4.2-1.36-6.53-1.36s-4.49.45-6.53,1.36c-2.03.89-3.81,2.14-5.37,3.75-1.55,1.61-2.77,3.49-3.64,5.65-.85,2.16-1.28,4.5-1.28,7.01ZM1032.4,22.92c0-3.01.52-5.8,1.56-8.38,1.04-2.57,2.49-4.82,4.34-6.73,1.86-1.93,4-3.43,6.42-4.49,2.44-1.08,5.06-1.62,7.84-1.62s5.39.54,7.81,1.62c2.44,1.06,4.58,2.56,6.42,4.49,1.86,1.91,3.31,4.16,4.35,6.73,1.06,2.57,1.59,5.37,1.59,8.38s-.53,5.8-1.59,8.38c-1.04,2.57-2.49,4.84-4.35,6.79-1.83,1.93-3.97,3.44-6.42,4.52-2.42,1.08-5.03,1.62-7.81,1.62s-5.39-.54-7.84-1.62c-2.42-1.08-4.56-2.58-6.42-4.52-1.85-1.95-3.3-4.21-4.34-6.79-1.04-2.57-1.56-5.37-1.56-8.38Z"
|
||||
></path>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
<p>Certificamos que</p>
|
||||
<h1>{{ name }}</h1>
|
||||
<p>
|
||||
Portador(a) do CPF <strong>{{ cpf }} </strong>, concluiu o curso
|
||||
de <strong>NR-10 Complementar (SEP)</strong> com aproveitamento
|
||||
de
|
||||
<strong>{{ progress }}%</strong>
|
||||
</p>
|
||||
<p>Realizado entre {{ started_date }} e {{ finished_date }}</p>
|
||||
<p>Florianópolis, SC, {{ today }}</p>
|
||||
|
||||
<div class="signatures">
|
||||
<div class="sign1"></div>
|
||||
<div class="sign2">
|
||||
<p>Tiago Maciel do Santos</p>
|
||||
<p>CEO/Diretor</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="qrcode">
|
||||
<img src="{{ qrcode }}" />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="back">
|
||||
<div class="space-y-2.5">
|
||||
<h1>Conteúdo programático ministrado</h1>
|
||||
<ul>
|
||||
<li>Organização do sistema elétrico de potência</li>
|
||||
<li>Organização do trabalho</li>
|
||||
<li>Aspectos comportamentais</li>
|
||||
<li>Condições impeditivas para serviços</li>
|
||||
<li>Riscos típicos no SEP e sua prevenção</li>
|
||||
<li>Técnicas de análise de riscos no SEP</li>
|
||||
<li>Procedimentos de trabalho (análise e discussão)</li>
|
||||
<li>Técnicas de análise de riscos no SEP</li>
|
||||
<li>Equipamentos e ferramentas de trabalho</li>
|
||||
<li>Sistemas de proteção coletiva</li>
|
||||
<li>Equipamentos de proteção individual</li>
|
||||
<li>Posturas e vestuários de trabalhos</li>
|
||||
<li>
|
||||
Segurança com veículos e transporte de pessoas,
|
||||
materiais e equipamentos
|
||||
</li>
|
||||
<li>Sinalização e isolamento de áreas de trabalho</li>
|
||||
<li>
|
||||
Liberação de instalação para serviço, operação e uso
|
||||
</li>
|
||||
<li>
|
||||
Treinamento em técnicas de remoção, atendimento e
|
||||
transporte de acidentados
|
||||
</li>
|
||||
<li>Acidentes típicos</li>
|
||||
<li>Responsabilidades</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2.5">
|
||||
<dd class="space-y-0.5">
|
||||
<h2>Carga horária</h2>
|
||||
<p>40 horas</p>
|
||||
</dd>
|
||||
|
||||
<dd class="space-y-0.5">
|
||||
<h2>Instrutor e responsável técnico</h2>
|
||||
<div>
|
||||
<p>Francis Ricardo Baretta</p>
|
||||
<p>CPF 039.539.409-02</p>
|
||||
<p>Eng. de Segurança no Trabalho Eng. Eletricista</p>
|
||||
<p>CREA/SC 126693-0</p>
|
||||
</div>
|
||||
</dd>
|
||||
</div>
|
||||
</section>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,28 +0,0 @@
|
||||
[project]
|
||||
name = "certs"
|
||||
version = "0.1.0"
|
||||
description = "Add your description here"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.13"
|
||||
dependencies = [
|
||||
"jinja2>=3.1.6",
|
||||
"layercake",
|
||||
"qrcode>=8.2",
|
||||
]
|
||||
|
||||
[tool.uv.sources]
|
||||
layercake = { path = "../layercake" }
|
||||
|
||||
[tool.ruff]
|
||||
target-version = "py311"
|
||||
src = ["app"]
|
||||
|
||||
[tool.ruff.format]
|
||||
quote-style = "single"
|
||||
|
||||
[tool.ruff.lint]
|
||||
select = ["E", "F", "I"]
|
||||
[dependency-groups]
|
||||
dev = [
|
||||
"ruff>=0.11.9",
|
||||
]
|
||||
1006
certs/uv.lock
generated
1006
certs/uv.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -1,5 +0,0 @@
|
||||
build:
|
||||
sam build --use-container
|
||||
|
||||
deploy: build
|
||||
sam deploy --debug
|
||||
@@ -1,13 +0,0 @@
|
||||
import os
|
||||
|
||||
import boto3
|
||||
|
||||
|
||||
def get_dynamodb_client():
|
||||
if os.getenv('AWS_LAMBDA_FUNCTION_NAME'):
|
||||
return boto3.client('dynamodb')
|
||||
|
||||
return boto3.client('dynamodb', endpoint_url='http://127.0.0.1:8000')
|
||||
|
||||
|
||||
dynamodb_client = get_dynamodb_client()
|
||||
@@ -1,16 +0,0 @@
|
||||
import os
|
||||
|
||||
USER_TABLE: str = os.getenv('USER_TABLE') # type: ignore
|
||||
ORDER_TABLE: str = os.getenv('ORDER_TABLE') # type: ignore
|
||||
ENROLLMENT_TABLE: str = os.getenv('ENROLLMENT_TABLE') # type: ignore
|
||||
COURSE_TABLE: str = os.getenv('COURSE_TABLE') # type: ignore
|
||||
|
||||
|
||||
# Post-migration: remove the lines below
|
||||
if os.getenv('AWS_LAMBDA_FUNCTION_NAME'):
|
||||
SQLITE_DATABASE = 'courses_export_2025-06-18_110214.db'
|
||||
else:
|
||||
SQLITE_DATABASE = 'app/courses_export_2025-06-18_110214.db'
|
||||
|
||||
SQLITE_TABLE = 'courses'
|
||||
OLD_ENROLLMENT_TABLE: str = os.getenv('OLD_ENROLLMENT_TABLE') # type: ignore
|
||||
@@ -1,4 +0,0 @@
|
||||
"""
|
||||
Stopgap events. Everything here is a quick fix and should be replaced with
|
||||
proper solutions.
|
||||
"""
|
||||
@@ -1,59 +0,0 @@
|
||||
import json
|
||||
import sqlite3
|
||||
|
||||
from aws_lambda_powertools import Logger
|
||||
from aws_lambda_powertools.utilities.data_classes import (
|
||||
EventBridgeEvent,
|
||||
event_source,
|
||||
)
|
||||
from aws_lambda_powertools.utilities.typing import LambdaContext
|
||||
from layercake.dynamodb import DynamoDBPersistenceLayer
|
||||
from sqlite_utils import Database
|
||||
|
||||
from boto3clients import dynamodb_client
|
||||
from config import (
|
||||
COURSE_TABLE,
|
||||
ENROLLMENT_TABLE,
|
||||
SQLITE_DATABASE,
|
||||
SQLITE_TABLE,
|
||||
)
|
||||
|
||||
sqlite3.register_converter('json', json.loads)
|
||||
|
||||
logger = Logger(__name__)
|
||||
enrollment_layer = DynamoDBPersistenceLayer(ENROLLMENT_TABLE, dynamodb_client)
|
||||
course_layer = DynamoDBPersistenceLayer(COURSE_TABLE, dynamodb_client)
|
||||
deduplication_window = {'offset_days': 90}
|
||||
|
||||
|
||||
class DeduplicationConflictError(Exception):
|
||||
def __init__(self, *args):
|
||||
super().__init__('Enrollment already exists')
|
||||
|
||||
|
||||
@event_source(data_class=EventBridgeEvent)
|
||||
@logger.inject_lambda_context
|
||||
def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool:
|
||||
new_image = event.detail['new_image']
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class CourseNotFoundError(Exception):
|
||||
def __init__(self, *args):
|
||||
super().__init__('Course not found')
|
||||
|
||||
|
||||
def _get_course(course_id: str) -> dict:
|
||||
with sqlite3.connect(
|
||||
database=SQLITE_DATABASE, detect_types=sqlite3.PARSE_DECLTYPES
|
||||
) as conn:
|
||||
db = Database(conn)
|
||||
rows = db[SQLITE_TABLE].rows_where(
|
||||
"json->>'$.metadata__betaeducacao_id' = ?", [course_id]
|
||||
)
|
||||
|
||||
for row in rows:
|
||||
return row['json']
|
||||
|
||||
raise CourseNotFoundError
|
||||
@@ -1,33 +0,0 @@
|
||||
[project]
|
||||
name = "enrollment-management"
|
||||
version = "0.1.0"
|
||||
description = ""
|
||||
readme = ""
|
||||
requires-python = ">=3.13"
|
||||
dependencies = ["layercake"]
|
||||
|
||||
[dependency-groups]
|
||||
dev = [
|
||||
"jsonlines>=4.0.0",
|
||||
"pytest>=8.3.4",
|
||||
"pytest-cov>=6.0.0",
|
||||
"ruff>=0.9.1",
|
||||
]
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
pythonpath = ["app/"]
|
||||
addopts = "--cov --cov-report html -v"
|
||||
|
||||
[tool.ruff]
|
||||
target-version = "py311"
|
||||
src = ["app"]
|
||||
|
||||
[tool.ruff.format]
|
||||
quote-style = "single"
|
||||
|
||||
[tool.ruff.lint]
|
||||
select = ["E", "F", "I"]
|
||||
|
||||
|
||||
[tool.uv.sources]
|
||||
layercake = { path = "../layercake" }
|
||||
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"extraPaths": ["app/"]
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
version = 0.1
|
||||
[default.deploy.parameters]
|
||||
stack_name = "saladeaula-enrollment-management"
|
||||
resolve_s3 = true
|
||||
s3_prefix = "enrollment_management"
|
||||
region = "sa-east-1"
|
||||
confirm_changeset = false
|
||||
capabilities = "CAPABILITY_IAM"
|
||||
image_repositories = []
|
||||
@@ -1,63 +0,0 @@
|
||||
AWSTemplateFormatVersion: 2010-09-09
|
||||
Transform: AWS::Serverless-2016-10-31
|
||||
|
||||
Parameters:
|
||||
UserTable:
|
||||
Type: String
|
||||
Default: betaeducacao-prod-users_d2o3r5gmm4it7j
|
||||
EnrollmentTable:
|
||||
Type: String
|
||||
Default: betaeducacao-prod-enrollments
|
||||
CourseTable:
|
||||
Type: String
|
||||
Default: saladeaula_courses
|
||||
OrderTable:
|
||||
Type: String
|
||||
Default: betaeducacao-prod-orders
|
||||
|
||||
Globals:
|
||||
Function:
|
||||
CodeUri: app/
|
||||
Runtime: python3.13
|
||||
Tracing: Active
|
||||
Architectures:
|
||||
- x86_64
|
||||
Layers:
|
||||
- !Sub arn:aws:lambda:sa-east-1:336641857101:layer:layercake:78
|
||||
Environment:
|
||||
Variables:
|
||||
TZ: America/Sao_Paulo
|
||||
LOG_LEVEL: DEBUG
|
||||
DYNAMODB_PARTITION_KEY: id
|
||||
POWERTOOLS_LOGGER_SAMPLE_RATE: 0.1
|
||||
POWERTOOLS_LOGGER_LOG_EVENT: true
|
||||
USER_TABLE: !Ref UserTable
|
||||
ENROLLMENT_TABLE: !Ref EnrollmentTable
|
||||
ORDER_TABLE: !Ref OrderTable
|
||||
COURSE_TABLE: !Ref CourseTable
|
||||
|
||||
Resources:
|
||||
EventLog:
|
||||
Type: AWS::Logs::LogGroup
|
||||
Properties:
|
||||
RetentionInDays: 90
|
||||
|
||||
EventEnrollFunction:
|
||||
Type: AWS::Serverless::Function
|
||||
Properties:
|
||||
Handler: events.stopgap.enroll.lambda_handler
|
||||
LoggingConfig:
|
||||
LogGroup: !Ref EventLog
|
||||
Policies:
|
||||
- DynamoDBCrudPolicy:
|
||||
TableName: !Ref EnrollmentTable
|
||||
Events:
|
||||
DynamoDBEvent:
|
||||
Type: EventBridgeRule
|
||||
Properties:
|
||||
Pattern:
|
||||
resources: [!Ref EnrollmentTable]
|
||||
detail-type: [INSERT]
|
||||
detail:
|
||||
new_image:
|
||||
sk: ["konviva"]
|
||||
@@ -1,74 +0,0 @@
|
||||
import os
|
||||
from dataclasses import dataclass
|
||||
|
||||
import jsonlines
|
||||
import pytest
|
||||
|
||||
PYTEST_TABLE_NAME = 'pytest'
|
||||
PK = 'id'
|
||||
SK = 'sk'
|
||||
|
||||
|
||||
# https://docs.pytest.org/en/7.1.x/reference/reference.html#pytest.hookspec.pytest_configure
|
||||
def pytest_configure():
|
||||
os.environ['TZ'] = 'America/Sao_Paulo'
|
||||
os.environ['DYNAMODB_PARTITION_KEY'] = PK
|
||||
os.environ['DYNAMODB_SORT_KEY'] = SK
|
||||
os.environ['USER_TABLE'] = PYTEST_TABLE_NAME
|
||||
os.environ['COURSE_TABLE'] = PYTEST_TABLE_NAME
|
||||
os.environ['ORDER_TABLE'] = PYTEST_TABLE_NAME
|
||||
os.environ['ENROLLMENT_TABLE'] = PYTEST_TABLE_NAME
|
||||
# Post-migration: remove it
|
||||
os.environ['OLD_ENROLLMENT_TABLE'] = PYTEST_TABLE_NAME
|
||||
|
||||
|
||||
@dataclass
|
||||
class LambdaContext:
|
||||
function_name: str = 'test'
|
||||
memory_limit_in_mb: int = 128
|
||||
invoked_function_arn: str = 'arn:aws:lambda:eu-west-1:809313241:function:test'
|
||||
aws_request_id: str = '52fdfc07-2182-154f-163f-5f0f9a621d72'
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def lambda_context() -> LambdaContext:
|
||||
return LambdaContext()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def dynamodb_client():
|
||||
from boto3clients import dynamodb_client as client
|
||||
|
||||
client.create_table(
|
||||
AttributeDefinitions=[
|
||||
{'AttributeName': PK, 'AttributeType': 'S'},
|
||||
{'AttributeName': SK, 'AttributeType': 'S'},
|
||||
],
|
||||
TableName=PYTEST_TABLE_NAME,
|
||||
KeySchema=[
|
||||
{'AttributeName': PK, 'KeyType': 'HASH'},
|
||||
{'AttributeName': SK, 'KeyType': 'RANGE'},
|
||||
],
|
||||
ProvisionedThroughput={
|
||||
'ReadCapacityUnits': 123,
|
||||
'WriteCapacityUnits': 123,
|
||||
},
|
||||
)
|
||||
|
||||
yield client
|
||||
|
||||
client.delete_table(TableName=PYTEST_TABLE_NAME)
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def dynamodb_persistence_layer(dynamodb_client):
|
||||
from layercake.dynamodb import DynamoDBPersistenceLayer
|
||||
|
||||
return DynamoDBPersistenceLayer(PYTEST_TABLE_NAME, dynamodb_client)
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def dynamodb_seeds(dynamodb_client):
|
||||
with jsonlines.open('tests/seeds.jsonl') as lines:
|
||||
for line in lines:
|
||||
dynamodb_client.put_item(TableName=PYTEST_TABLE_NAME, Item=line)
|
||||
@@ -1,39 +0,0 @@
|
||||
import pprint
|
||||
|
||||
import app.events.stopgap.enroll as app
|
||||
from aws_lambda_powertools.utilities.typing import LambdaContext
|
||||
from layercake.dynamodb import (
|
||||
DynamoDBPersistenceLayer,
|
||||
SortKey,
|
||||
TransactKey,
|
||||
)
|
||||
|
||||
|
||||
def test_enroll(
|
||||
dynamodb_seeds,
|
||||
dynamodb_client,
|
||||
dynamodb_persistence_layer: DynamoDBPersistenceLayer,
|
||||
lambda_context: LambdaContext,
|
||||
):
|
||||
event = {
|
||||
'detail': {
|
||||
'new_image': {
|
||||
'id': '47ZxxcVBjvhDS5TE98tpfQ',
|
||||
'sk': 'konviva',
|
||||
}
|
||||
}
|
||||
}
|
||||
assert app.lambda_handler(event, lambda_context) # type: ignore
|
||||
|
||||
result = dynamodb_persistence_layer.collection.get_items(
|
||||
TransactKey('47ZxxcVBjvhDS5TE98tpfQ')
|
||||
+ SortKey('0')
|
||||
+ SortKey('metadata#tenant')
|
||||
+ SortKey('metadata#author')
|
||||
+ SortKey('metadata#konviva')
|
||||
+ SortKey('metadata#lock')
|
||||
+ SortKey('metadata#deduplication_window')
|
||||
+ SortKey('metadata#cert')
|
||||
)
|
||||
|
||||
pprint.pprint(result)
|
||||
@@ -1,3 +0,0 @@
|
||||
{"id": {"S": "47ZxxcVBjvhDS5TE98tpfQ"}, "sk": {"S": "0"}, "course": {"M": {"id": {"S": "42"}, "name": {"S": "NR-35 Segurança nos Trabalhos em Altura (Teórico)"},"time_in_days": {"N": "720"}}},"create_date": {"S": "2025-04-10T11:58:33.303347-03:00"},"konviva:id": {"N": "238662"},"progress": {"N": "16.67"},"score": {"NULL": true},"status": {"S": "IN_PROGRESS"}, "update_date": {"S": "2025-04-10T15:44:03.023054-03:00"}, "user": {"M": {"id": {"S": "5OxmMjL-ujoR5IMGegQz"}, "cpf": {"S": "07879819908"}, "email": {"S": "sergio@somosbeta.com.br"}, "name": {"S": "Sérgio Rafael Siqueira"}}}}
|
||||
{"id": {"S": "47ZxxcVBjvhDS5TE98tpfQ"}, "sk": {"S": "konviva"}, "create_date": {"S": "2025-04-10T11:58:35.035729-03:00"}, "konviva_id": {"N": "238662"}}
|
||||
{"id": {"S": "47ZxxcVBjvhDS5TE98tpfQ"}, "sk": {"S": "tenant"}, "create_date": {"S": "2025-04-10T11:58:33.303347-03:00"}, "name": {"S": "Beta Educação"},"org_id": {"S": "cJtK9SsnJhKPyxESe7g3DG"}}
|
||||
1113
enrollment-management/uv.lock
generated
1113
enrollment-management/uv.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -42,22 +42,27 @@ class LifecycleEvents(str, Enum):
|
||||
"""Lifecycle events related to scheduling actions."""
|
||||
|
||||
# Reminder if the user does not access within 3 days
|
||||
REMINDER_NO_ACCESS_3_DAYS = 'schedules#reminder_no_access_3_days'
|
||||
# REMINDER_NO_ACCESS_3_DAYS = 'schedules#reminder_no_access_3_days'
|
||||
DOES_NOT_ACCESS = 'schedules#does_not_access'
|
||||
|
||||
# When there is no activity 7 days after the first access
|
||||
NO_ACTIVITY_7_DAYS = 'schedules#no_activity_7_days'
|
||||
# NO_ACTIVITY_7_DAYS = 'schedules#no_activity_7_days'
|
||||
NO_ACTIVITY = 'schedules#no_activity'
|
||||
|
||||
# Reminder 30 days before the access period expires
|
||||
ACCESS_PERIOD_REMINDER_30_DAYS = 'schedules#access_period_reminder_30_days'
|
||||
# ACCESS_PERIOD_REMINDER_30_DAYS = 'schedules#access_period_reminder_30_days'
|
||||
ACCESS_PERIOD_ENDS = 'schedules#access_period_ends'
|
||||
|
||||
# Reminder for certificate expiration set to 30 days from now
|
||||
CERT_EXPIRATION_REMINDER_30_DAYS = 'schedules#cert_expiration_reminder_30_days'
|
||||
|
||||
# Archive the course after the certificate expires
|
||||
COURSE_ARCHIVED = 'schedules#course_archived'
|
||||
# COURSE_ARCHIVED = 'schedules#course_archived'
|
||||
ARCHIVE_IT = 'schedules#archive_it'
|
||||
|
||||
# When the access period ends for a course without a certificate
|
||||
COURSE_EXPIRED = 'schedules#course_expired'
|
||||
# COURSE_EXPIRED = 'schedules#course_expired'
|
||||
EXPIRATION = 'schedules#expiration'
|
||||
|
||||
|
||||
def enroll(
|
||||
@@ -106,7 +111,8 @@ def enroll(
|
||||
transact.put(
|
||||
item={
|
||||
'id': enrollment.id,
|
||||
'sk': LifecycleEvents.REMINDER_NO_ACCESS_3_DAYS,
|
||||
# 'sk': LifecycleEvents.REMINDER_NO_ACCESS_3_DAYS,
|
||||
'sk': LifecycleEvents.DOES_NOT_ACCESS,
|
||||
'name': user.name,
|
||||
'email': user.email,
|
||||
'course': course.name,
|
||||
@@ -120,7 +126,8 @@ def enroll(
|
||||
transact.put(
|
||||
item={
|
||||
'id': enrollment.id,
|
||||
'sk': LifecycleEvents.COURSE_EXPIRED,
|
||||
'sk': LifecycleEvents.EXPIRATION,
|
||||
# 'sk': LifecycleEvents.COURSE_EXPIRED,
|
||||
'name': user.name,
|
||||
'email': user.email,
|
||||
'course': course.name,
|
||||
@@ -131,7 +138,8 @@ def enroll(
|
||||
transact.put(
|
||||
item={
|
||||
'id': enrollment.id,
|
||||
'sk': LifecycleEvents.ACCESS_PERIOD_REMINDER_30_DAYS,
|
||||
# 'sk': LifecycleEvents.ACCESS_PERIOD_REMINDER_30_DAYS,
|
||||
'sk': LifecycleEvents.ACCESS_PERIOD_ENDS,
|
||||
'name': user.name,
|
||||
'email': user.email,
|
||||
'course': course.name,
|
||||
@@ -161,14 +169,14 @@ def enroll(
|
||||
}
|
||||
)
|
||||
|
||||
class VacancyDoesNotExistError(Exception):
|
||||
class SlotDoesNotExistError(Exception):
|
||||
def __init__(self, *args):
|
||||
super().__init__('Vacancy does not exist')
|
||||
super().__init__('Slot does not exist')
|
||||
|
||||
transact.delete(
|
||||
key=KeyPair(vacancy.id, vacancy.sk),
|
||||
cond_expr='attribute_exists(sk)',
|
||||
exc_cls=VacancyDoesNotExistError,
|
||||
exc_cls=SlotDoesNotExistError,
|
||||
)
|
||||
transact.put(
|
||||
item={
|
||||
@@ -277,10 +285,10 @@ def set_status_as_canceled(
|
||||
cond_expr='attribute_exists(sk)',
|
||||
)
|
||||
# Remove schedules lifecycle events, referencies and locks
|
||||
transact.delete(key=KeyPair(id, 'schedules#archive_it'))
|
||||
transact.delete(key=KeyPair(id, 'schedules#no_activity'))
|
||||
transact.delete(key=KeyPair(id, 'schedules#access_period_ends'))
|
||||
transact.delete(key=KeyPair(id, 'schedules#does_not_access'))
|
||||
transact.delete(key=KeyPair(id, LifecycleEvents.ARCHIVE_IT))
|
||||
transact.delete(key=KeyPair(id, LifecycleEvents.NO_ACTIVITY))
|
||||
transact.delete(key=KeyPair(id, LifecycleEvents.ACCESS_PERIOD_ENDS))
|
||||
transact.delete(key=KeyPair(id, LifecycleEvents.DOES_NOT_ACCESS))
|
||||
transact.delete(key=KeyPair(id, 'parent_vacancy'))
|
||||
transact.delete(key=KeyPair(id, 'lock'))
|
||||
transact.delete(key=KeyPair('lock', lock_hash))
|
||||
@@ -305,8 +313,9 @@ def set_status_as_canceled(
|
||||
},
|
||||
cond_expr='attribute_not_exists(sk)',
|
||||
)
|
||||
# Post-migration: rename `generated_items` to `slots`.
|
||||
# Set the status of `generated_items` to `ROLLBACK` to know
|
||||
# which vacancy is available for reuse
|
||||
# which slot is available for reuse
|
||||
transact.update(
|
||||
key=KeyPair(order_id, f'generated_items#{enrollment_id}'),
|
||||
update_expr='SET #status = :status, update_date = :update',
|
||||
|
||||
@@ -26,7 +26,7 @@ Globals:
|
||||
Architectures:
|
||||
- x86_64
|
||||
Layers:
|
||||
- !Sub arn:aws:lambda:sa-east-1:336641857101:layer:layercake:75
|
||||
- !Sub arn:aws:lambda:sa-east-1:336641857101:layer:layercake:79
|
||||
Environment:
|
||||
Variables:
|
||||
TZ: America/Sao_Paulo
|
||||
@@ -37,7 +37,6 @@ Globals:
|
||||
USER_TABLE: !Ref UserTable
|
||||
ORDER_TABLE: !Ref OrderTable
|
||||
ENROLLMENT_TABLE: !Ref EnrollmentTable
|
||||
NEW_ENROLLMENT_TABLE: !Ref NewEnrollmentTable
|
||||
COURSE_TABLE: !Ref CourseTable
|
||||
ELASTIC_CLOUD_ID: "{{resolve:ssm:/betaeducacao/elastic/cloud_id/str}}"
|
||||
ELASTIC_AUTH_PASS: "{{resolve:ssm:/betaeducacao/elastic/auth_pass/str}}"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "layercake"
|
||||
version = "0.6.11"
|
||||
version = "0.6.12"
|
||||
description = "Packages shared dependencies to optimize deployment and ensure consistency across functions."
|
||||
readme = "README.md"
|
||||
authors = [
|
||||
@@ -24,6 +24,8 @@ dependencies = [
|
||||
"weasyprint>=65.0",
|
||||
"smart-open[s3]>=7.1.0",
|
||||
"sqlite-utils>=3.38",
|
||||
"jinja2>=3.1.6",
|
||||
"qrcode>=8.2",
|
||||
]
|
||||
|
||||
[dependency-groups]
|
||||
|
||||
@@ -16,7 +16,7 @@ Resources:
|
||||
CompatibleRuntimes:
|
||||
- python3.12
|
||||
- python3.13
|
||||
RetentionPolicy: Delete
|
||||
RetentionPolicy: Retain
|
||||
Metadata:
|
||||
BuildMethod: python3.13
|
||||
BuildArchitecture: x86_64
|
||||
|
||||
129
layercake/uv.lock
generated
129
layercake/uv.lock
generated
@@ -299,6 +299,30 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/0e/f6/65ecc6878a89bb1c23a086ea335ad4bf21a588990c3f535a227b9eea9108/charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85", size = 49767, upload-time = "2024-12-24T18:12:32.852Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "click"
|
||||
version = "8.2.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/60/6c/8ca2efa64cf75a977a0d7fac081354553ebe483345c734fb6b6515d96bbc/click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", size = 286342, upload-time = "2025-05-20T23:19:49.832Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b", size = 102215, upload-time = "2025-05-20T23:19:47.796Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "click-default-group"
|
||||
version = "1.2.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "click" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/1d/ce/edb087fb53de63dad3b36408ca30368f438738098e668b78c87f93cd41df/click_default_group-1.2.4.tar.gz", hash = "sha256:eb3f3c99ec0d456ca6cd2a7f08f7d4e91771bef51b01bdd9580cc6450fe1251e", size = 3505, upload-time = "2023-08-04T07:54:58.425Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/1a/aff8bb287a4b1400f69e09a53bd65de96aa5cee5691925b38731c67fc695/click_default_group-1.2.4-py2.py3-none-any.whl", hash = "sha256:9b60486923720e7fc61731bdb32b617039aba820e22e1c88766b1125592eaa5f", size = 4123, upload-time = "2023-08-04T07:54:56.875Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "colorama"
|
||||
version = "0.4.6"
|
||||
@@ -554,6 +578,18 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jinja2"
|
||||
version = "3.1.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "markupsafe" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jmespath"
|
||||
version = "1.0.1"
|
||||
@@ -589,7 +625,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "layercake"
|
||||
version = "0.6.5"
|
||||
version = "0.6.11"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "arnparse" },
|
||||
@@ -598,14 +634,17 @@ dependencies = [
|
||||
{ name = "elasticsearch-dsl" },
|
||||
{ name = "ftfy" },
|
||||
{ name = "glom" },
|
||||
{ name = "jinja2" },
|
||||
{ name = "meilisearch" },
|
||||
{ name = "orjson" },
|
||||
{ name = "pycpfcnpj" },
|
||||
{ name = "pydantic", extra = ["email"] },
|
||||
{ name = "pydantic-extra-types" },
|
||||
{ name = "pytz" },
|
||||
{ name = "qrcode" },
|
||||
{ name = "requests" },
|
||||
{ name = "smart-open", extra = ["s3"] },
|
||||
{ name = "sqlite-utils" },
|
||||
{ name = "weasyprint" },
|
||||
]
|
||||
|
||||
@@ -627,14 +666,17 @@ requires-dist = [
|
||||
{ name = "elasticsearch-dsl", specifier = ">=8.17.1" },
|
||||
{ name = "ftfy", specifier = ">=6.3.1" },
|
||||
{ name = "glom", specifier = ">=24.11.0" },
|
||||
{ name = "jinja2", specifier = ">=3.1.6" },
|
||||
{ name = "meilisearch", specifier = ">=0.34.0" },
|
||||
{ name = "orjson", specifier = ">=3.10.15" },
|
||||
{ name = "pycpfcnpj", specifier = ">=1.8" },
|
||||
{ name = "pydantic", extras = ["email"], specifier = ">=2.10.6" },
|
||||
{ name = "pydantic-extra-types", specifier = ">=2.10.3" },
|
||||
{ name = "pytz", specifier = ">=2025.1" },
|
||||
{ name = "qrcode", specifier = ">=8.2" },
|
||||
{ name = "requests", specifier = ">=2.32.3" },
|
||||
{ name = "smart-open", extras = ["s3"], specifier = ">=7.1.0" },
|
||||
{ name = "sqlite-utils", specifier = ">=3.38" },
|
||||
{ name = "weasyprint", specifier = ">=65.0" },
|
||||
]
|
||||
|
||||
@@ -648,6 +690,44 @@ dev = [
|
||||
{ name = "ruff", specifier = ">=0.11.1" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "markupsafe"
|
||||
version = "3.0.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537, upload-time = "2024-10-18T15:21:54.129Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/22/09/d1f21434c97fc42f09d290cbb6350d44eb12f09cc62c9476effdb33a18aa/MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", size = 14274, upload-time = "2024-10-18T15:21:13.777Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6b/b0/18f76bba336fa5aecf79d45dcd6c806c280ec44538b3c13671d49099fdd0/MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", size = 12348, upload-time = "2024-10-18T15:21:14.822Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/25/dd5c0f6ac1311e9b40f4af06c78efde0f3b5cbf02502f8ef9501294c425b/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", size = 24149, upload-time = "2024-10-18T15:21:15.642Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f3/f0/89e7aadfb3749d0f52234a0c8c7867877876e0a20b60e2188e9850794c17/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", size = 23118, upload-time = "2024-10-18T15:21:17.133Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/da/f2eeb64c723f5e3777bc081da884b414671982008c47dcc1873d81f625b6/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", size = 22993, upload-time = "2024-10-18T15:21:18.064Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/0e/1f32af846df486dce7c227fe0f2398dc7e2e51d4a370508281f3c1c5cddc/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", size = 24178, upload-time = "2024-10-18T15:21:18.859Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c4/f6/bb3ca0532de8086cbff5f06d137064c8410d10779c4c127e0e47d17c0b71/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", size = 23319, upload-time = "2024-10-18T15:21:19.671Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a2/82/8be4c96ffee03c5b4a034e60a31294daf481e12c7c43ab8e34a1453ee48b/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", size = 23352, upload-time = "2024-10-18T15:21:20.971Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/51/ae/97827349d3fcffee7e184bdf7f41cd6b88d9919c80f0263ba7acd1bbcb18/MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", size = 15097, upload-time = "2024-10-18T15:21:22.646Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c1/80/a61f99dc3a936413c3ee4e1eecac96c0da5ed07ad56fd975f1a9da5bc630/MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", size = 15601, upload-time = "2024-10-18T15:21:23.499Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274, upload-time = "2024-10-18T15:21:24.577Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352, upload-time = "2024-10-18T15:21:25.382Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122, upload-time = "2024-10-18T15:21:26.199Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085, upload-time = "2024-10-18T15:21:27.029Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978, upload-time = "2024-10-18T15:21:27.846Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208, upload-time = "2024-10-18T15:21:28.744Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357, upload-time = "2024-10-18T15:21:29.545Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344, upload-time = "2024-10-18T15:21:30.366Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101, upload-time = "2024-10-18T15:21:31.207Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603, upload-time = "2024-10-18T15:21:32.032Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510, upload-time = "2024-10-18T15:21:33.625Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486, upload-time = "2024-10-18T15:21:34.611Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480, upload-time = "2024-10-18T15:21:35.398Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914, upload-time = "2024-10-18T15:21:36.231Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796, upload-time = "2024-10-18T15:21:37.073Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473, upload-time = "2024-10-18T15:21:37.932Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114, upload-time = "2024-10-18T15:21:39.799Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098, upload-time = "2024-10-18T15:21:40.813Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208, upload-time = "2024-10-18T15:21:41.814Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739, upload-time = "2024-10-18T15:21:42.784Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "meilisearch"
|
||||
version = "0.34.0"
|
||||
@@ -1001,6 +1081,18 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/eb/38/ac33370d784287baa1c3d538978b5e2ea064d4c1b93ffbd12826c190dd10/pytz-2025.1-py2.py3-none-any.whl", hash = "sha256:89dd22dca55b46eac6eda23b2d72721bf1bdfef212645d81513ef5d03038de57", size = 507930, upload-time = "2025-01-31T01:54:45.634Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "qrcode"
|
||||
version = "8.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/8f/b2/7fc2931bfae0af02d5f53b174e9cf701adbb35f39d69c2af63d4a39f81a9/qrcode-8.2.tar.gz", hash = "sha256:35c3f2a4172b33136ab9f6b3ef1c00260dd2f66f858f24d88418a015f446506c", size = 43317, upload-time = "2025-05-01T15:44:24.726Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/dd/b8/d2d6d731733f51684bbf76bf34dab3b70a9148e8f2cef2bb544fccec681a/qrcode-8.2-py3-none-any.whl", hash = "sha256:16e64e0716c14960108e85d853062c9e8bba5ca8252c0b4d0231b9df4060ff4f", size = 45986, upload-time = "2025-05-01T15:44:22.781Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "requests"
|
||||
version = "2.32.3"
|
||||
@@ -1079,6 +1171,41 @@ s3 = [
|
||||
{ name = "boto3" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sqlite-fts4"
|
||||
version = "1.0.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c2/6d/9dad6c3b433ab8912ace969c66abd595f8e0a2ccccdb73602b1291dbda29/sqlite-fts4-1.0.3.tar.gz", hash = "sha256:78b05eeaf6680e9dbed8986bde011e9c086a06cb0c931b3cf7da94c214e8930c", size = 9718, upload-time = "2022-07-30T01:14:26.943Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/51/29/0096e8b1811aaa78cfb296996f621f41120c21c2f5cd448ae1d54979d9fc/sqlite_fts4-1.0.3-py3-none-any.whl", hash = "sha256:0359edd8dea6fd73c848989e1e2b1f31a50fe5f9d7272299ff0e8dbaa62d035f", size = 9972, upload-time = "2022-07-30T01:14:24.942Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sqlite-utils"
|
||||
version = "3.38"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "click" },
|
||||
{ name = "click-default-group" },
|
||||
{ name = "pluggy" },
|
||||
{ name = "python-dateutil" },
|
||||
{ name = "sqlite-fts4" },
|
||||
{ name = "tabulate" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/51/43/ce9183a21911e0b73248c8fb83f8b8038515cb80053912c2a009e9765564/sqlite_utils-3.38.tar.gz", hash = "sha256:1ae77b931384052205a15478d429464f6c67a3ac3b4eafd3c674ac900f623aab", size = 214449, upload-time = "2024-11-23T22:49:40.308Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/4d/eb/f8e8e827805f810838efff3311cccd2601238c5fa3fc35c1f878709e161b/sqlite_utils-3.38-py3-none-any.whl", hash = "sha256:8a27441015c3b2ef475f555861f7a2592f73bc60d247af9803a11b65fc605bf9", size = 68183, upload-time = "2024-11-23T22:49:38.289Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tabulate"
|
||||
version = "0.9.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ec/fe/802052aecb21e3797b8f7902564ab6ea0d60ff8ca23952079064155d1ae1/tabulate-0.9.0.tar.gz", hash = "sha256:0095b12bf5966de529c0feb1fa08671671b3368eec77d7ef7ab114be2c068b3c", size = 81090, upload-time = "2022-10-06T17:21:48.54Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/40/44/4a5f08c96eb108af5cb50b41f76142f0afa346dfa99d5296fe7202a11854/tabulate-0.9.0-py3-none-any.whl", hash = "sha256:024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f", size = 35252, upload-time = "2022-10-06T17:21:44.262Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tinycss2"
|
||||
version = "1.4.0"
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
build:
|
||||
sam build --use-container
|
||||
|
||||
deploy: build
|
||||
sam deploy --debug
|
||||
@@ -1,13 +0,0 @@
|
||||
import os
|
||||
|
||||
import boto3
|
||||
|
||||
|
||||
def get_dynamodb_client():
|
||||
if os.getenv('AWS_LAMBDA_FUNCTION_NAME'):
|
||||
return boto3.client('dynamodb')
|
||||
|
||||
return boto3.client('dynamodb', endpoint_url='http://localhost:8000')
|
||||
|
||||
|
||||
dynamodb_client = get_dynamodb_client()
|
||||
@@ -1,5 +0,0 @@
|
||||
import os
|
||||
|
||||
USER_TABLE: str = os.getenv('USER_TABLE') # type: ignore
|
||||
ORDER_TABLE: str = os.getenv('ORDER_TABLE') # type: ignore
|
||||
ENROLLMENT_TABLE: str = os.getenv('ENROLLMENT_TABLE') # type: ignore
|
||||
@@ -1,78 +0,0 @@
|
||||
from aws_lambda_powertools import Logger
|
||||
from aws_lambda_powertools.utilities.data_classes import (
|
||||
EventBridgeEvent,
|
||||
event_source,
|
||||
)
|
||||
from aws_lambda_powertools.utilities.typing import LambdaContext
|
||||
from layercake.dateutils import now
|
||||
from layercake.dynamodb import (
|
||||
DynamoDBPersistenceLayer,
|
||||
KeyPair,
|
||||
SortKey,
|
||||
)
|
||||
|
||||
from boto3clients import dynamodb_client
|
||||
from config import ORDER_TABLE, USER_TABLE
|
||||
|
||||
logger = Logger(__name__)
|
||||
user_layer = DynamoDBPersistenceLayer(USER_TABLE, dynamodb_client)
|
||||
order_layer = DynamoDBPersistenceLayer(ORDER_TABLE, dynamodb_client)
|
||||
|
||||
|
||||
@event_source(data_class=EventBridgeEvent)
|
||||
@logger.inject_lambda_context
|
||||
def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool:
|
||||
new_image = event.detail['new_image']
|
||||
now_ = now()
|
||||
ids = user_layer.collection.get_items(
|
||||
KeyPair(
|
||||
pk='cnpj',
|
||||
sk=SortKey(new_image['cnpj'], path_spec='user_id'),
|
||||
rename_key='org_id',
|
||||
)
|
||||
+ KeyPair(
|
||||
pk='email',
|
||||
sk=SortKey(new_image['email'], path_spec='user_id'),
|
||||
rename_key='user_id',
|
||||
),
|
||||
flatten_top=False,
|
||||
)
|
||||
|
||||
# Sometimes the function executes before the user insertion completes,
|
||||
# so an exception is raised to trigger a retry.
|
||||
if len(ids) < 2:
|
||||
raise ValueError('IDs not found.')
|
||||
|
||||
with order_layer.transact_writer() as transact:
|
||||
transact.update(
|
||||
key=KeyPair(new_image['id'], '0'),
|
||||
update_expr='SET metadata__tenant_id = :tenant_id, \
|
||||
metadata__related_ids = :related_ids, \
|
||||
update_date = :update_date',
|
||||
expr_attr_values={
|
||||
':tenant_id': ids['org_id'],
|
||||
':related_ids': set(ids.values()),
|
||||
':update_date': now_,
|
||||
},
|
||||
)
|
||||
transact.put(
|
||||
item={
|
||||
'id': new_image['id'],
|
||||
'sk': 'metadata#tenant',
|
||||
'tenant_id': f'ORG#{ids["org_id"]}',
|
||||
'create_date': now_,
|
||||
}
|
||||
)
|
||||
|
||||
for k, v in ids.items():
|
||||
kind = k.removesuffix('_id')
|
||||
transact.put(
|
||||
item={
|
||||
'id': new_image['id'],
|
||||
'sk': f'related_ids#{kind}', # e.g. related_ids#user
|
||||
'create_date': now_,
|
||||
k: v,
|
||||
}
|
||||
)
|
||||
|
||||
return True
|
||||
@@ -1,57 +0,0 @@
|
||||
from aws_lambda_powertools import Logger
|
||||
from aws_lambda_powertools.utilities.data_classes import (
|
||||
EventBridgeEvent,
|
||||
event_source,
|
||||
)
|
||||
from aws_lambda_powertools.utilities.typing import LambdaContext
|
||||
from layercake.dynamodb import (
|
||||
ComposeKey,
|
||||
DynamoDBPersistenceLayer,
|
||||
KeyPair,
|
||||
SortKey,
|
||||
)
|
||||
|
||||
from boto3clients import dynamodb_client
|
||||
from config import ENROLLMENT_TABLE, ORDER_TABLE
|
||||
|
||||
logger = Logger(__name__)
|
||||
enrollment_layer = DynamoDBPersistenceLayer(ENROLLMENT_TABLE, dynamodb_client)
|
||||
order_layer = DynamoDBPersistenceLayer(ORDER_TABLE, dynamodb_client)
|
||||
|
||||
|
||||
class TenantDoesNotExistError(Exception):
|
||||
def __init__(self, *args):
|
||||
super().__init__('Tenant does not exist')
|
||||
|
||||
|
||||
@event_source(data_class=EventBridgeEvent)
|
||||
@logger.inject_lambda_context
|
||||
def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool:
|
||||
new_image = event.detail['new_image']
|
||||
order_id = new_image['id']
|
||||
tenant_id = order_layer.collection.get_item(
|
||||
KeyPair(
|
||||
order_id,
|
||||
SortKey('metadata#tenant', path_spec='tenant_id'),
|
||||
),
|
||||
exc_cls=TenantDoesNotExistError,
|
||||
)
|
||||
|
||||
result = enrollment_layer.collection.query(
|
||||
KeyPair(
|
||||
# Post-migration: rename `vacancies` to `slots`
|
||||
ComposeKey(tenant_id, prefix='vacancies'),
|
||||
order_id,
|
||||
)
|
||||
)
|
||||
with enrollment_layer.batch_writer() as batch:
|
||||
for pair in result['items']:
|
||||
batch.delete_item(
|
||||
Key={
|
||||
# Post-migration: rename `vacancies` to `slots`
|
||||
'id': {'S': ComposeKey(pair['id'], prefix='vacancies')},
|
||||
'sk': {'S': pair['sk']},
|
||||
}
|
||||
)
|
||||
|
||||
return True
|
||||
@@ -1,4 +0,0 @@
|
||||
"""
|
||||
Stopgap events. Everything here is a quick fix and should be replaced with
|
||||
proper solutions.
|
||||
"""
|
||||
@@ -1,71 +0,0 @@
|
||||
from aws_lambda_powertools import Logger
|
||||
from aws_lambda_powertools.utilities.data_classes import (
|
||||
EventBridgeEvent,
|
||||
event_source,
|
||||
)
|
||||
from aws_lambda_powertools.utilities.typing import LambdaContext
|
||||
from layercake.dynamodb import (
|
||||
ComposeKey,
|
||||
DynamoDBPersistenceLayer,
|
||||
KeyPair,
|
||||
SortKey,
|
||||
TransactKey,
|
||||
)
|
||||
|
||||
from boto3clients import dynamodb_client
|
||||
from config import ENROLLMENT_TABLE, ORDER_TABLE, USER_TABLE
|
||||
|
||||
logger = Logger(__name__)
|
||||
user_layer = DynamoDBPersistenceLayer(USER_TABLE, dynamodb_client)
|
||||
order_layer = DynamoDBPersistenceLayer(ORDER_TABLE, dynamodb_client)
|
||||
enrollment_layer = DynamoDBPersistenceLayer(ENROLLMENT_TABLE, dynamodb_client)
|
||||
|
||||
|
||||
@event_source(data_class=EventBridgeEvent)
|
||||
@logger.inject_lambda_context
|
||||
def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool:
|
||||
"""Remove slots if the Tenant has a `metadata#billing_policy` and
|
||||
the order is positive."""
|
||||
new_image = event.detail['new_image']
|
||||
order_id = new_image['id']
|
||||
data = order_layer.collection.get_items(
|
||||
TransactKey(order_id)
|
||||
+ SortKey('0')
|
||||
+ KeyPair(
|
||||
pk=order_id,
|
||||
sk=SortKey(
|
||||
sk='metadata#tenant',
|
||||
path_spec='tenant_id',
|
||||
remove_prefix='metadata#',
|
||||
),
|
||||
rename_key='tenant_id',
|
||||
)
|
||||
)
|
||||
tenant_id = data['tenant_id'].removeprefix('ORG#')
|
||||
|
||||
policy = user_layer.collection.get_item(
|
||||
KeyPair(pk=tenant_id, sk='metadata#billing_policy'),
|
||||
raise_on_error=False,
|
||||
default=False,
|
||||
)
|
||||
|
||||
# Skip if missing billing policy or order is zero/negative
|
||||
if not policy or data['total'] <= 0:
|
||||
return False
|
||||
|
||||
result = enrollment_layer.collection.query(
|
||||
KeyPair(
|
||||
ComposeKey(tenant_id, prefix='vacancies'),
|
||||
order_id,
|
||||
)
|
||||
)
|
||||
with enrollment_layer.batch_writer() as batch:
|
||||
for pair in result['items']:
|
||||
batch.delete_item(
|
||||
Key={
|
||||
'id': {'S': ComposeKey(pair['id'], prefix='vacancies')},
|
||||
'sk': {'S': pair['sk']},
|
||||
}
|
||||
)
|
||||
|
||||
return True
|
||||
@@ -1,46 +0,0 @@
|
||||
from aws_lambda_powertools import Logger
|
||||
from aws_lambda_powertools.utilities.data_classes import (
|
||||
EventBridgeEvent,
|
||||
event_source,
|
||||
)
|
||||
from aws_lambda_powertools.utilities.typing import LambdaContext
|
||||
from layercake.dateutils import now
|
||||
from layercake.dynamodb import (
|
||||
DynamoDBPersistenceLayer,
|
||||
KeyPair,
|
||||
)
|
||||
|
||||
from boto3clients import dynamodb_client
|
||||
from config import ORDER_TABLE
|
||||
|
||||
logger = Logger(__name__)
|
||||
order_layer = DynamoDBPersistenceLayer(ORDER_TABLE, dynamodb_client)
|
||||
|
||||
|
||||
@event_source(data_class=EventBridgeEvent)
|
||||
@logger.inject_lambda_context
|
||||
def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool:
|
||||
new_image = event.detail['new_image']
|
||||
now_ = now()
|
||||
|
||||
with order_layer.transact_writer() as transact:
|
||||
transact.update(
|
||||
key=KeyPair(new_image['id'], '0'),
|
||||
update_expr='SET #status = :status, update_date = :update_date',
|
||||
expr_attr_names={
|
||||
'#status': 'status',
|
||||
},
|
||||
expr_attr_values={
|
||||
':status': 'PAID',
|
||||
':update_date': now_,
|
||||
},
|
||||
)
|
||||
transact.put(
|
||||
item={
|
||||
'id': new_image['id'],
|
||||
'sk': 'paid_date',
|
||||
'create_date': now_,
|
||||
}
|
||||
)
|
||||
|
||||
return True
|
||||
@@ -1,33 +0,0 @@
|
||||
[project]
|
||||
name = "order-management"
|
||||
version = "0.1.0"
|
||||
description = ""
|
||||
readme = ""
|
||||
requires-python = ">=3.13"
|
||||
dependencies = ["layercake"]
|
||||
|
||||
[dependency-groups]
|
||||
dev = [
|
||||
"jsonlines>=4.0.0",
|
||||
"pytest>=8.3.4",
|
||||
"pytest-cov>=6.0.0",
|
||||
"ruff>=0.9.1",
|
||||
]
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
pythonpath = ["app/"]
|
||||
addopts = "--cov --cov-report html -v"
|
||||
|
||||
[tool.ruff]
|
||||
target-version = "py311"
|
||||
src = ["app"]
|
||||
|
||||
[tool.ruff.format]
|
||||
quote-style = "single"
|
||||
|
||||
[tool.ruff.lint]
|
||||
select = ["E", "F", "I"]
|
||||
|
||||
|
||||
[tool.uv.sources]
|
||||
layercake = { path = "../layercake" }
|
||||
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"extraPaths": ["app/"]
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
version = 0.1
|
||||
[default.deploy.parameters]
|
||||
stack_name = "saladeaula-order-management"
|
||||
resolve_s3 = true
|
||||
s3_prefix = "order_management"
|
||||
region = "sa-east-1"
|
||||
confirm_changeset = false
|
||||
capabilities = "CAPABILITY_IAM"
|
||||
image_repositories = []
|
||||
@@ -1,139 +0,0 @@
|
||||
AWSTemplateFormatVersion: 2010-09-09
|
||||
Transform: AWS::Serverless-2016-10-31
|
||||
|
||||
Parameters:
|
||||
UserTable:
|
||||
Type: String
|
||||
Default: betaeducacao-prod-users_d2o3r5gmm4it7j
|
||||
EnrollmentTable:
|
||||
Type: String
|
||||
Default: betaeducacao-prod-enrollments
|
||||
OrderTable:
|
||||
Type: String
|
||||
Default: betaeducacao-prod-orders
|
||||
|
||||
Globals:
|
||||
Function:
|
||||
CodeUri: app/
|
||||
Runtime: python3.13
|
||||
Tracing: Active
|
||||
Architectures:
|
||||
- x86_64
|
||||
Layers:
|
||||
- !Sub arn:aws:lambda:sa-east-1:336641857101:layer:layercake:78
|
||||
Environment:
|
||||
Variables:
|
||||
TZ: America/Sao_Paulo
|
||||
LOG_LEVEL: DEBUG
|
||||
DYNAMODB_PARTITION_KEY: id
|
||||
POWERTOOLS_LOGGER_SAMPLE_RATE: 0.1
|
||||
POWERTOOLS_LOGGER_LOG_EVENT: true
|
||||
USER_TABLE: !Ref UserTable
|
||||
ORDER_TABLE: !Ref OrderTable
|
||||
ENROLLMENT_TABLE: !Ref EnrollmentTable
|
||||
|
||||
Resources:
|
||||
EventLog:
|
||||
Type: AWS::Logs::LogGroup
|
||||
Properties:
|
||||
RetentionInDays: 90
|
||||
|
||||
EventAssignTenantCnpjFunction:
|
||||
Type: AWS::Serverless::Function
|
||||
Properties:
|
||||
Handler: events.assign_tenant_cnpj.lambda_handler
|
||||
LoggingConfig:
|
||||
LogGroup: !Ref EventLog
|
||||
Policies:
|
||||
- DynamoDBCrudPolicy:
|
||||
TableName: !Ref UserTable
|
||||
- DynamoDBCrudPolicy:
|
||||
TableName: !Ref OrderTable
|
||||
Events:
|
||||
Event:
|
||||
Type: EventBridgeRule
|
||||
Properties:
|
||||
Pattern:
|
||||
resources: [!Ref OrderTable]
|
||||
detail-type: [INSERT]
|
||||
detail:
|
||||
new_image:
|
||||
sk: ["0"]
|
||||
cnpj:
|
||||
- exists: true
|
||||
metadata__tenant_id:
|
||||
- exists: false
|
||||
|
||||
EventRemoveSlotsOnCanceledFunction:
|
||||
Type: AWS::Serverless::Function
|
||||
Properties:
|
||||
Handler: events.delete_slots_on_canceled.lambda_handler
|
||||
LoggingConfig:
|
||||
LogGroup: !Ref EventLog
|
||||
Policies:
|
||||
- DynamoDBWritePolicy:
|
||||
TableName: !Ref OrderTable
|
||||
- DynamoDBWritePolicy:
|
||||
TableName: !Ref EnrollmentTable
|
||||
Events:
|
||||
Event:
|
||||
Type: EventBridgeRule
|
||||
Properties:
|
||||
Pattern:
|
||||
resources: [!Ref OrderTable]
|
||||
detail-type: [MODIFY]
|
||||
detail:
|
||||
new_image:
|
||||
sk: ["0"]
|
||||
cnpj:
|
||||
- exists: true
|
||||
status: [CANCELED, EXPIRED]
|
||||
|
||||
EventSetAsPaidFunction:
|
||||
Type: AWS::Serverless::Function
|
||||
Properties:
|
||||
Handler: events.stopgap.set_as_paid.lambda_handler
|
||||
LoggingConfig:
|
||||
LogGroup: !Ref EventLog
|
||||
Policies:
|
||||
- DynamoDBWritePolicy:
|
||||
TableName: !Ref OrderTable
|
||||
Events:
|
||||
Event:
|
||||
Type: EventBridgeRule
|
||||
Properties:
|
||||
Pattern:
|
||||
resources: [!Ref OrderTable]
|
||||
detail-type: [INSERT]
|
||||
detail:
|
||||
new_image:
|
||||
sk: ["0"]
|
||||
cnpj:
|
||||
- exists: true
|
||||
total: [0]
|
||||
status: [CREATING, PENDING]
|
||||
payment_method: [MANUAL]
|
||||
|
||||
EventRemoveSlotsFunction:
|
||||
Type: AWS::Serverless::Function
|
||||
Properties:
|
||||
Handler: events.stopgap.remove_slots.lambda_handler
|
||||
LoggingConfig:
|
||||
LogGroup: !Ref EventLog
|
||||
Policies:
|
||||
- DynamoDBReadPolicy:
|
||||
TableName: !Ref UserTable
|
||||
- DynamoDBReadPolicy:
|
||||
TableName: !Ref OrderTable
|
||||
- DynamoDBCrudPolicy:
|
||||
TableName: !Ref EnrollmentTable
|
||||
Events:
|
||||
DynamoDBEvent:
|
||||
Type: EventBridgeRule
|
||||
Properties:
|
||||
Pattern:
|
||||
resources: [!Ref OrderTable]
|
||||
detail:
|
||||
new_image:
|
||||
sk: [generated_items]
|
||||
status: [SUCCESS]
|
||||
@@ -1,72 +0,0 @@
|
||||
import os
|
||||
from dataclasses import dataclass
|
||||
|
||||
import jsonlines
|
||||
import pytest
|
||||
|
||||
PYTEST_TABLE_NAME = 'pytest'
|
||||
PK = 'id'
|
||||
SK = 'sk'
|
||||
|
||||
|
||||
# https://docs.pytest.org/en/7.1.x/reference/reference.html#pytest.hookspec.pytest_configure
|
||||
def pytest_configure():
|
||||
os.environ['TZ'] = 'America/Sao_Paulo'
|
||||
os.environ['DYNAMODB_PARTITION_KEY'] = PK
|
||||
os.environ['DYNAMODB_SORT_KEY'] = SK
|
||||
os.environ['USER_TABLE'] = PYTEST_TABLE_NAME
|
||||
os.environ['COURSE_TABLE'] = PYTEST_TABLE_NAME
|
||||
os.environ['ENROLLMENT_TABLE'] = PYTEST_TABLE_NAME
|
||||
os.environ['ORDER_TABLE'] = PYTEST_TABLE_NAME
|
||||
|
||||
|
||||
@dataclass
|
||||
class LambdaContext:
|
||||
function_name: str = 'test'
|
||||
memory_limit_in_mb: int = 128
|
||||
invoked_function_arn: str = 'arn:aws:lambda:eu-west-1:809313241:function:test'
|
||||
aws_request_id: str = '52fdfc07-2182-154f-163f-5f0f9a621d72'
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def lambda_context() -> LambdaContext:
|
||||
return LambdaContext()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def dynamodb_client():
|
||||
from boto3clients import dynamodb_client as client
|
||||
|
||||
client.create_table(
|
||||
AttributeDefinitions=[
|
||||
{'AttributeName': PK, 'AttributeType': 'S'},
|
||||
{'AttributeName': SK, 'AttributeType': 'S'},
|
||||
],
|
||||
TableName=PYTEST_TABLE_NAME,
|
||||
KeySchema=[
|
||||
{'AttributeName': PK, 'KeyType': 'HASH'},
|
||||
{'AttributeName': SK, 'KeyType': 'RANGE'},
|
||||
],
|
||||
ProvisionedThroughput={
|
||||
'ReadCapacityUnits': 123,
|
||||
'WriteCapacityUnits': 123,
|
||||
},
|
||||
)
|
||||
|
||||
yield client
|
||||
|
||||
client.delete_table(TableName=PYTEST_TABLE_NAME)
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def dynamodb_persistence_layer(dynamodb_client):
|
||||
from layercake.dynamodb import DynamoDBPersistenceLayer
|
||||
|
||||
return DynamoDBPersistenceLayer(PYTEST_TABLE_NAME, dynamodb_client)
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def dynamodb_seeds(dynamodb_client):
|
||||
with jsonlines.open('tests/seeds.jsonl') as lines:
|
||||
for line in lines:
|
||||
dynamodb_client.put_item(TableName=PYTEST_TABLE_NAME, Item=line)
|
||||
@@ -1,30 +0,0 @@
|
||||
from layercake.dynamodb import PartitionKey
|
||||
|
||||
import events.stopgap.remove_slots as app
|
||||
|
||||
from ...conftest import LambdaContext
|
||||
|
||||
|
||||
def test_remove_slots(
|
||||
dynamodb_seeds,
|
||||
dynamodb_persistence_layer,
|
||||
lambda_context: LambdaContext,
|
||||
):
|
||||
event = {
|
||||
'detail': {
|
||||
'new_image': {
|
||||
'id': '9omWNKymwU5U4aeun6mWzZ',
|
||||
'sk': 'generated_items',
|
||||
'create_date': '2024-07-23T20:43:37.303418-03:00',
|
||||
'status': 'SUCCESS',
|
||||
'scope': 'MILTI_USER',
|
||||
}
|
||||
},
|
||||
}
|
||||
assert app.lambda_handler(event, lambda_context) # type: ignore
|
||||
|
||||
result = dynamodb_persistence_layer.collection.query(
|
||||
PartitionKey('vacancies#cJtK9SsnJhKPyxESe7g3DG')
|
||||
)
|
||||
|
||||
assert len(result['items']) == 0
|
||||
@@ -1,24 +0,0 @@
|
||||
from aws_lambda_powertools.utilities.typing import LambdaContext
|
||||
from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair
|
||||
|
||||
import events.stopgap.set_as_paid as app
|
||||
|
||||
|
||||
def test_set_as_paid(
|
||||
dynamodb_seeds,
|
||||
dynamodb_persistence_layer: DynamoDBPersistenceLayer,
|
||||
lambda_context: LambdaContext,
|
||||
):
|
||||
event = {
|
||||
'detail': {
|
||||
'new_image': {
|
||||
'id': '9omWNKymwU5U4aeun6mWzZ',
|
||||
}
|
||||
}
|
||||
}
|
||||
assert app.lambda_handler(event, lambda_context) # type: ignore
|
||||
|
||||
doc = dynamodb_persistence_layer.get_item(
|
||||
key=KeyPair('9omWNKymwU5U4aeun6mWzZ', '0'),
|
||||
)
|
||||
assert doc['status'] == 'PAID'
|
||||
@@ -1,28 +0,0 @@
|
||||
from aws_lambda_powertools.utilities.typing import LambdaContext
|
||||
from layercake.dynamodb import DynamoDBPersistenceLayer, PartitionKey
|
||||
|
||||
import events.assign_tenant_cnpj as app
|
||||
|
||||
|
||||
def test_assign_tenant_cnpj(
|
||||
dynamodb_seeds,
|
||||
dynamodb_persistence_layer: DynamoDBPersistenceLayer,
|
||||
lambda_context: LambdaContext,
|
||||
):
|
||||
event = {
|
||||
'detail': {
|
||||
'new_image': {
|
||||
'id': '9omWNKymwU5U4aeun6mWzZ',
|
||||
'cnpj': '15608435000190',
|
||||
'email': 'sergio@somosbeta.com.br',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assert app.lambda_handler(event, lambda_context) # type: ignore
|
||||
|
||||
result = dynamodb_persistence_layer.collection.query(
|
||||
PartitionKey('9omWNKymwU5U4aeun6mWzZ')
|
||||
)
|
||||
|
||||
assert 4 == len(result['items'])
|
||||
@@ -1,29 +0,0 @@
|
||||
from aws_lambda_powertools.utilities.typing import LambdaContext
|
||||
from layercake.dynamodb import DynamoDBPersistenceLayer, PartitionKey
|
||||
|
||||
import events.assign_tenant_cnpj as app
|
||||
|
||||
|
||||
def test_assign_tenant_cnpj(
|
||||
dynamodb_seeds,
|
||||
dynamodb_persistence_layer: DynamoDBPersistenceLayer,
|
||||
lambda_context: LambdaContext,
|
||||
):
|
||||
event = {
|
||||
'detail': {
|
||||
'new_image': {
|
||||
'id': '9omWNKymwU5U4aeun6mWzZ',
|
||||
'cnpj': '15608435000190',
|
||||
'email': 'sergio@somosbeta.com.br',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assert app.lambda_handler(event, lambda_context) # type: ignore
|
||||
|
||||
result = dynamodb_persistence_layer.collection.query(
|
||||
PartitionKey('9omWNKymwU5U4aeun6mWzZ')
|
||||
)
|
||||
|
||||
assert 4 == len(result['items'])
|
||||
print(result['items'])
|
||||
@@ -1,27 +0,0 @@
|
||||
from aws_lambda_powertools.utilities.typing import LambdaContext
|
||||
from layercake.dynamodb import DynamoDBPersistenceLayer, PartitionKey
|
||||
|
||||
import events.remove_slots_on_canceled as app
|
||||
|
||||
|
||||
def test_delete_slots_on_canceled(
|
||||
dynamodb_seeds,
|
||||
dynamodb_persistence_layer: DynamoDBPersistenceLayer,
|
||||
lambda_context: LambdaContext,
|
||||
):
|
||||
event = {
|
||||
'detail': {
|
||||
'new_image': {
|
||||
'id': '9omWNKymwU5U4aeun6mWzZ',
|
||||
'status': 'CANCELED',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assert app.lambda_handler(event, lambda_context) # type: ignore
|
||||
|
||||
result = dynamodb_persistence_layer.collection.query(
|
||||
PartitionKey('vacancies#cJtK9SsnJhKPyxESe7g3DG')
|
||||
)
|
||||
|
||||
assert len(result['items']) == 0
|
||||
@@ -1,10 +0,0 @@
|
||||
{"id": {"S": "cJtK9SsnJhKPyxESe7g3DG"}, "sk": {"S": "metadata#payment_policy"}, "due_days": {"N": "90"}}
|
||||
{"id": {"S": "cJtK9SsnJhKPyxESe7g3DG"}, "sk": {"S": "metadata#billing_policy"}, "billing_day": {"N": "1"}, "payment_method": {"S": "PIX"}}
|
||||
{"id": {"S": "9omWNKymwU5U4aeun6mWzZ"}, "sk": {"S": "0"}, "total": {"N": "398"}, "status": {"S": "PENDING"}}
|
||||
{"id": {"S": "9omWNKymwU5U4aeun6mWzZ"}, "sk": {"S": "metadata#tenant"}, "tenant_id": {"S": "cJtK9SsnJhKPyxESe7g3DG"}}
|
||||
{"id": {"S": "cnpj"}, "sk": {"S": "15608435000190"}, "user_id": {"S": "cJtK9SsnJhKPyxESe7g3DG"}}
|
||||
{"id": {"S": "email"}, "sk": {"S": "sergio@somosbeta.com.br"}, "user_id": {"S": "5OxmMjL-ujoR5IMGegQz"}}
|
||||
{"id": {"S": "5OxmMjL-ujoR5IMGegQz"}, "sk": {"S": "0"}, "name": {"S": "Sérgio R Siqueira"}}
|
||||
{"id": {"S": "vacancies#cJtK9SsnJhKPyxESe7g3DG"}, "sk": {"S": "9omWNKymwU5U4aeun6mWzZ#1"}}
|
||||
{"id": {"S": "vacancies#cJtK9SsnJhKPyxESe7g3DG"}, "sk": {"S": "9omWNKymwU5U4aeun6mWzZ#2"}}
|
||||
{"id": {"S": "vacancies#cJtK9SsnJhKPyxESe7g3DG"}, "sk": {"S": "9omWNKymwU5U4aeun6mWzZ#3"}}
|
||||
1113
order-management/uv.lock
generated
1113
order-management/uv.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -1,11 +0,0 @@
|
||||
build:
|
||||
sam build --use-container
|
||||
|
||||
deploy: build
|
||||
sam deploy --debug
|
||||
|
||||
pytest:
|
||||
uv run pytest
|
||||
|
||||
htmlcov: pytest
|
||||
uv run python -m http.server 80 -d htmlcov
|
||||
@@ -1,4 +0,0 @@
|
||||
import os
|
||||
|
||||
MEILISEARCH_HOST: str = os.getenv('MEILISEARCH_HOST') # type: ignore
|
||||
MEILISEARCH_API_KEY: str = os.getenv('MEILISEARCH_API_KEY') # type: ignore
|
||||
@@ -1,32 +0,0 @@
|
||||
from arnparse import arnparse
|
||||
from aws_lambda_powertools.utilities.data_classes import (
|
||||
DynamoDBStreamEvent,
|
||||
event_source,
|
||||
)
|
||||
from aws_lambda_powertools.utilities.typing import LambdaContext
|
||||
from meilisearch import Client as Meilisearch
|
||||
|
||||
from config import MEILISEARCH_API_KEY, MEILISEARCH_HOST
|
||||
from meili import Op
|
||||
|
||||
meili_client = Meilisearch(MEILISEARCH_HOST, MEILISEARCH_API_KEY)
|
||||
|
||||
|
||||
@event_source(data_class=DynamoDBStreamEvent)
|
||||
def lambda_handler(event: DynamoDBStreamEvent, context: LambdaContext):
|
||||
with Op(meili_client) as op:
|
||||
for record in event.records:
|
||||
pk = record.dynamodb.keys['id'] # type: ignore
|
||||
new_image = record.dynamodb.new_image # type: ignore
|
||||
index = table_from_arn(record.event_source_arn) # type: ignore
|
||||
|
||||
op.append(
|
||||
index,
|
||||
op=record.event_name, # type: ignore
|
||||
data=new_image or pk,
|
||||
)
|
||||
|
||||
|
||||
def table_from_arn(arn: str) -> str:
|
||||
arn_ = arnparse(arn)
|
||||
return arn_.resource.split('/')[0]
|
||||
@@ -1,56 +0,0 @@
|
||||
from typing import Self
|
||||
|
||||
from aws_lambda_powertools.shared.json_encoder import Encoder
|
||||
from aws_lambda_powertools.utilities.data_classes.dynamo_db_stream_event import (
|
||||
DynamoDBRecordEventName,
|
||||
)
|
||||
from meilisearch import Client
|
||||
|
||||
|
||||
class Op:
|
||||
def __init__(self, client: Client) -> None:
|
||||
self.op = {}
|
||||
self.client = client
|
||||
|
||||
def __enter__(self) -> Self:
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
op = self.op
|
||||
client = self.client
|
||||
|
||||
for index_, ops in op.items():
|
||||
index = client.index(index_)
|
||||
|
||||
for op, doc in ops.items():
|
||||
match op:
|
||||
case DynamoDBRecordEventName.INSERT:
|
||||
index.add_documents(doc, serializer=JSONEncoder)
|
||||
case DynamoDBRecordEventName.MODIFY:
|
||||
index.update_documents(doc, serializer=JSONEncoder)
|
||||
case DynamoDBRecordEventName.REMOVE:
|
||||
index.delete_documents(doc)
|
||||
|
||||
self.op = {}
|
||||
|
||||
def append(
|
||||
self,
|
||||
index: str,
|
||||
/,
|
||||
op: DynamoDBRecordEventName,
|
||||
data: dict | str,
|
||||
) -> bool:
|
||||
if index not in self.op:
|
||||
self.op[index] = {}
|
||||
|
||||
if op not in self.op[index]:
|
||||
self.op[index][op] = []
|
||||
|
||||
return self.op[index][op].append(data)
|
||||
|
||||
|
||||
class JSONEncoder(Encoder):
|
||||
def default(self, obj):
|
||||
if isinstance(obj, set):
|
||||
return list(obj)
|
||||
return super(__class__, self).default(obj)
|
||||
@@ -1,23 +0,0 @@
|
||||
[project]
|
||||
name = "streams"
|
||||
version = "0.1.0"
|
||||
description = "Streaming DynamoDB events to Meilisearch and EventBridge."
|
||||
readme = ""
|
||||
requires-python = ">=3.12"
|
||||
dependencies = ["layercake"]
|
||||
|
||||
[dependency-groups]
|
||||
dev = [
|
||||
"pytest>=8.3.4",
|
||||
"pytest-cov>=6.0.0",
|
||||
"ruff>=0.9.1",
|
||||
]
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
addopts = "--cov --cov-report html -v"
|
||||
|
||||
[tool.ruff.format]
|
||||
quote-style = "single"
|
||||
|
||||
[tool.uv.sources]
|
||||
layercake = { path = "../layercake" }
|
||||
@@ -1,9 +0,0 @@
|
||||
version = 0.1
|
||||
[default.deploy.parameters]
|
||||
stack_name = "saladeaula-streams"
|
||||
resolve_s3 = true
|
||||
s3_prefix = "streams"
|
||||
region = "sa-east-1"
|
||||
confirm_changeset = false
|
||||
capabilities = "CAPABILITY_IAM"
|
||||
image_repositories = []
|
||||
@@ -1,54 +0,0 @@
|
||||
AWSTemplateFormatVersion: 2010-09-09
|
||||
Transform: AWS::Serverless-2016-10-31
|
||||
|
||||
Globals:
|
||||
Function:
|
||||
CodeUri: .
|
||||
Runtime: python3.13
|
||||
Architectures:
|
||||
- x86_64
|
||||
Layers:
|
||||
- !Sub arn:aws:lambda:sa-east-1:336641857101:layer:layercake:75
|
||||
Environment:
|
||||
Variables:
|
||||
LOG_LEVEL: DEBUG
|
||||
TZ: America/Sao_Paulo
|
||||
POWERTOOLS_LOGGER_SAMPLE_RATE: 0.1
|
||||
POWERTOOLS_LOGGER_LOG_EVENT: true
|
||||
MEILISEARCH_HOST: https://meili.eduseg.com.br
|
||||
MEILISEARCH_API_KEY: "{{resolve:ssm:/saladeaula/meili_api_key}}"
|
||||
|
||||
Resources:
|
||||
MeilisearchLog:
|
||||
Type: AWS::Logs::LogGroup
|
||||
Properties:
|
||||
RetentionInDays: 90
|
||||
|
||||
EventIndexDocsFunction:
|
||||
Type: AWS::Serverless::Function
|
||||
Properties:
|
||||
Handler: events.index_docs.lambda_handler
|
||||
LoggingConfig:
|
||||
LogGroup: !Ref MeilisearchLog
|
||||
Events:
|
||||
Enrollments:
|
||||
Type: DynamoDB
|
||||
Properties:
|
||||
Stream: !Sub arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/saladeaula_enrollments/stream/2025-06-04T16:44:42.524
|
||||
StartingPosition: LATEST
|
||||
MaximumRetryAttempts: 5
|
||||
BatchSize: 25
|
||||
FilterCriteria:
|
||||
Filters:
|
||||
- Pattern: '{ "dynamodb" : { "Keys" : { "sk" : { "S" : [ "0" ] } } } }'
|
||||
|
||||
Courses:
|
||||
Type: DynamoDB
|
||||
Properties:
|
||||
Stream: !Sub arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/saladeaula_courses/stream/2025-03-12T20:42:46.706
|
||||
StartingPosition: LATEST
|
||||
MaximumRetryAttempts: 5
|
||||
BatchSize: 25
|
||||
FilterCriteria:
|
||||
Filters:
|
||||
- Pattern: '{ "dynamodb" : { "Keys" : { "sk" : { "S" : [ "0" ] } } } }'
|
||||
@@ -1,34 +0,0 @@
|
||||
import json
|
||||
import os
|
||||
from dataclasses import dataclass
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
# https://docs.pytest.org/en/7.1.x/reference/reference.html#pytest.hookspec.pytest_configure
|
||||
def pytest_configure():
|
||||
os.environ['TZ'] = 'America/Sao_Paulo'
|
||||
os.environ['MEILISEARCH_HOST'] = 'http://127.0.0.1:7700'
|
||||
|
||||
|
||||
def load_jsonfile(path: str) -> dict:
|
||||
with open(path) as fp:
|
||||
return json.load(fp)
|
||||
|
||||
|
||||
@dataclass
|
||||
class LambdaContext:
|
||||
function_name: str = 'test'
|
||||
memory_limit_in_mb: int = 128
|
||||
invoked_function_arn: str = 'arn:aws:lambda:eu-west-1:809313241:function:test'
|
||||
aws_request_id: str = '52fdfc07-2182-154f-163f-5f0f9a621d72'
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def lambda_context() -> LambdaContext:
|
||||
return LambdaContext()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def dynamodb_stream_event():
|
||||
return load_jsonfile('tests/samples/dynamodb_stream_event.json')
|
||||
@@ -1,212 +0,0 @@
|
||||
{
|
||||
"Records": [
|
||||
{
|
||||
"eventID": "c4ca4238a0b923820dcc509a6f75849b",
|
||||
"eventName": "INSERT",
|
||||
"eventVersion": "1.1",
|
||||
"eventSource": "aws:dynamodb",
|
||||
"awsRegion": "us-east-1",
|
||||
"dynamodb": {
|
||||
"Keys": {
|
||||
"id": {
|
||||
"S": "102"
|
||||
}
|
||||
},
|
||||
"NewImage": {
|
||||
"message": {
|
||||
"S": "New item!!"
|
||||
},
|
||||
"id": {
|
||||
"S": "102"
|
||||
},
|
||||
"cpf": {
|
||||
"NULL": true
|
||||
},
|
||||
"tenant:org_id": {
|
||||
"SS": ["5OxmMjL-ujoR5IMGegQz"]
|
||||
}
|
||||
},
|
||||
"ApproximateCreationDateTime": 1428537600,
|
||||
"SequenceNumber": "4421584500000000017450439091",
|
||||
"SizeBytes": 26,
|
||||
"StreamViewType": "NEW_AND_OLD_IMAGES"
|
||||
},
|
||||
"eventSourceARN": "arn:aws:dynamodb:us-east-1:123456789012:table/example_table_with_stream/stream/2015-06-27T00:48:05.899"
|
||||
},
|
||||
{
|
||||
"eventID": "c4ca4238a0b923820dcc509a6f75849b",
|
||||
"eventName": "INSERT",
|
||||
"eventVersion": "1.1",
|
||||
"eventSource": "aws:dynamodb",
|
||||
"awsRegion": "us-east-1",
|
||||
"dynamodb": {
|
||||
"Keys": {
|
||||
"id": {
|
||||
"S": "102"
|
||||
}
|
||||
},
|
||||
"NewImage": {
|
||||
"message": {
|
||||
"S": "New item!"
|
||||
},
|
||||
"id": {
|
||||
"S": "101"
|
||||
},
|
||||
"cpf": {
|
||||
"NULL": true
|
||||
}
|
||||
},
|
||||
"ApproximateCreationDateTime": 1428537600,
|
||||
"SequenceNumber": "4421584500000000017450439091",
|
||||
"SizeBytes": 26,
|
||||
"StreamViewType": "NEW_AND_OLD_IMAGES"
|
||||
},
|
||||
"eventSourceARN": "arn:aws:dynamodb:us-east-1:123456789012:table/example_table_with_stream/stream/2015-06-27T00:48:05.899"
|
||||
},
|
||||
{
|
||||
"eventID": "c81e728d9d4c2f636f067f89cc14862c",
|
||||
"eventName": "MODIFY",
|
||||
"eventVersion": "1.1",
|
||||
"eventSource": "aws:dynamodb",
|
||||
"awsRegion": "us-east-1",
|
||||
"dynamodb": {
|
||||
"Keys": {
|
||||
"id": {
|
||||
"S": "101"
|
||||
}
|
||||
},
|
||||
"NewImage": {
|
||||
"message": {
|
||||
"S": "This item has changed"
|
||||
},
|
||||
"id": {
|
||||
"S": "101"
|
||||
},
|
||||
"assignee": {
|
||||
"M": {
|
||||
"name": {
|
||||
"S": "Sérgio R Siqueira"
|
||||
}
|
||||
}
|
||||
},
|
||||
"cpf": {
|
||||
"S": "07879819908"
|
||||
}
|
||||
},
|
||||
"OldImage": {
|
||||
"message": {
|
||||
"S": "New item!"
|
||||
},
|
||||
"id": {
|
||||
"S": "101"
|
||||
},
|
||||
"assignee": {
|
||||
"M": {
|
||||
"name": {
|
||||
"S": "Sérgio R Siqueira"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"ApproximateCreationDateTime": 1428537600,
|
||||
"SequenceNumber": "4421584500000000017450439092",
|
||||
"SizeBytes": 59,
|
||||
"StreamViewType": "NEW_AND_OLD_IMAGES"
|
||||
},
|
||||
"eventSourceARN": "arn:aws:dynamodb:us-east-1:123456789012:table/example_table_with_stream/stream/2015-06-27T00:48:05.899"
|
||||
},
|
||||
{
|
||||
"eventID": "eccbc87e4b5ce2fe28308fd9f2a7baf3",
|
||||
"eventName": "REMOVE",
|
||||
"eventVersion": "1.1",
|
||||
"eventSource": "aws:dynamodb",
|
||||
"awsRegion": "us-east-1",
|
||||
"dynamodb": {
|
||||
"Keys": {
|
||||
"id": {
|
||||
"S": "101"
|
||||
}
|
||||
},
|
||||
"OldImage": {
|
||||
"message": {
|
||||
"S": "This item has changed"
|
||||
},
|
||||
"id": {
|
||||
"S": "101"
|
||||
},
|
||||
"ttl": {
|
||||
"N": "1710532240"
|
||||
}
|
||||
},
|
||||
"ApproximateCreationDateTime": 1428537600,
|
||||
"SequenceNumber": "4421584500000000017450439093",
|
||||
"SizeBytes": 38,
|
||||
"StreamViewType": "NEW_AND_OLD_IMAGES"
|
||||
},
|
||||
"eventSourceARN": "arn:aws:dynamodb:us-east-1:123456789012:table/example_table_with_stream/stream/2015-06-27T00:48:05.899"
|
||||
},
|
||||
{
|
||||
"eventID": "eccbc87e4b5ce2fe28308fd9f2a7baf3",
|
||||
"eventName": "REMOVE",
|
||||
"eventVersion": "1.1",
|
||||
"eventSource": "aws:dynamodb",
|
||||
"awsRegion": "us-east-1",
|
||||
"dynamodb": {
|
||||
"Keys": {
|
||||
"id": {
|
||||
"S": "102"
|
||||
}
|
||||
},
|
||||
"OldImage": {
|
||||
"message": {
|
||||
"S": "This item has changed"
|
||||
},
|
||||
"id": {
|
||||
"S": "102"
|
||||
},
|
||||
"ttl": {
|
||||
"N": "2530997445"
|
||||
}
|
||||
},
|
||||
"ApproximateCreationDateTime": 1428537600,
|
||||
"SequenceNumber": "4421584500000000017450439093",
|
||||
"SizeBytes": 38,
|
||||
"StreamViewType": "NEW_AND_OLD_IMAGES"
|
||||
},
|
||||
"eventSourceARN": "arn:aws:dynamodb:us-east-1:123456789012:table/example_table_with_stream/stream/2015-06-27T00:48:05.899"
|
||||
},
|
||||
{
|
||||
"eventID": "bbb152116867ab05f3abfcadd4873bee",
|
||||
"eventName": "REMOVE",
|
||||
"eventVersion": "1.1",
|
||||
"eventSource": "aws:dynamodb",
|
||||
"awsRegion": "sa-east-1",
|
||||
"dynamodb": {
|
||||
"ApproximateCreationDateTime": 1710529909,
|
||||
"Keys": {
|
||||
"sk": {
|
||||
"S": "0"
|
||||
},
|
||||
"id": {
|
||||
"S": "DwHRXCm5bE64rcu5VA6ai6"
|
||||
}
|
||||
},
|
||||
"OldImage": {
|
||||
"sk": {
|
||||
"S": "0"
|
||||
},
|
||||
"id": {
|
||||
"S": "DwHRXCm5bE64rcu5VA6ai6"
|
||||
},
|
||||
"createDate": {
|
||||
"S": "2024-03-15T15:44:30.374640-03:00"
|
||||
}
|
||||
},
|
||||
"SequenceNumber": "3173521300000000009361288070",
|
||||
"SizeBytes": 156,
|
||||
"StreamViewType": "NEW_AND_OLD_IMAGES"
|
||||
},
|
||||
"eventSourceARN": "arn:aws:dynamodb:sa-east-1:336641857101:table/betaeducacao-prod-users_d2o3r5gmm4it7j/stream/2022-06-12T21:33:25.634"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
import events.index_docs as app
|
||||
|
||||
|
||||
def test_record_handler(monkeypatch, dynamodb_stream_event, lambda_context):
|
||||
app.lambda_handler(dynamodb_stream_event, lambda_context)
|
||||
1147
streams/uv.lock
generated
1147
streams/uv.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -1,5 +0,0 @@
|
||||
build:
|
||||
sam build --use-container
|
||||
|
||||
deploy: build
|
||||
sam deploy --debug
|
||||
@@ -1,14 +0,0 @@
|
||||
import os
|
||||
|
||||
import boto3
|
||||
|
||||
|
||||
def get_dynamodb_client():
|
||||
if os.getenv('AWS_LAMBDA_FUNCTION_NAME'):
|
||||
return boto3.client('dynamodb')
|
||||
|
||||
return boto3.client('dynamodb', endpoint_url='http://127.0.0.1:8000')
|
||||
|
||||
|
||||
dynamodb_client = get_dynamodb_client()
|
||||
s3_client = boto3.client('s3')
|
||||
@@ -1,4 +0,0 @@
|
||||
import os
|
||||
|
||||
USER_TABLE: str = os.getenv('USER_TABLE') # type: ignore
|
||||
CHUNK_SIZE = 50
|
||||
@@ -1,83 +0,0 @@
|
||||
import csv
|
||||
from typing import TextIO
|
||||
|
||||
from smart_open import open
|
||||
|
||||
|
||||
def byte_ranges(
|
||||
csvfile: str,
|
||||
chunk_size: int = 100,
|
||||
**kwargs,
|
||||
) -> list[tuple[int, int]]:
|
||||
"""Compute byte ranges for reading a CSV file in fixed-size line chunks.
|
||||
|
||||
Returns pairs (start_byte, end_byte) for each fixed-size group of lines.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
csvfile : str
|
||||
Path to the CSV file, opened in binary mode internally.
|
||||
chunk_size : int, optional
|
||||
Number of lines per chunk. Default is 100.
|
||||
**kwargs :
|
||||
Extra options passed to `open()`, e.g., buffering.
|
||||
|
||||
Returns
|
||||
-------
|
||||
list of tuple[int, int]
|
||||
Byte ranges covering each chunk of lines.
|
||||
|
||||
Example
|
||||
-------
|
||||
>>> byte_ranges("users.csv", chunk_size=500)
|
||||
[(0, 3125), (3126, 6150), (6151, 9124)]
|
||||
"""
|
||||
line_offsets = [0]
|
||||
|
||||
with open(csvfile, 'rb', **kwargs) as fp:
|
||||
while True:
|
||||
if not fp.readline():
|
||||
break
|
||||
line_offsets.append(fp.tell())
|
||||
|
||||
total_lines = len(line_offsets) - 1
|
||||
byte_ranges = []
|
||||
|
||||
for start_line in range(1, total_lines + 1, chunk_size):
|
||||
# Calculate the end line index, bounded by total lines
|
||||
end_line = min(start_line + chunk_size - 1, total_lines)
|
||||
# Get byte range for this chunk
|
||||
start_byte = line_offsets[start_line - 1]
|
||||
end_byte = line_offsets[end_line] - 1
|
||||
|
||||
byte_ranges.append((start_byte, end_byte))
|
||||
|
||||
return byte_ranges
|
||||
|
||||
|
||||
def detect_delimiter(sample: TextIO) -> str:
|
||||
"""Detect the delimiter character used in a CSV file.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
sample : TextIO
|
||||
A file-like object opened in text mode (e.g., from `open('file.csv')`).
|
||||
Must be readable and at position 0.
|
||||
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
The detected delimiter character (e.g., ',', ';', '\\t').
|
||||
|
||||
Raises
|
||||
------
|
||||
csv.Error
|
||||
If the file cannot be parsed as CSV or delimiter detection fails.
|
||||
ValueError
|
||||
If the file is empty or contains no detectable delimiter.
|
||||
"""
|
||||
sniffer = csv.Sniffer()
|
||||
dialect = sniffer.sniff(sample.read())
|
||||
sample.seek(0)
|
||||
|
||||
return dialect.delimiter
|
||||
@@ -1,20 +0,0 @@
|
||||
from aws_lambda_powertools.utilities.data_classes import (
|
||||
EventBridgeEvent,
|
||||
event_source,
|
||||
)
|
||||
from aws_lambda_powertools.utilities.typing import LambdaContext
|
||||
|
||||
from boto3clients import s3_client
|
||||
from config import CHUNK_SIZE
|
||||
from csv_utils import byte_ranges
|
||||
|
||||
transport_params = {'client': s3_client}
|
||||
|
||||
|
||||
@event_source(data_class=EventBridgeEvent)
|
||||
def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool:
|
||||
new_image = event.detail['new_image']
|
||||
csvfile = new_image['s3uri']
|
||||
pairs = byte_ranges(csvfile, CHUNK_SIZE, transport_params=transport_params)
|
||||
|
||||
return True
|
||||
@@ -1,14 +0,0 @@
|
||||
from aws_lambda_powertools.utilities.data_classes import (
|
||||
EventBridgeEvent,
|
||||
event_source,
|
||||
)
|
||||
from aws_lambda_powertools.utilities.typing import LambdaContext
|
||||
|
||||
from boto3clients import s3_client
|
||||
|
||||
transport_params = {'client': s3_client}
|
||||
|
||||
|
||||
@event_source(data_class=EventBridgeEvent)
|
||||
def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool:
|
||||
return True
|
||||
@@ -1,55 +0,0 @@
|
||||
import csv
|
||||
from io import StringIO
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from aws_lambda_powertools.utilities.data_classes import (
|
||||
EventBridgeEvent,
|
||||
event_source,
|
||||
)
|
||||
from aws_lambda_powertools.utilities.typing import LambdaContext
|
||||
|
||||
from boto3clients import s3_client
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from mypy_boto3_s3.client import S3Client
|
||||
else:
|
||||
S3Client = object
|
||||
|
||||
transport_params = {'client': s3_client}
|
||||
|
||||
|
||||
@event_source(data_class=EventBridgeEvent)
|
||||
def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool:
|
||||
new_image = event.detail['new_image']
|
||||
csvfile = new_image['s3_uri']
|
||||
|
||||
data = _get_s3_object_range(
|
||||
csvfile,
|
||||
start_byte=new_image['start_byte'],
|
||||
end_byte=new_image['end_byte'],
|
||||
s3_client=s3_client,
|
||||
)
|
||||
reader = csv.reader(data)
|
||||
|
||||
for x in reader:
|
||||
print(x)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def _get_s3_object_range(
|
||||
s3_uri: str,
|
||||
*,
|
||||
start_byte: int,
|
||||
end_byte: int,
|
||||
s3_client: S3Client,
|
||||
) -> StringIO:
|
||||
bucket, key = s3_uri.replace('s3://', '').split('/', 1)
|
||||
|
||||
response = s3_client.get_object(
|
||||
Bucket=bucket,
|
||||
Key=key,
|
||||
Range=f'bytes={start_byte}-{end_byte}',
|
||||
)
|
||||
|
||||
return StringIO(response['Body'].read().decode('utf-8'))
|
||||
@@ -1,40 +0,0 @@
|
||||
import urllib.parse as urllib_parse
|
||||
from email.utils import parseaddr
|
||||
|
||||
from aws_lambda_powertools import Logger
|
||||
from aws_lambda_powertools.utilities.data_classes import SESEvent, event_source
|
||||
from aws_lambda_powertools.utilities.typing import LambdaContext
|
||||
from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair, SortKey
|
||||
|
||||
from boto3clients import dynamodb_client
|
||||
from config import USER_TABLE
|
||||
from ses_utils import get_header_value
|
||||
|
||||
logger = Logger(__name__)
|
||||
user_layer = DynamoDBPersistenceLayer(USER_TABLE, dynamodb_client)
|
||||
|
||||
|
||||
@logger.inject_lambda_context
|
||||
@event_source(data_class=SESEvent)
|
||||
def lambda_handler(event: SESEvent, context: LambdaContext) -> dict:
|
||||
ses = event.record.ses
|
||||
to = urllib_parse.unquote(ses.receipt.recipients[0]).lower()
|
||||
name, email_from = parseaddr(get_header_value(ses.mail.headers, 'from'))
|
||||
|
||||
org_id = user_layer.collection.get_item(
|
||||
KeyPair('email', SortKey(to, path_spec='user_id')),
|
||||
raise_on_error=False,
|
||||
default={},
|
||||
)
|
||||
|
||||
if not org_id:
|
||||
return {'disposition': 'STOP_RULE_SET'}
|
||||
|
||||
print(
|
||||
{
|
||||
'id': f'mailbox#{org_id}',
|
||||
'sk': ses.mail.message_id,
|
||||
}
|
||||
)
|
||||
|
||||
return {'disposition': 'CONTINUE'}
|
||||
@@ -1,20 +0,0 @@
|
||||
from typing import Any, Iterator
|
||||
|
||||
from aws_lambda_powertools.utilities.data_classes.ses_event import SESMailHeader
|
||||
|
||||
|
||||
def get_header_value(
|
||||
headers: Iterator[SESMailHeader],
|
||||
header_name: str,
|
||||
*,
|
||||
default: Any = None,
|
||||
raise_on_missing: bool = True,
|
||||
) -> str:
|
||||
for header in headers:
|
||||
if header.name.lower() == header_name:
|
||||
return header.value
|
||||
|
||||
if raise_on_missing:
|
||||
raise ValueError(f'{header_name} not found.')
|
||||
|
||||
return default
|
||||
@@ -1,62 +0,0 @@
|
||||
# /// script
|
||||
# dependencies = [
|
||||
# "cloudflare"
|
||||
# ]
|
||||
# ///
|
||||
|
||||
from cloudflare import Cloudflare
|
||||
|
||||
CLOUDFLARE_ACCOUNT_ID = '5436b62470020c04b434ad31c3e4cf4e'
|
||||
CLOUDFLARE_API_TOKEN = 'gFndkBJCzH4pRX7mKXokdWfw1xhm8-9FHfvLfhwa'
|
||||
|
||||
|
||||
client = Cloudflare(api_token=CLOUDFLARE_API_TOKEN)
|
||||
|
||||
assistant = """
|
||||
You are a data analysis assistant specialized in identifying Brazilian
|
||||
personal data from CSV files.
|
||||
|
||||
These CSV files may or may not include headers.
|
||||
|
||||
Your task is to analyze the content and identify only three possible
|
||||
data types: 'name', 'cpf', and 'email'.
|
||||
|
||||
Ignore all other fields.
|
||||
"""
|
||||
|
||||
csv_content = """
|
||||
,RICARDO GALLES BONET,ricardo.bonet@fanucamerica.com,424.430.528-93,NR-10 (RECICLAGEM)
|
||||
,RULIO SIEFERT SERA,rulio.sera@fanucamerica.com,063.916.859-08,NR-10 (RECICLAGEM)
|
||||
,MACIEL FERREIRA BOMFIM,maciel.bomfim@fanucamerica.com,334.547.088-85,NR-10 (RECICLAGEM)
|
||||
,JAIME EDUARDO GALVEZ AVILES,jaime.galvez@fanucamerica.com,280.238.818-50,NR-12
|
||||
,JAIME EDUARDO GALVEZ AVILES,jaime.galvez@fanucamerica.com,280.238.818-50,NR-35 (RECICLAGEM)
|
||||
,HIGOR MACHADO SILVA,higor.silva@fanucamerica.com,419.879.878-88,NR-12
|
||||
,LÁZARO SOUZA DIAS,lazaro.dias@fanucamerica.com,067.179.825-19,NR-12
|
||||
,JOÃO PEDRO AGUIAR GALASSO,joao.pedro@fanucamerica.com,570.403.588-40,NR-12
|
||||
"""
|
||||
|
||||
prompt = f"""
|
||||
Here is a CSV sample:
|
||||
|
||||
{csv_content}
|
||||
|
||||
Your task is to:
|
||||
- Detect which columns most likely contain "name", "cpf", or "email".
|
||||
- Skip any category that is not present in the data.
|
||||
- Return ONLY a valid Python list of tuples, like:
|
||||
[('name', index), ('cpf', index), ('email', index)]
|
||||
- Use the column index that most likely matches each data type,
|
||||
based on frequency and data format.
|
||||
- Don't include explanations, code, or any additional text.
|
||||
"""
|
||||
|
||||
r = client.ai.run(
|
||||
model_name='@cf/meta/llama-3-8b-instruct',
|
||||
account_id=CLOUDFLARE_ACCOUNT_ID,
|
||||
messages=[
|
||||
{'role': 'system', 'content': assistant},
|
||||
{'role': 'user', 'content': prompt},
|
||||
],
|
||||
)
|
||||
|
||||
print(r)
|
||||
@@ -1,33 +0,0 @@
|
||||
[project]
|
||||
name = "user-management"
|
||||
version = "0.1.0"
|
||||
description = ""
|
||||
readme = ""
|
||||
requires-python = ">=3.13"
|
||||
dependencies = ["layercake"]
|
||||
|
||||
[dependency-groups]
|
||||
dev = [
|
||||
"boto3-stubs[essential]>=1.38.26",
|
||||
"jsonlines>=4.0.0",
|
||||
"pytest>=8.3.4",
|
||||
"pytest-cov>=6.0.0",
|
||||
"ruff>=0.9.1",
|
||||
]
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
pythonpath = ["app/"]
|
||||
addopts = "--cov --cov-report html -v"
|
||||
|
||||
[tool.ruff]
|
||||
target-version = "py311"
|
||||
src = ["app"]
|
||||
|
||||
[tool.ruff.format]
|
||||
quote-style = "single"
|
||||
|
||||
[tool.ruff.lint]
|
||||
select = ["E", "F", "I"]
|
||||
|
||||
[tool.uv.sources]
|
||||
layercake = { path = "../layercake" }
|
||||
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"extraPaths": ["app/"]
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
version = 0.1
|
||||
[default.deploy.parameters]
|
||||
stack_name = "saladeaula-user-management"
|
||||
resolve_s3 = true
|
||||
s3_prefix = "user_management"
|
||||
region = "sa-east-1"
|
||||
confirm_changeset = false
|
||||
capabilities = "CAPABILITY_IAM"
|
||||
image_repositories = []
|
||||
@@ -1,113 +0,0 @@
|
||||
AWSTemplateFormatVersion: 2010-09-09
|
||||
Transform: AWS::Serverless-2016-10-31
|
||||
|
||||
Parameters:
|
||||
BucketName:
|
||||
Type: String
|
||||
Default: saladeaula.digital
|
||||
UserTable:
|
||||
Type: String
|
||||
Default: betaeducacao-prod-users_d2o3r5gmm4it7j
|
||||
|
||||
Globals:
|
||||
Function:
|
||||
CodeUri: app/
|
||||
Runtime: python3.13
|
||||
Tracing: Active
|
||||
Architectures:
|
||||
- x86_64
|
||||
Layers:
|
||||
- !Sub arn:aws:lambda:sa-east-1:336641857101:layer:layercake:72
|
||||
Environment:
|
||||
Variables:
|
||||
TZ: America/Sao_Paulo
|
||||
LOG_LEVEL: DEBUG
|
||||
POWERTOOLS_LOGGER_SAMPLE_RATE: 0.1
|
||||
POWERTOOLS_LOGGER_LOG_EVENT: true
|
||||
USER_TABLE: !Ref UserTable
|
||||
|
||||
Resources:
|
||||
EventLog:
|
||||
Type: AWS::Logs::LogGroup
|
||||
Properties:
|
||||
RetentionInDays: 90
|
||||
|
||||
EventCsvChunksFunction:
|
||||
Type: AWS::Serverless::Function
|
||||
Properties:
|
||||
Handler: events.batch.csv_chunks.lambda_handler
|
||||
LoggingConfig:
|
||||
LogGroup: !Ref EventLog
|
||||
Policies:
|
||||
- S3CrudPolicy:
|
||||
BucketName: !Ref BucketName
|
||||
Events:
|
||||
DynamoDBEvent:
|
||||
Type: EventBridgeRule
|
||||
Properties:
|
||||
Pattern:
|
||||
resources: [betaeducacao-prod-users_d2o3r5gmm4it7j]
|
||||
detail:
|
||||
new_image:
|
||||
sk:
|
||||
- prefix: batch_jobs#
|
||||
|
||||
EventEmailReceivingFunction:
|
||||
Type: AWS::Serverless::Function
|
||||
Properties:
|
||||
Handler: events.email_receiving.lambda_handler
|
||||
LoggingConfig:
|
||||
LogGroup: !Ref EventLog
|
||||
Policies:
|
||||
- DynamoDBCrudPolicy:
|
||||
TableName: !Ref UserTable
|
||||
|
||||
LambdaInvokePermission:
|
||||
Type: AWS::Lambda::Permission
|
||||
Properties:
|
||||
FunctionName: !GetAtt EventEmailReceivingFunction.Arn
|
||||
Action: lambda:InvokeFunction
|
||||
Principal: ses.amazonaws.com
|
||||
SourceArn: !Sub arn:aws:ses:${AWS::Region}:${AWS::AccountId}:receipt-rule-set/*
|
||||
|
||||
BucketPolicy:
|
||||
Type: AWS::S3::BucketPolicy
|
||||
Properties:
|
||||
Bucket: !Ref BucketName
|
||||
PolicyDocument:
|
||||
Version: 2012-10-17
|
||||
Statement:
|
||||
- Effect: Allow
|
||||
Principal:
|
||||
Service: ses.amazonaws.com
|
||||
Action: s3:PutObject
|
||||
Resource: !Sub arn:aws:s3:::${BucketName}/*
|
||||
Condition:
|
||||
StringEquals:
|
||||
aws:SourceAccount: !Ref AWS::AccountId
|
||||
StringLike:
|
||||
aws:SourceArn: !Sub arn:aws:ses:${AWS::Region}:${AWS::AccountId}:receipt-rule-set/*
|
||||
|
||||
EmailReceiptRuleSet:
|
||||
Type: AWS::SES::ReceiptRuleSet
|
||||
Properties:
|
||||
RuleSetName: users.noreply.saladeaula.digital
|
||||
|
||||
EmailReceiptRule:
|
||||
Type: AWS::SES::ReceiptRule
|
||||
DependsOn:
|
||||
- LambdaInvokePermission
|
||||
- BucketPolicy
|
||||
Properties:
|
||||
RuleSetName: !Ref EmailReceiptRuleSet
|
||||
Rule:
|
||||
Name: lambda
|
||||
Enabled: true
|
||||
Actions:
|
||||
- LambdaAction:
|
||||
FunctionArn: !GetAtt EventEmailReceivingFunction.Arn
|
||||
InvocationType: RequestResponse
|
||||
- S3Action:
|
||||
BucketName: !Ref BucketName
|
||||
ObjectKeyPrefix: "mailbox"
|
||||
ScanEnabled: true
|
||||
@@ -1,69 +0,0 @@
|
||||
import os
|
||||
from dataclasses import dataclass
|
||||
|
||||
import jsonlines
|
||||
import pytest
|
||||
|
||||
PYTEST_TABLE_NAME = 'pytest'
|
||||
PK = 'id'
|
||||
SK = 'sk'
|
||||
|
||||
|
||||
# https://docs.pytest.org/en/7.1.x/reference/reference.html#pytest.hookspec.pytest_configure
|
||||
def pytest_configure():
|
||||
os.environ['TZ'] = 'America/Sao_Paulo'
|
||||
os.environ['DYNAMODB_PARTITION_KEY'] = PK
|
||||
os.environ['DYNAMODB_SORT_KEY'] = SK
|
||||
os.environ['USER_TABLE'] = PYTEST_TABLE_NAME
|
||||
|
||||
|
||||
@dataclass
|
||||
class LambdaContext:
|
||||
function_name: str = 'test'
|
||||
memory_limit_in_mb: int = 128
|
||||
invoked_function_arn: str = 'arn:aws:lambda:eu-west-1:809313241:function:test'
|
||||
aws_request_id: str = '52fdfc07-2182-154f-163f-5f0f9a621d72'
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def lambda_context() -> LambdaContext:
|
||||
return LambdaContext()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def dynamodb_client():
|
||||
from boto3clients import dynamodb_client as client
|
||||
|
||||
client.create_table(
|
||||
AttributeDefinitions=[
|
||||
{'AttributeName': PK, 'AttributeType': 'S'},
|
||||
{'AttributeName': SK, 'AttributeType': 'S'},
|
||||
],
|
||||
TableName=PYTEST_TABLE_NAME,
|
||||
KeySchema=[
|
||||
{'AttributeName': PK, 'KeyType': 'HASH'},
|
||||
{'AttributeName': SK, 'KeyType': 'RANGE'},
|
||||
],
|
||||
ProvisionedThroughput={
|
||||
'ReadCapacityUnits': 123,
|
||||
'WriteCapacityUnits': 123,
|
||||
},
|
||||
)
|
||||
|
||||
yield client
|
||||
|
||||
client.delete_table(TableName=PYTEST_TABLE_NAME)
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def dynamodb_persistence_layer(dynamodb_client):
|
||||
from layercake.dynamodb import DynamoDBPersistenceLayer
|
||||
|
||||
return DynamoDBPersistenceLayer(PYTEST_TABLE_NAME, dynamodb_client)
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def dynamodb_seeds(dynamodb_client):
|
||||
with jsonlines.open('tests/seeds.jsonl') as lines:
|
||||
for line in lines:
|
||||
dynamodb_client.put_item(TableName=PYTEST_TABLE_NAME, Item=line)
|
||||
@@ -1,13 +0,0 @@
|
||||
import events.batch.csv_into_chunks as app
|
||||
|
||||
|
||||
def test_chunk_csv(lambda_context):
|
||||
event = {
|
||||
'detail': {
|
||||
'new_image': {
|
||||
's3uri': 's3://saladeaula.digital/samples/large_users.csv',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
app.lambda_handler(event, lambda_context) # type: ignore
|
||||
@@ -1,136 +0,0 @@
|
||||
from aws_lambda_powertools.utilities.typing import LambdaContext
|
||||
|
||||
import events.email_receiving as app
|
||||
|
||||
event = {
|
||||
'Records': [
|
||||
{
|
||||
'eventSource': 'aws:ses',
|
||||
'eventVersion': '1.0',
|
||||
'ses': {
|
||||
'mail': {
|
||||
'timestamp': '2025-05-29T15:50:41.604Z',
|
||||
'source': 'sergio@somosbeta.com.br',
|
||||
'messageId': '2994higq3tr7efijr3lj65etntffapgg1q7hea81',
|
||||
'destination': [
|
||||
'org+15608435000190@users.noreply.saladeaula.digital'
|
||||
],
|
||||
'headersTruncated': False,
|
||||
'headers': [
|
||||
{'name': 'Return-Path', 'value': '<sergio@somosbeta.com.br>'},
|
||||
{
|
||||
'name': 'Received',
|
||||
'value': 'from mail-lf1-f54.google.com (mail-lf1-f54.google.com [209.85.167.54]) by inbound-smtp.sa-east-1.amazonaws.com with SMTP id 2994higq3tr7efijr3lj65etntffapgg1q7hea81 for org+35980592000130@users.noreply.saladeaula.digital; Thu, 29 May 2025 15:50:41 +0000 (UTC)',
|
||||
},
|
||||
{'name': 'X-SES-Spam-Verdict', 'value': 'PASS'},
|
||||
{'name': 'X-SES-Virus-Verdict', 'value': 'PASS'},
|
||||
{
|
||||
'name': 'Received-SPF',
|
||||
'value': 'pass (spfCheck: domain of somosbeta.com.br designates 209.85.167.54 as permitted sender) client-ip=209.85.167.54; envelope-from=sergio@somosbeta.com.br; helo=mail-lf1-f54.google.com;',
|
||||
},
|
||||
{
|
||||
'name': 'Authentication-Results',
|
||||
'value': 'amazonses.com; spf=pass (spfCheck: domain of somosbeta.com.br designates 209.85.167.54 as permitted sender) client-ip=209.85.167.54; envelope-from=sergio@somosbeta.com.br; helo=mail-lf1-f54.google.com; dkim=pass header.i=@somosbeta.com.br; dmarc=none header.from=somosbeta.com.br;',
|
||||
},
|
||||
{
|
||||
'name': 'X-SES-RECEIPT',
|
||||
'value': 'AEFBQUFBQUFBQUFHVWpuODdPY2tGUlordE5YWkVEUlZNWWZFYkpDMU5MUURyaHNVSldnTGhEVWhCQzd5UGpzWHI4LzJoS1VaN0lOU0FkMzJFU0h6MjVuUzk2c09KUXlzbUJQdHd6T0d0Y2ptZXhRVk1KY3RkOXpRamZMb3hwSGJIVlFla2tBcmZvRmYwQS9WU3hBVlBqcUpDYm00eTdiRnRqNW45ek9ld0ZyTGJKV3k2TXRpc0J6aGhBdmFvZDFDQ000Zm9QTng3VHljNXArM0hjT2ZsYkhtM3RCZnpRV1NOczU2RDdmL0RKclJOcDNvY2ZxV1hmajNYMkczVHpsWEZCMm40Z2pQM29udkMyb01vN3JwU0p2TUI1WGorN2JPd2RPYW5lUDN3T3RMRlhsdEpGbGNCa3c9PQ==',
|
||||
},
|
||||
{
|
||||
'name': 'X-SES-DKIM-SIGNATURE',
|
||||
'value': 'a=rsa-sha256; q=dns/txt; b=KPtFiBwsOTBl1YVLRTSfaZ+X6h7uSSOu/i1Cw6Pd+wvMBHRWy9EYcWUjyDjsLG/uYHShLW4+LHsSg9HiqrAP2jVJSAawrIwZr1wPQo7ovQvWuZfHQN/StgXIgBU+L7Bp6GSR26LRufxjj7q9YBmEeirjJ3d0G8E/rF2QqeITlpo=; c=relaxed/simple; s=bm3ypaoivbtdzmy3b6w37fzb5voa2uru; d=amazonses.com; t=1748533842; v=1; bh=kTUCV1DQAazu4FsUi1MrelD2QvSfHGArZ/c6A79t3/E=; h=From:To:Cc:Bcc:Subject:Date:Message-ID:MIME-Version:Content-Type:X-SES-RECEIPT;',
|
||||
},
|
||||
{
|
||||
'name': 'Received',
|
||||
'value': 'by mail-lf1-f54.google.com with SMTP id 2adb3069b0e04-54b0d638e86so1570269e87.1 for <org+35980592000130@users.noreply.saladeaula.digital>; Thu, 29 May 2025 08:50:40 -0700 (PDT)',
|
||||
},
|
||||
{
|
||||
'name': 'DKIM-Signature',
|
||||
'value': 'v=1; a=rsa-sha256; c=relaxed/relaxed; d=somosbeta.com.br; s=google; t=1748533838; x=1749138638; darn=users.noreply.saladeaula.digital; h=to:subject:message-id:date:from:in-reply-to:references:mime-version:from:to:cc:subject:date:message-id:reply-to; bh=kTUCV1DQAazu4FsUi1MrelD2QvSfHGArZ/c6A79t3/E=; b=Qi8gk/kTpwXCLDM7FPS7ULTy+9gO/4WsGL9zY1xEDw0Rp38f4rVR8L95hIhwK2daA27mq3pv9TdrK3XKQQIuSvRVvaM0b/evkZD8QhaT9tCmL0eKEBB4czGB0OSS3Q4qP34GFWMmXIaxoKIo1td76JnXbto9ZQvjUTBr3GGlF3Lm/MPTaAHs1b3dalv2diTvyj1tzoeb4wGePKsqLh5LKGOxbbWsxPeHEJ8sLM4LyJjxoqSOO0wgKdH5S/ZNpHWcJtXBntjiDUZNeQ5ucEn8ZLbADCObZZV/gH9i/cB1BmlSvJP3D07uJTAEBqyepd+W9fIW2mox/+fmOb3OEHRthQ==',
|
||||
},
|
||||
{
|
||||
'name': 'X-Google-DKIM-Signature',
|
||||
'value': 'v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20230601; t=1748533838; x=1749138638; h=to:subject:message-id:date:from:in-reply-to:references:mime-version :x-gm-message-state:from:to:cc:subject:date:message-id:reply-to; bh=kTUCV1DQAazu4FsUi1MrelD2QvSfHGArZ/c6A79t3/E=; b=UnL/uRXRahnH5uStZ276LH4kqpHigngw4iql9GHKmFaIKxJ8hLGn/wu7ie4ljnw8m/ I4CvhDKH4TVtIWPS81fm06PMgeqQYRX3jLhHvIltROCNVX6ZFzbAXgiAlk0NS1npvDYJ evVgSaPPco4D/8pMWZX4fUjU+8me32ChKxHsklEAts/LiD+MvTuHCHZovSEj1aXAz91b yGZe1bx2+phuqzUZyIOeheKjl4TNjEBx83omOzf9HtClKhjzCwHjfZ8uk2lhJ10ogKZ0 GNQ5OlnPkgdAg0/+HsifvGR6xfkFsiunIDyinBWoOhMU1o0+DiicxOIjY8QEayF3MLUt REoA==',
|
||||
},
|
||||
{
|
||||
'name': 'X-Gm-Message-State',
|
||||
'value': 'AOJu0Yxw0icQkFV090vn5hx/hKp0ePH78Wr0iqi4V3x4mpVXrRX8te2o 30aBYeZRPwn8SRSrq/kbn4bLcs5mPDB+iRP9IGFxS7KLSQi+KG4PQeDHyW3R/AgOPHACUUXUUyz Vcwny029WGY5PVhxlikAYdDfhNdO8GM2DKMV1+Oxy/a+qmt5LZeuy',
|
||||
},
|
||||
{
|
||||
'name': 'X-Gm-Gg',
|
||||
'value': 'ASbGncslCMPPU/pax0+RNy/cQR/Y/wUroSJMvI2DCCMq6Qld+Ih1jG4+HnhQPqn3nTK EEW6/99tqazq+SKy+31AB77ajVczvJQTElRSW/+bhd42l7by2hicTKElcR3GWivlrqd1TywUZOB DkB9J/vupSV0PDCJfZVi+7Tb9Pb61nnxaU+SQ=',
|
||||
},
|
||||
{
|
||||
'name': 'X-Google-Smtp-Source',
|
||||
'value': 'AGHT+IFYi41KmJjGcfQmUvWJDdTAzGIv2JlL9XAwBpAb53mMOOm3tttzkhbvfuiKh/DI9NjITHuO3xuEPqnPI9lpum8=',
|
||||
},
|
||||
{
|
||||
'name': 'X-Received',
|
||||
'value': 'by 2002:a05:6512:1392:b0:553:2f61:58f1 with SMTP id 2adb3069b0e04-5532f615a8dmr2268707e87.53.1748533837647; Thu, 29 May 2025 08:50:37 -0700 (PDT)',
|
||||
},
|
||||
{'name': 'MIME-Version', 'value': '1.0'},
|
||||
{
|
||||
'name': 'References',
|
||||
'value': '<CAMThe4mV9=1-BLiOi9MU3fAS=C6uYE9+3hKUjibrwxxngYNn2Q@mail.gmail.com>',
|
||||
},
|
||||
{
|
||||
'name': 'In-Reply-To',
|
||||
'value': '<CAMThe4mV9=1-BLiOi9MU3fAS=C6uYE9+3hKUjibrwxxngYNn2Q@mail.gmail.com>',
|
||||
},
|
||||
{
|
||||
'name': 'From',
|
||||
'value': 'Sérgio Rafael Siqueira <sergio@somosbeta.com.br>',
|
||||
},
|
||||
{'name': 'Date', 'value': 'Thu, 29 May 2025 12:50:26 -0300'},
|
||||
{
|
||||
'name': 'X-Gm-Features',
|
||||
'value': 'AX0GCFvofROqzf21KTgiIJq_AULCNljEuNFUJBk2xQGwVKmPjim_4slYIOP0WRw',
|
||||
},
|
||||
{
|
||||
'name': 'Message-ID',
|
||||
'value': '<CAMThe4=yMRJg4YOcACYAR509N1RyWyQgAghyVmr=NuSJnbondg@mail.gmail.com>',
|
||||
},
|
||||
{'name': 'Subject', 'value': 'Re: test'},
|
||||
{
|
||||
'name': 'To',
|
||||
'value': 'org+15608435000190@users.noreply.saladeaula.digital',
|
||||
},
|
||||
{
|
||||
'name': 'Content-Type',
|
||||
'value': 'multipart/alternative; boundary="00000000000045b8c206364842b3"',
|
||||
},
|
||||
],
|
||||
'commonHeaders': {
|
||||
'returnPath': 'sergio@somosbeta.com.br',
|
||||
'from': ['"Sérgio Rafael Siqueira" <sergio@somosbeta.com.br>'],
|
||||
'date': 'Thu, 29 May 2025 12:50:26 -0300',
|
||||
'to': ['org+15608435000190@users.noreply.saladeaula.digital'],
|
||||
'messageId': '<CAMThe4=yMRJg4YOcACYAR509N1RyWyQgAghyVmr=NuSJnbondg@mail.gmail.com>',
|
||||
'subject': 'Re: test',
|
||||
},
|
||||
},
|
||||
'receipt': {
|
||||
'timestamp': '2025-05-29T15:50:41.604Z',
|
||||
'processingTimeMillis': 1105,
|
||||
'recipients': [
|
||||
'org+15608435000190@users.noreply.saladeaula.digital'
|
||||
],
|
||||
'spamVerdict': {'status': 'PASS'},
|
||||
'virusVerdict': {'status': 'PASS'},
|
||||
'spfVerdict': {'status': 'PASS'},
|
||||
'dkimVerdict': {'status': 'PASS'},
|
||||
'dmarcVerdict': {'status': 'GRAY'},
|
||||
'action': {
|
||||
'type': 'Lambda',
|
||||
'functionArn': 'arn:aws:lambda:sa-east-1:336641857101:function:saladeaula-user-managemen-EventEmailReceivingFunct-LmnnEfi9tL2O',
|
||||
'invocationType': 'Event',
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
def test_email_receiving(dynamodb_seeds, lambda_context: LambdaContext):
|
||||
assert app.lambda_handler(event, lambda_context) == {'disposition': 'CONTINUE'}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,28 +0,0 @@
|
||||
CADASTRO DE COLABORADOR,,,,
|
||||
,NOME COMPLETO,EMAIL (letra minúscula),CPF,TREINAMENTO
|
||||
,ANDRE HENRIQUE LOPES ZAFALON,henrique.zafalon@fanucamerica.com,261.955.138-22,NR-35 (RECICLAGEM)
|
||||
,SERGIO DA SILVA CUPERTINO,sergio.cupertino@fanucamerica.com,066.945.708-64,NR-10 (RECICLAGEM)
|
||||
,SERGIO DA SILVA CUPERTINO,sergio.cupertino@fanucamerica.com,066.945.708-64,NR-35 (RECICLAGEM)
|
||||
,ROVANE CAMPOS,rovane.campos@fanucamerica.com,095.958.578-82,NR-10 (RECICLAGEM)
|
||||
,ROVANE CAMPOS,rovane.campos@fanucamerica.com,095.958.578-82,NR-35 (RECICLAGEM)
|
||||
,MARCIO ATSUSHI KANEKO MASUDA,marcio.masuda@fanucamerica.com,293.042.798-10,NR-10 (RECICLAGEM)
|
||||
,FABIO AKIRA HARAGUCHI,fabio.haraguchi@fanucamerica.com,287.018.428-03,NR-10 (RECICLAGEM)
|
||||
,EMIDIO YOITI MOCHIZUKI,emidio.mochizuki@fanucamerica.com,268.579.208-26,NR-10 (RECICLAGEM)
|
||||
,EMIDIO YOITI MOCHIZUKI,emidio.mochizuki@fanucamerica.com,268.579.208-26,NR-35 (RECICLAGEM)
|
||||
,ERIC HIDEKI MORIKIO,eric.morikio@fanucamerica.com,417.359.838-61,NR-10 (RECICLAGEM)
|
||||
,HENRIQUE DE FIGUEIREDO BASTOS FERRAZ,henrique.ferraz@fanucamerica.com,417.059.788-51,NR-10 (RECICLAGEM)
|
||||
,LAYS MORETTI DA SILVA,lays.silva@fanucamerica.com,013.107.662-07,NR-10 (RECICLAGEM)
|
||||
,LAYS MORETTI DA SILVA,lays.silva@fanucamerica.com,013.107.662-07,NR-12
|
||||
,ANDRE DE SOUZA,andre.souza@fanucamerica.com,290.688.648-31,NR-10 (RECICLAGEM)
|
||||
,ANDRE DE SOUZA,andre.souza@fanucamerica.com,290.688.648-31,NR-12
|
||||
,RAFAEL TOSHIO BURATO MAEDA,rafael.maeda@fanucamerica.com,394.153.268-59,NR-10 (RECICLAGEM)
|
||||
,RAFAEL TOSHIO BURATO MAEDA,rafael.maeda@fanucamerica.com,394.153.268-59,NR-12
|
||||
,RAFAEL TOSHIO BURATO MAEDA,rafael.maeda@fanucamerica.com,394.153.268-59,NR-35 (RECICLAGEM)
|
||||
,RICARDO GALLES BONET,ricardo.bonet@fanucamerica.com,424.430.528-93,NR-10 (RECICLAGEM)
|
||||
,RULIO SIEFERT SERA,rulio.sera@fanucamerica.com,063.916.859-08,NR-10 (RECICLAGEM)
|
||||
,MACIEL FERREIRA BOMFIM,maciel.bomfim@fanucamerica.com,334.547.088-85,NR-10 (RECICLAGEM)
|
||||
,JAIME EDUARDO GALVEZ AVILES,jaime.galvez@fanucamerica.com,280.238.818-50,NR-12
|
||||
,JAIME EDUARDO GALVEZ AVILES,jaime.galvez@fanucamerica.com,280.238.818-50,NR-35 (RECICLAGEM)
|
||||
,HIGOR MACHADO SILVA,higor.silva@fanucamerica.com,419.879.878-88,NR-12
|
||||
,LÁZARO SOUZA DIAS,lazaro.dias@fanucamerica.com,067.179.825-19,NR-12
|
||||
,JOÃO PEDRO AGUIAR GALASSO,joao.pedro@fanucamerica.com,570.403.588-40,NR-12
|
||||
|
@@ -1,4 +0,0 @@
|
||||
{"id": {"S": "cJtK9SsnJhKPyxESe7g3DG"}, "sk": {"S": "0"}, "name": {"S": "EDUSEG"}}
|
||||
{"id": {"S": "cJtK9SsnJhKPyxESe7g3DG"}, "sk": {"S": "admins#5OxmMjL-ujoR5IMGegQz"}, "name": {"S": "Sérgio R Siqueira"}, "email": {"S": "sergio@somosbeta.com.br"}}
|
||||
{"id": {"S": "cnpj"}, "sk": {"S": "15608435000190"}, "user_id": {"S": "cJtK9SsnJhKPyxESe7g3DG"}}
|
||||
{"id": {"S": "email"}, "sk": {"S": "org+15608435000190@users.noreply.saladeaula.digital"}, "user_id": {"S": "cJtK9SsnJhKPyxESe7g3DG"}}
|
||||
@@ -1,29 +0,0 @@
|
||||
from csv_utils import byte_ranges, detect_delimiter
|
||||
|
||||
|
||||
def test_detect_delimiter():
|
||||
with open('tests/samples/users.csv') as fp:
|
||||
assert detect_delimiter(fp) == ','
|
||||
|
||||
|
||||
def test_byte_ranges():
|
||||
csvpath = 'tests/samples/users.csv'
|
||||
ranges = byte_ranges(csvpath, 10)
|
||||
*_, pair = ranges
|
||||
start_byte, end_byte = pair
|
||||
|
||||
assert ranges == [(0, 808), (809, 1655), (1656, 2303)]
|
||||
|
||||
expected = """,RICARDO GALLES BONET,ricardo.bonet@fanucamerica.com,424.430.528-93,NR-10 (RECICLAGEM)
|
||||
,RULIO SIEFERT SERA,rulio.sera@fanucamerica.com,063.916.859-08,NR-10 (RECICLAGEM)
|
||||
,MACIEL FERREIRA BOMFIM,maciel.bomfim@fanucamerica.com,334.547.088-85,NR-10 (RECICLAGEM)
|
||||
,JAIME EDUARDO GALVEZ AVILES,jaime.galvez@fanucamerica.com,280.238.818-50,NR-12
|
||||
,JAIME EDUARDO GALVEZ AVILES,jaime.galvez@fanucamerica.com,280.238.818-50,NR-35 (RECICLAGEM)
|
||||
,HIGOR MACHADO SILVA,higor.silva@fanucamerica.com,419.879.878-88,NR-12
|
||||
,LÁZARO SOUZA DIAS,lazaro.dias@fanucamerica.com,067.179.825-19,NR-12
|
||||
,JOÃO PEDRO AGUIAR GALASSO,joao.pedro@fanucamerica.com,570.403.588-40,NR-12"""
|
||||
|
||||
with open(csvpath, 'rb') as f:
|
||||
f.seek(start_byte)
|
||||
data = f.read(end_byte - start_byte + 1)
|
||||
assert data.decode('utf-8') == expected
|
||||
1171
user-management/uv.lock
generated
1171
user-management/uv.lock
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user