add lookup

This commit is contained in:
2025-12-01 22:27:14 -03:00
parent f3e3d9f8c2
commit 8eb5427af4
22 changed files with 548 additions and 133 deletions

View File

@@ -5,8 +5,6 @@ import { AwardIcon, BanIcon, LaptopIcon } from 'lucide-react'
import { Suspense, useMemo } from 'react'
import { Await, useSearchParams } from 'react-router'
import placeholder from '@/assets/placeholder.webp'
import { SearchForm } from '@repo/ui/components/search-form'
import { Skeleton } from '@repo/ui/components/skeleton'
import {
@@ -27,6 +25,8 @@ import { cn } from '@repo/ui/lib/utils'
import { createSearch } from '@repo/util/meili'
import { request as req } from '@repo/util/request'
import placeholder from '@/assets/placeholder.webp'
type Cert = {
exp_interval: number
}
@@ -61,7 +61,7 @@ export async function loader({ context, request, params }: Route.LoaderArgs) {
url: `/orgs/${params.orgid}/custom-pricing`,
context,
request
}).then((r) => r.json())
}).then((r) => r.json() as Promise<{ items: CustomPricing[] }>)
return {
data: Promise.all([courses, customPricing])
@@ -70,7 +70,7 @@ export async function loader({ context, request, params }: Route.LoaderArgs) {
export default function Route({ loaderData: { data } }: Route.ComponentProps) {
const [searchParams, setSearchParams] = useSearchParams()
const term = searchParams.get('term') as string
const s = searchParams.get('s') as string
return (
<Suspense fallback={<Skeleton />}>
@@ -96,17 +96,15 @@ export default function Route({ loaderData: { data } }: Route.ComponentProps) {
pesquisar
</>
}
defaultValue={term}
onChange={(term) => {
setSearchParams({ term })
defaultValue={s}
onChange={(s) => {
setSearchParams({ s: String(s) })
}}
/>
</div>
</div>
<div className="grid lg:grid-cols-3 xl:grid-cols-4 gap-5">
<List term={term} hits={hits} customPricing={items} />
</div>
<List s={s} hits={hits as Course[]} customPricing={items} />
</>
)
}}
@@ -116,11 +114,11 @@ export default function Route({ loaderData: { data } }: Route.ComponentProps) {
}
function List({
term,
s,
hits = [],
customPricing = []
}: {
term: string
s: string
hits: Course[]
customPricing: CustomPricing[]
}) {
@@ -133,12 +131,12 @@ function List({
}, [hits])
const hits_ = useMemo(() => {
if (!term) {
if (!s) {
return hits
}
return fuse.search(term).map(({ item }) => item)
}, [term, fuse, hits])
return fuse.search(s).map(({ item }) => item)
}, [s, fuse, hits])
const customPricingMap = new Map(
customPricing.map((x) => {
@@ -149,29 +147,35 @@ function List({
if (hits_.length === 0) {
return (
<Empty>
<Empty className="border border-dashed">
<EmptyHeader>
<EmptyMedia variant="icon">
<BanIcon />
</EmptyMedia>
<EmptyTitle>Nada encontrado</EmptyTitle>
<EmptyDescription>
Nenhum resultado para <mark>{term}</mark>.
Nenhum resultado para <mark>{s}</mark>.
</EmptyDescription>
</EmptyHeader>
</Empty>
)
}
return hits_.map((props: Course, idx) => {
return (
<Course
key={idx}
custom_pricing={customPricingMap.get(props.id)}
{...props}
/>
)
})
return (
<div className="grid lg:grid-cols-3 xl:grid-cols-4 gap-5">
{hits_
.filter(({ metadata__unit_price = 0 }) => metadata__unit_price > 0)
.map((props: Course, idx) => {
return (
<Course
key={idx}
custom_pricing={customPricingMap.get(props.id)}
{...props}
/>
)
})}
</div>
)
}
function Course({

View File

@@ -177,7 +177,7 @@ export default function Route({}: Route.ComponentProps) {
/>
</FormControl>
<FormLabel className="cursor-pointer">
Usar um email fornecido pela plataforma.
Usar email gerado pela plataforma
</FormLabel>
</FormItem>
)}

View File

@@ -8,7 +8,9 @@ import {
export default [
layout('routes/layout.tsx', [
index('routes/index.tsx'),
route('/signup', 'routes/signup.tsx'),
layout('routes/register/layout.tsx', [
route('/register', 'routes/register/index.tsx')
]),
route('/forgot', 'routes/forgot.tsx'),
route('/deny', 'routes/deny.tsx')
]),

View File

@@ -136,7 +136,7 @@ export default function Index({}: Route.ComponentProps) {
<p className="text-white/50 text-sm">
Não tem uma senha?{' '}
<Link
to="/signup"
to="/register"
className="font-medium text-white hover:underline"
>
Criar senha
@@ -183,6 +183,7 @@ export default function Index({}: Route.ComponentProps) {
<FormControl>
<Input
type={show ? 'text' : 'password'}
autoComplete="false"
placeholder="••••••••"
{...field}
/>

View File

@@ -0,0 +1,97 @@
import { PatternFormat } from 'react-number-format'
import { useRequest } from 'ahooks'
import { z } from 'zod'
import { zodResolver } from '@hookform/resolvers/zod'
import { useForm } from 'react-hook-form'
import { Button } from '@repo/ui/components/ui/button'
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage
} from '@repo/ui/components/ui/form'
import { Input } from '@repo/ui/components/ui/input'
import { Spinner } from '@repo/ui/components/ui/spinner'
import { cpf, type RegisterContextProps, type User } from './data'
import { RegisterContext } from './data'
import { use } from 'react'
const formSchema = z.object({
cpf: cpf
})
type Schema = z.infer<typeof formSchema>
export function Cpf() {
const { setUser } = use(RegisterContext) as RegisterContextProps
const form = useForm({
resolver: zodResolver(formSchema)
})
const { control, handleSubmit, formState } = form
const { runAsync } = useRequest(
async ({ cpf }) => {
return await fetch(`/lookup?cpf=${cpf}`, {
method: 'GET',
headers: new Headers({ 'Content-Type': 'application/json' })
})
},
{ manual: true }
)
const onSubmit = async ({ cpf }: Schema) => {
const r = await runAsync({ cpf })
const user = (await r.json()) as any
setUser({ cpf, ...user })
}
return (
<>
<Form {...form}>
<form onSubmit={handleSubmit(onSubmit)}>
<fieldset disabled={formState.isSubmitting} className="grid gap-6">
<FormField
control={control}
name="cpf"
defaultValue=""
render={({ field: { ref, onChange, ...props } }) => (
<FormItem>
<FormLabel>CPF</FormLabel>
<FormControl>
<PatternFormat
format="###.###.###-##"
mask="_"
placeholder="___.___.___-__"
customInput={Input}
autoFocus={true}
getInputRef={ref}
onValueChange={({ value }) => {
onChange(value)
}}
{...props}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button
type="submit"
className="w-full cursor-pointer relative overflow-hidden"
>
{formState.isSubmitting && (
<div className="absolute bg-lime-500 inset-0 flex items-center justify-center">
<Spinner />
</div>
)}
Continuar
</Button>
</fieldset>
</form>
</Form>
</>
)
}

View File

@@ -0,0 +1,39 @@
import { z } from 'zod'
import { isValidCPF } from '@brazilian-utils/brazilian-utils'
import { createContext } from 'react'
const isName = (name: string) => name && name.includes(' ')
export type User = {
id?: string | null
cpf: string
name: string
email: string
}
export const cpf = z
.string()
.nonempty('Digite seu CPF')
.refine(isValidCPF, 'Deve ser um CPF válido')
export const formSchema = z.object({
name: z
.string()
.trim()
.nonempty('Digite seu nome')
.refine(isName, { message: 'Nome inválido' }),
email: z.email('Digite seu email'),
password: z
.string()
.nonempty('Digite sua senha')
.min(6, 'Deve ter no mínimo 6 caracteres'),
cpf: cpf
})
export type Schema = z.infer<typeof formSchema>
export type RegisterContextProps = {
user: User | null
setUser: (user: User) => void
}
export const RegisterContext = createContext<RegisterContextProps | null>(null)

View File

@@ -1,13 +1,10 @@
import type { Route } from './+types'
import type { Route } from '../+types'
import { isValidCPF } from '@brazilian-utils/brazilian-utils'
import { PatternFormat } from 'react-number-format'
import { zodResolver } from '@hookform/resolvers/zod'
import { useState } from 'react'
import { useState, createContext, type ReactNode, use } from 'react'
import { useForm } from 'react-hook-form'
import { Link } from 'react-router'
import { z } from 'zod'
import logo from '@repo/ui/components/logo2.svg'
import { Button } from '@repo/ui/components/ui/button'
import { Checkbox } from '@repo/ui/components/ui/checkbox'
import {
@@ -21,20 +18,8 @@ import {
import { Input } from '@repo/ui/components/ui/input'
import { Label } from '@repo/ui/components/ui/label'
const schema = z.object({
name: z.string().trim().nonempty('Digite seu nome'),
email: z.email('Digite seu email'),
password: z
.string()
.nonempty('Digite sua senha')
.min(6, 'Deve ter no mínimo 6 caracteres'),
cpf: z
.string()
.nonempty('Digite seu CPF')
.refine(isValidCPF, 'Deve ser um CPF válido')
})
type Schema = z.infer<typeof schema>
import { Cpf } from './cpf'
import { formSchema, type Schema, RegisterContext, type User } from './data'
export function meta({}: Route.MetaArgs) {
return [{ title: 'Criar conta · EDUSEG®' }]
@@ -42,8 +27,9 @@ export function meta({}: Route.MetaArgs) {
export default function Signup({}: Route.ComponentProps) {
const [show, setShow] = useState(false)
const [user, setUser] = useState<User | null>(null)
const form = useForm({
resolver: zodResolver(schema)
resolver: zodResolver(formSchema)
})
const { control, handleSubmit, formState } = form
@@ -51,34 +37,17 @@ export default function Signup({}: Route.ComponentProps) {
console.log(data)
}
console.log(user)
return (
<>
<div className="space-y-6">
<div className="flex justify-center">
<div className="border border-white/15 bg-white/5 px-2.5 py-3 rounded-xl">
<img src={logo} alt="EDUSEG®" className="block size-12" />
</div>
</div>
<div className="text-center space-y-1.5">
<h1 className="text-2xl font-semibold font-display text-balance">
Criar conta
</h1>
<p className="text-white/50 text-sm">
tem uma conta?{' '}
<Link to="/" className="font-medium text-white hover:underline">
Faça login
</Link>
.
</p>
</div>
<RegisterContext value={{ user, setUser }}>
{user ? (
<Form {...form}>
<form onSubmit={handleSubmit(onSubmit)} className="grid gap-6">
<FormField
control={control}
name="name"
defaultValue=""
defaultValue={user?.name}
render={({ field }) => (
<FormItem>
<FormLabel>Nome</FormLabel>
@@ -93,6 +62,7 @@ export default function Signup({}: Route.ComponentProps) {
<FormField
control={control}
name="email"
defaultValue={user?.email}
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
@@ -107,12 +77,22 @@ export default function Signup({}: Route.ComponentProps) {
<FormField
control={control}
name="cpf"
defaultValue=""
render={({ field }) => (
defaultValue={user.cpf}
render={({ field: { ref, onChange, ...props } }) => (
<FormItem>
<FormLabel>CPF</FormLabel>
<FormControl>
<Input placeholder="___.___.___-__" {...field} />
<PatternFormat
format="###.###.###-##"
mask="_"
placeholder="___.___.___-__"
customInput={Input}
getInputRef={ref}
onValueChange={({ value }) => {
onChange(value)
}}
{...props}
/>
</FormControl>
<FormMessage />
</FormItem>
@@ -130,6 +110,7 @@ export default function Signup({}: Route.ComponentProps) {
<Input
type={show ? 'text' : 'password'}
placeholder="••••••••"
autoComplete="false"
{...field}
/>
</FormControl>
@@ -155,19 +136,9 @@ export default function Signup({}: Route.ComponentProps) {
</Button>
</form>
</Form>
<p className="text-white/50 text-xs text-center">
Ao fazer login, você concorda com nossa{' '}
<a
href="//eduseg.com.br/politica"
target="_blank"
className="underline hover:no-underline"
>
política de privacidade
</a>
.
</p>
</div>
</>
) : (
<Cpf />
)}
</RegisterContext>
)
}

View File

@@ -0,0 +1,50 @@
import type { Route } from './+types/index'
import { Outlet, Link } from 'react-router'
import logo from '@repo/ui/components/logo2.svg'
export function meta({}: Route.MetaArgs) {
return [{ title: 'Criar conta · EDUSEG®' }]
}
export default function Layout() {
return (
<>
<div className="space-y-6">
<div className="flex justify-center">
<div className="border border-white/15 bg-white/5 px-2.5 py-3 rounded-xl">
<img src={logo} alt="EDUSEG®" className="block size-12" />
</div>
</div>
<div className="text-center space-y-1.5">
<h1 className="text-2xl font-semibold font-display text-balance">
Criar conta
</h1>
<p className="text-white/50 text-sm">
tem uma conta?{' '}
<Link to="/" className="font-medium text-white hover:underline">
Faça login
</Link>
.
</p>
</div>
<Outlet />
<p className="text-white/50 text-xs text-center">
Ao cadastrar, você concorda com nossa{' '}
<a
href="//eduseg.com.br/politica"
target="_blank"
className="underline hover:no-underline"
>
política de privacidade
</a>
.
</p>
</div>
</>
)
}

View File

@@ -9,8 +9,8 @@ async function proxy({
request,
context
}: Route.ActionArgs): Promise<Response> {
const pathname = new URL(request.url).pathname
const url = new URL(pathname, context.cloudflare.env.ISSUER_URL)
const { pathname, search } = new URL(request.url)
const url = new URL(pathname + search, context.cloudflare.env.ISSUER_URL)
const headers = new Headers(request.headers)
const shouldCache =

View File

@@ -87,7 +87,7 @@ export default function Component({
loaderData: { data }
}: Route.ComponentProps) {
const [searchParams, setSearchParams] = useSearchParams()
const term = searchParams.get('term') as string
const s = searchParams.get('s') as string
return (
<Container className="space-y-4">
@@ -104,7 +104,7 @@ export default function Component({
<div className="flex gap-2.5">
<div className="w-full xl:w-1/3">
<SearchForm
defaultValue={term || ''}
defaultValue={s || ''}
placeholder={
<>
Digite <Kbd>/</Kbd> para pesquisar
@@ -112,7 +112,7 @@ export default function Component({
}
onChange={(value) =>
setSearchParams((searchParams) => {
searchParams.set('term', String(value))
searchParams.set('s', String(value))
return searchParams
})
}
@@ -145,14 +145,14 @@ export default function Component({
</div>
<Await resolve={data}>
{({ hits = [] }) => <List term={term} hits={hits as Enrollment[]} />}
{({ hits = [] }) => <List s={s} hits={hits as Enrollment[]} />}
</Await>
</Suspense>
</Container>
)
}
function List({ term, hits = [] }: { term: string; hits: Enrollment[] }) {
function List({ s, hits = [] }: { s: string; hits: Enrollment[] }) {
const fuse = useMemo(() => {
return new Fuse(hits, {
keys: ['course.name'],
@@ -162,12 +162,12 @@ function List({ term, hits = [] }: { term: string; hits: Enrollment[] }) {
}, [hits])
const hits_ = useMemo(() => {
if (!term) {
if (!s) {
return hits
}
return fuse.search(term).map(({ item }) => item)
}, [term, fuse, hits])
return fuse.search(s).map(({ item }) => item)
}, [s, fuse, hits])
if (hits_.length === 0) {
return (
@@ -178,7 +178,7 @@ function List({ term, hits = [] }: { term: string; hits: Enrollment[] }) {
</EmptyMedia>
<EmptyTitle>Nada encontrado</EmptyTitle>
<EmptyDescription>
Nenhum resultado para <mark>{term}</mark>.
Nenhum resultado para <mark>{s}</mark>.
</EmptyDescription>
</EmptyHeader>
</Empty>

View File

@@ -69,11 +69,15 @@ def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool:
update_expr = 'SET cert = :cert, updated_at = :now'
expr_attr_values = {
':now': now_,
':cert': {'issued_at': now_} | ({'s3_uri': s3_uri} if s3_uri else {}),
':cert': {'issued_at': now_}
| ({'expires_at': expires_at} if expires_at else {})
| ({'s3_uri': s3_uri} if s3_uri else {}),
}
# Post-migration: remove the following lines
if expires_at:
update_expr = 'SET cert = :cert, cert_expires_at = :cert_expires_at, \
update_expr = 'SET cert = :cert, \
cert_expires_at = :cert_expires_at, \
updated_at = :now'
expr_attr_values[':cert_expires_at'] = expires_at

View File

@@ -41,9 +41,9 @@ Globals:
POSTGRES_DB: saladeaula.digital
POSTGRES_HOST: sp-node01.saladeaula.digital
POSTGRES_PORT: 5432
POSTGRES_USER: "{{resolve:ssm:/saladeaula/postgres_user}}"
POSTGRES_PASSWORD: "{{resolve:ssm:/saladeaula/postgres_password}}"
DOCUSEAL_KEY: "{{resolve:ssm:/saladeaula/docuseal_key}}"
POSTGRES_USER: '{{resolve:ssm:/saladeaula/postgres_user}}'
POSTGRES_PASSWORD: '{{resolve:ssm:/saladeaula/postgres_password}}'
DOCUSEAL_KEY: '{{resolve:ssm:/saladeaula/docuseal_key}}'
Resources:
EventLog:
@@ -60,7 +60,7 @@ Resources:
Type: AWS::Serverless::HttpApi
Properties:
CorsConfiguration:
AllowOrigins: ["*"]
AllowOrigins: ['*']
AllowMethods: [POST, OPTIONS]
AllowHeaders: [Content-Type, X-Requested-With]
@@ -104,7 +104,7 @@ Resources:
detail-type: [INSERT]
detail:
new_image:
sk: ["0"]
sk: ['0']
org_id:
- exists: true
@@ -128,7 +128,7 @@ Resources:
detail-type: [INSERT]
detail:
new_image:
sk: ["0"]
sk: ['0']
access_expires_at:
- exists: false
@@ -154,7 +154,7 @@ Resources:
detail-type: [INSERT]
detail:
new_image:
sk: ["0"]
sk: ['0']
EventEnrollFunction:
Type: AWS::Serverless::Function
@@ -185,6 +185,27 @@ Resources:
scope: [SINGLE_USER]
status: [PENDING]
EventEnrollScheduledFunction:
Type: AWS::Serverless::Function
Properties:
Handler: events.enroll_scheduled.lambda_handler
LoggingConfig:
LogGroup: !Ref EventLog
Policies:
- DynamoDBCrudPolicy:
TableName: !Ref EnrollmentTable
Events:
DynamoDBEvent:
Type: EventBridgeRule
Properties:
Pattern:
resources: [!Ref EnrollmentTable]
detail-type: [EXPIRE]
detail:
keys:
id:
- prefix: SCHEDULED#ORG#
EventReenrollIfFailedFunction:
Type: AWS::Serverless::Function
Properties:
@@ -204,10 +225,10 @@ Resources:
detail:
changes: [status]
new_image:
sk: ["0"]
sk: ['0']
status: [FAILED]
score:
- numeric: ["<", 70]
- numeric: ['<', 70]
old_image:
status: [IN_PROGRESS]
@@ -298,9 +319,10 @@ Resources:
detail-type: [INSERT]
detail:
new_image:
sk: ["0"]
sk: ['0']
status: [PENDING]
# Deprecated
EventSetAccessExpiredFunction:
Type: AWS::Serverless::Function
Properties:
@@ -321,6 +343,7 @@ Resources:
keys:
sk: [SCHEDULE#SET_ACCESS_EXPIRED, SCHEDULE#SET_AS_EXPIRED]
# Deprecated
EventSetCertExpiredFunction:
Type: AWS::Serverless::Function
Properties:
@@ -364,7 +387,7 @@ Resources:
resources: [!Ref EnrollmentTable]
detail:
keys:
sk: ["0"]
sk: ['0']
new_image:
status: [COMPLETED]
old_image:
@@ -389,7 +412,7 @@ Resources:
resources: [!Ref EnrollmentTable]
detail:
keys:
sk: ["0"]
sk: ['0']
new_image:
status: [COMPLETED]
cert:
@@ -417,7 +440,7 @@ Resources:
detail-type: [MODIFY]
detail:
keys:
sk: ["0"]
sk: ['0']
new_image:
status: [COMPLETED]
cert_expires_at:
@@ -463,7 +486,7 @@ Outputs:
HttpApiUrl:
Description: URL of your API endpoint
Value:
Fn::Sub: "https://${HttpApi}.execute-api.${AWS::Region}.${AWS::URLSuffix}"
Fn::Sub: 'https://${HttpApi}.execute-api.${AWS::Region}.${AWS::URLSuffix}'
HttpApiId:
Description: Api ID of HttpApi
Value:

View File

@@ -9,9 +9,12 @@ from aws_lambda_powertools.utilities.typing import LambdaContext
from routes.authentication import router as authentication
from routes.authorize import router as authorize
from routes.forgot import router as forgot
from routes.jwks import router as jwks
from routes.lookup import router as lookup
from routes.openid_configuration import router as openid_configuration
from routes.register import router as register
from routes.reset import router as reset
from routes.revoke import router as revoke
from routes.token import router as token
from routes.userinfo import router as userinfo
@@ -21,9 +24,12 @@ tracer = Tracer()
app = APIGatewayHttpResolver(enable_validation=True)
app.include_router(authentication)
app.include_router(authorize)
app.include_router(forgot)
app.include_router(jwks)
app.include_router(lookup)
app.include_router(openid_configuration)
app.include_router(register)
app.include_router(reset)
app.include_router(revoke)
app.include_router(token)
app.include_router(userinfo)

View File

@@ -0,0 +1,12 @@
from typing import Annotated
from aws_lambda_powertools.event_handler.api_gateway import Router
from aws_lambda_powertools.event_handler.openapi.params import Body
from pydantic import EmailStr
router = Router()
@router.post('/forgot')
def forgot(email: Annotated[EmailStr, Body(embed=True)]):
return {}

View File

@@ -0,0 +1,49 @@
from typing import Annotated
from aws_lambda_powertools.event_handler.api_gateway import Router
from aws_lambda_powertools.event_handler.openapi.params import Path
from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair, SortKey, TransactKey
from layercake.extra_types import CnpjStr, CpfStr
from layercake.funcs import pick
from pydantic import EmailStr
from boto3clients import dynamodb_client
from config import OAUTH2_TABLE
router = Router()
dyn = DynamoDBPersistenceLayer(OAUTH2_TABLE, dynamodb_client)
@router.get('/lookup')
def lookup(
email: Annotated[EmailStr, Path] = 'unknown',
cpf: Annotated[CpfStr, Path] = 'unknown',
cnpj: Annotated[CnpjStr, Path] = 'unknown',
):
r = dyn.collection.get_items(
KeyPair(
pk='email',
sk=SortKey(email, path_spec='user_id'),
rename_key='id',
)
+ KeyPair(
pk='cpf',
sk=SortKey(cpf, path_spec='user_id'),
rename_key='id',
)
+ KeyPair(
pk='cnpj',
sk=SortKey(cnpj, path_spec='user_id'),
rename_key='org_id',
),
flatten_top=False,
)
if 'id' in r:
user = dyn.collection.get_items(
TransactKey(r['id']) + SortKey('0') + SortKey('FRESH_USER')
)
return r | pick(('name', 'email'), user) if 'FRESH_USER' in user else {}
return r

View File

@@ -1,8 +1,41 @@
from http import HTTPStatus
from typing import Annotated
from aws_lambda_powertools.event_handler.api_gateway import Router
from aws_lambda_powertools.event_handler.exceptions import ServiceError
from aws_lambda_powertools.event_handler.openapi.params import Body
from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair, SortKey
from layercake.extra_types import CnpjStr, CpfStr, NameStr
from pydantic import BaseModel, EmailStr
from boto3clients import dynamodb_client
from config import OAUTH2_TABLE
router = Router()
dyn = DynamoDBPersistenceLayer(OAUTH2_TABLE, dynamodb_client)
class UserConflictError(ServiceError):
def __init__(self, msg: str | dict):
super().__init__(HTTPStatus.CONFLICT, msg)
class Org(BaseModel):
id: str | None
name: str
cnpj: CnpjStr
@router.get('/register')
def register():
def register(
name: Annotated[NameStr, Body(embed=True)],
email: Annotated[EmailStr, Body(embed=True)],
password: Annotated[str, Body(min_length=6, embed=True)],
cpf: Annotated[CpfStr, Body(embed=True)],
user_id: Annotated[str | None, Body(embed=True)] = None,
org: Annotated[Org | None, Body(embed=True)] = None,
):
if user_id:
...
return {}

View File

@@ -0,0 +1,14 @@
from typing import Annotated
from aws_lambda_powertools.event_handler.api_gateway import Router
from aws_lambda_powertools.event_handler.openapi.params import Body, Path
router = Router()
@router.post('/reset')
def reset(
new_password: Annotated[str, Body(min_length=6, embed=True)],
code: Annotated[str, Path],
):
return {}

View File

@@ -68,6 +68,24 @@ Resources:
Path: /register
Method: POST
ApiId: !Ref HttpApi
Forgot:
Type: HttpApi
Properties:
Path: /forgot
Method: POST
ApiId: !Ref HttpApi
Reset:
Type: HttpApi
Properties:
Path: /reset
Method: POST
ApiId: !Ref HttpApi
Lookup:
Type: HttpApi
Properties:
Path: /lookup
Method: GET
ApiId: !Ref HttpApi
Authorize:
Type: HttpApi
Properties:

View File

@@ -0,0 +1,30 @@
from http import HTTPMethod
from layercake.dynamodb import DynamoDBPersistenceLayer, PartitionKey
from ..conftest import HttpApiProxy, LambdaContext
def test_register(
app,
seeds,
dynamodb_persistence_layer: DynamoDBPersistenceLayer,
http_api_proxy: HttpApiProxy,
lambda_context: LambdaContext,
):
r = app.lambda_handler(
http_api_proxy(
raw_path='/register',
method=HTTPMethod.POST,
body={
'name': '07879819908',
},
),
lambda_context,
)
assert len(r['cookies']) == 1
session = dynamodb_persistence_layer.collection.query(PartitionKey('SESSION'))
# One seesion if created from seeds
assert len(session['items']) == 2

View File

@@ -28,9 +28,7 @@ def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool:
subscription = enrollment_layer.collection.get_items(
TransactKey(enrollment_id)
+ SortKey('METADATA#SUBSCRIPTION_COVERED')
# Post-migration: uncomment the following line
# + SortKey('CANCELED', path_spec='canceled_by', rename_key='canceled_by')
+ SortKey('CANCELED', path_spec='author', rename_key='canceled_by')
+ SortKey('CANCELED', path_spec='canceled_by', rename_key='canceled_by')
)
created_at: datetime = fromisoformat(new_image['created_at']) # type: ignore
@@ -67,7 +65,7 @@ def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool:
'created_at': now_,
}
| pick(('user', 'course', 'enrolled_at'), old_enrollment)
# Add created_by if present
# Add `created_by` if present
| ({'author': canceled_by} if canceled_by else {}),
# Post-migration: uncomment the following line
# | ({'created_by': canceled_by} if canceled_by else {}),

View File

@@ -68,7 +68,7 @@ Resources:
detail-type: [INSERT]
detail:
new_image:
sk: ["METADATA#SUBSCRIPTION_COVERED"]
sk: ['METADATA#SUBSCRIPTION_COVERED']
billing_period:
- exists: false
@@ -92,7 +92,7 @@ Resources:
detail-type: [MODIFY]
detail:
new_image:
sk: ["0"]
sk: ['0']
status: [CANCELED]
subscription_covered: [true]
old_image:
@@ -180,7 +180,7 @@ Resources:
detail-type: [INSERT]
detail:
new_image:
sk: ["0"]
sk: ['0']
cnpj:
- exists: true
# Post-migration: rename `tenant_id` to `org_id`
@@ -207,7 +207,7 @@ Resources:
detail-type: [INSERT]
detail:
new_image:
sk: ["0"]
sk: ['0']
cpf:
- exists: true
user_id:
@@ -233,7 +233,7 @@ Resources:
detail-type: [MODIFY]
detail:
new_image:
sk: ["0"]
sk: ['0']
cnpj:
- exists: true
status: [CANCELED, EXPIRED]
@@ -256,7 +256,7 @@ Resources:
detail-type: [INSERT]
detail:
new_image:
sk: ["0"]
sk: ['0']
cnpj:
- exists: true
total: [0]

76
orders-events/uv.lock generated
View File

@@ -1,5 +1,5 @@
version = 1
revision = 2
revision = 3
requires-python = ">=3.13"
[[package]]
@@ -67,15 +67,15 @@ wheels = [
[[package]]
name = "aws-lambda-powertools"
version = "3.19.0"
version = "3.23.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "jmespath" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/58/db/eb2708f7c27ab02b8d85936ce9308538e1e22c8c8224be5f00da3e6f44f7/aws_lambda_powertools-3.19.0.tar.gz", hash = "sha256:8897ba4be0b3a51f2b8f68946d650f3ef574fa2c40395544de03bd0c61979999", size = 689768, upload-time = "2025-08-12T08:45:46.887Z" }
sdist = { url = "https://files.pythonhosted.org/packages/38/24/78f320a310d98df8c831e15c5f04fec20ba4958253deb165ab2d10d3392b/aws_lambda_powertools-3.23.0.tar.gz", hash = "sha256:30ab45960989dd75a4d84de4f156509458f8782038d532eee2f815488d7cc929", size = 702835, upload-time = "2025-11-13T16:44:23.659Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c6/52/5a73194286af329309263e9c4e2a57b8feac63bb6027be8d2d6222cd4da7/aws_lambda_powertools-3.19.0-py3-none-any.whl", hash = "sha256:98f18d35f843cd46b80ccadcf39eefc0c489325bea116383bd93048a5241d9fc", size = 832645, upload-time = "2025-08-12T08:45:44.982Z" },
{ url = "https://files.pythonhosted.org/packages/70/48/f59597b0acbe3bcd829ae5b13b49a29039c5b2a5a6771f765ad3f3f576a3/aws_lambda_powertools-3.23.0-py3-none-any.whl", hash = "sha256:f3d16f1b0304c686cc956ecf0f6f8907d21992a4a5070e2388c21571d8c84cc2", size = 848256, upload-time = "2025-11-13T16:44:21.459Z" },
]
[package.optional-dependencies]
@@ -576,7 +576,7 @@ wheels = [
[[package]]
name = "layercake"
version = "0.11.0"
version = "0.11.2"
source = { directory = "../layercake" }
dependencies = [
{ name = "arnparse" },
@@ -593,6 +593,7 @@ dependencies = [
{ name = "pycpfcnpj" },
{ name = "pydantic", extra = ["email"] },
{ name = "pydantic-extra-types" },
{ name = "python-calamine" },
{ name = "python-multipart" },
{ name = "pytz" },
{ name = "requests" },
@@ -605,7 +606,7 @@ dependencies = [
requires-dist = [
{ name = "arnparse", specifier = ">=0.0.2" },
{ name = "authlib", specifier = ">=1.6.5" },
{ name = "aws-lambda-powertools", extras = ["all"], specifier = ">=3.18.0" },
{ name = "aws-lambda-powertools", extras = ["all"], specifier = ">=3.23.0" },
{ name = "dictdiffer", specifier = ">=0.9.0" },
{ name = "ftfy", specifier = ">=6.3.1" },
{ name = "glom", specifier = ">=24.11.0" },
@@ -617,6 +618,7 @@ requires-dist = [
{ name = "pycpfcnpj", specifier = ">=1.8" },
{ name = "pydantic", extras = ["email"], specifier = ">=2.10.6" },
{ name = "pydantic-extra-types", specifier = ">=2.10.3" },
{ name = "python-calamine", specifier = ">=0.5.4" },
{ name = "python-multipart", specifier = ">=0.0.20" },
{ name = "pytz", specifier = ">=2025.1" },
{ name = "requests", specifier = ">=2.32.3" },
@@ -1062,6 +1064,68 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/28/d0/def53b4a790cfb21483016430ed828f64830dd981ebe1089971cd10cab25/pytest_cov-6.1.1-py3-none-any.whl", hash = "sha256:bddf29ed2d0ab6f4df17b4c55b0a657287db8684af9c42ea546b21b1041b3dde", size = 23841, upload-time = "2025-04-05T14:07:49.641Z" },
]
[[package]]
name = "python-calamine"
version = "0.6.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/9b/32/99a794a1ca7b654cecdb76d4d61f21658b6f76574321341eb47df4365807/python_calamine-0.6.1.tar.gz", hash = "sha256:5974989919aa0bb55a136c1822d6f8b967d13c0fd0f245e3293abb4e63ab0f4b", size = 138354, upload-time = "2025-11-26T10:48:35.331Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f3/b6/d9b1a6432d33d43ded44ca01dff2c2a41f68a169413bdbe7677fc6598bfc/python_calamine-0.6.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:44dcffccbc3d9d258848d84ed685803ecb196f6b44bff271418283a0d015a6ea", size = 877262, upload-time = "2025-11-26T10:46:49.271Z" },
{ url = "https://files.pythonhosted.org/packages/4d/09/29a113debc6c389065057c9f72e8837760b36ae86a6363a31c18b699adfb/python_calamine-0.6.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:66373ae273ef356a55b53c2348335274b6d25c08d75a399a3f167d93e13aa1b6", size = 854634, upload-time = "2025-11-26T10:46:50.716Z" },
{ url = "https://files.pythonhosted.org/packages/89/c4/0a68314336b8b1d04ae1cda98cc8c191829547d652394f34e5360d9563c9/python_calamine-0.6.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:02482677cea6d3c2a09008469b7f5544d4d8c79af8fc7d49edcc669cfc75f640", size = 927779, upload-time = "2025-11-26T10:46:52.146Z" },
{ url = "https://files.pythonhosted.org/packages/29/ab/ce23029f808e31e12fe9ca26b038b67c8f065b9c666a1e73aacaa086d177/python_calamine-0.6.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6794c55fa3d3dc88deda7377fc721b6506d186ec149e04b38109b1f58cc0b61f", size = 912282, upload-time = "2025-11-26T10:46:53.875Z" },
{ url = "https://files.pythonhosted.org/packages/90/d9/e4bfad521a92ebb330f16a0ab7ad57da35ded14d90e9e395e97aacd63bef/python_calamine-0.6.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:79c33a5384221d8ab7d4b91b83374317b403ef945b5aa18f6e6ea6cbba661393", size = 1071785, upload-time = "2025-11-26T10:46:55.735Z" },
{ url = "https://files.pythonhosted.org/packages/ee/e8/18894883669644da9d14f8c6db0db00b793eaac3cd7268bcafb4a73b9837/python_calamine-0.6.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0e36211a7feaa56d12e8ea1ddeeae6c4887764c351c275b034c07c9b7d66455e", size = 964443, upload-time = "2025-11-26T10:46:57.208Z" },
{ url = "https://files.pythonhosted.org/packages/0c/0d/7482fcded940d1adc4c8eaf47488a69ef1e3fd86eb8c6d33a981ddf5f82a/python_calamine-0.6.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7c3e6aeedeb289311907f8d59b2a32a404433d1af4dfce0ba4e3badd30f9775d", size = 932682, upload-time = "2025-11-26T10:46:59.006Z" },
{ url = "https://files.pythonhosted.org/packages/ee/88/4898de6ce811c936168b48c92d310bba0e8f4ab6e56059b537d9d6d72c05/python_calamine-0.6.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a2aa2989e5231cda2a15d21fd6e7cf3fc4ce09535756bdb7b2f32197fd6a566a", size = 975624, upload-time = "2025-11-26T10:47:00.844Z" },
{ url = "https://files.pythonhosted.org/packages/10/1e/85ef4693452cc21cb912e32e33c8aa4add399b3fb0c1af8036692fd33f61/python_calamine-0.6.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:e5761dc896446d6e9dd40c7e781908c1ae919d31bdd00b5dedc033525f440dec", size = 1110373, upload-time = "2025-11-26T10:47:02.483Z" },
{ url = "https://files.pythonhosted.org/packages/2f/18/67aaa61c4bea9fd99ed44ff50e93fac70096b992275bae3552f98f6a1229/python_calamine-0.6.1-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:d1118d9d4f626f62663bfd5c83de07bc8455463081de6bc3b4264414e81a56a9", size = 1179486, upload-time = "2025-11-26T10:47:04.067Z" },
{ url = "https://files.pythonhosted.org/packages/db/f5/73baef823b41f7b50a86ddb36d1ea2c19882414568aaa2d8ed7afb96dc71/python_calamine-0.6.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:7e5500a8769bdf0efaef10bcce2613d5240823891172d1a943b776f18977c2f1", size = 1108067, upload-time = "2025-11-26T10:47:05.873Z" },
{ url = "https://files.pythonhosted.org/packages/aa/f2/db7fc4d14ff0bf8a8bf3ee43daad2e63fc2f46605e5972d97543e0f95e62/python_calamine-0.6.1-cp313-cp313-win32.whl", hash = "sha256:ec7928740519a8471ad8f1ec429301fb8a31a9c6adbfea51d7ff6ef2cb116835", size = 695391, upload-time = "2025-11-26T10:47:07.254Z" },
{ url = "https://files.pythonhosted.org/packages/1d/c9/2e6b5d073885051ee7b5947156678c0cf5dfedf0dd10c5f23b694dcef824/python_calamine-0.6.1-cp313-cp313-win_amd64.whl", hash = "sha256:8f24740645a773cefae8507a13d03981867fa3dbd7fad1c3c667a1a3cd43235b", size = 747094, upload-time = "2025-11-26T10:47:08.69Z" },
{ url = "https://files.pythonhosted.org/packages/f2/c4/8ff9ecfe3b9b2bf556474e8ee8de541edfd650fd3e77752fa5705cbee3dc/python_calamine-0.6.1-cp313-cp313-win_arm64.whl", hash = "sha256:8e4ac2732aadc98bee412b59770dc6f4a6a886b5308cb57bfea53e877ae1a913", size = 716857, upload-time = "2025-11-26T10:47:11.062Z" },
{ url = "https://files.pythonhosted.org/packages/ec/0d/83e44b3cbc7712ffac7750b14a817e34637904bcaa435626799506bf998b/python_calamine-0.6.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:caab3bafa99b62d0aed0abf261a9f9df045eef11c5410ed91aa1b25f8381a087", size = 873582, upload-time = "2025-11-26T10:47:12.463Z" },
{ url = "https://files.pythonhosted.org/packages/1f/7e/b47cfe737f885b139dae63f4139cb2ed1515994b465cf0370e25ce8d0065/python_calamine-0.6.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3aefcdea5bdd2972e999264435b97e71855f02481688d213a4473d372b8288b0", size = 850739, upload-time = "2025-11-26T10:47:13.989Z" },
{ url = "https://files.pythonhosted.org/packages/9a/ea/6aa2f277271323c5fbbde8718a7cad5ecf1fed9f637f648b0f6ae2c240cd/python_calamine-0.6.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2e9d10c91308eacfc1f76ff08bb7a8316c61f8f47619f9e4e254dd888fb3e9b", size = 923053, upload-time = "2025-11-26T10:47:15.671Z" },
{ url = "https://files.pythonhosted.org/packages/00/2a/bf6ff24816fa60646d61a00f8a69113239a6a97207cdb2d541936003d030/python_calamine-0.6.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:71eb5069b3d3639594a4fdccb3cb95a1b8f650e12def39a752ad8ff19eea620f", size = 907953, upload-time = "2025-11-26T10:47:17.535Z" },
{ url = "https://files.pythonhosted.org/packages/c1/24/54bb664dc9cc1252207bf5512d9870be23fdba2e5b94300d7e32e8c39a82/python_calamine-0.6.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:def9e6be95496c660b6dc55b37eac3c6a479a71522e849f3a1ed4435788c6599", size = 1071663, upload-time = "2025-11-26T10:47:18.967Z" },
{ url = "https://files.pythonhosted.org/packages/c0/b7/4e2e5c8fd00ee7d80d272cb5e3cf170615a99911b515a2b4347995df0aa8/python_calamine-0.6.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c4740797c7e794acd907c7fa84ec09931ed2dfc3c9d1c689f7c7d236498d74cc", size = 961235, upload-time = "2025-11-26T10:47:21.117Z" },
{ url = "https://files.pythonhosted.org/packages/b8/61/25193d600bf0e48513d275a69e5cdb158c27d11573bed74a28eb88d88592/python_calamine-0.6.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b67f1a9f7452fa6ee736ac5a59349bbfc66087b96402051656c9b5a54a111ef", size = 930561, upload-time = "2025-11-26T10:47:22.904Z" },
{ url = "https://files.pythonhosted.org/packages/dc/3d/b0f434622c31182b64bd2e0e6c81cf35cf240ccee38cfb8074fbde9add98/python_calamine-0.6.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1f369ebb8d6bf2ac66fbe38f5e6adf7b6a81fa71c1b5e2e7b2bb4a5c9667711", size = 971200, upload-time = "2025-11-26T10:47:24.837Z" },
{ url = "https://files.pythonhosted.org/packages/39/8e/502bbb06fa70f1f52f4f46efc0b331b31124110986a5378c1be711ad05e9/python_calamine-0.6.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:99bf12605466097219ebb133df54e41e479cb2559359d2dbad624dc301d4286b", size = 1106302, upload-time = "2025-11-26T10:47:26.706Z" },
{ url = "https://files.pythonhosted.org/packages/c7/63/6fbda3f58aa5907cdfb628fc96e26e10820000858a9dd4fe6053e05a9310/python_calamine-0.6.1-cp313-cp313t-musllinux_1_1_armv7l.whl", hash = "sha256:96a44d48b9c4b05fb70396674ca7c90e4b4286845b5937606b60babe90f1fa4c", size = 1174437, upload-time = "2025-11-26T10:47:28.229Z" },
{ url = "https://files.pythonhosted.org/packages/07/e0/9e027e79de13424844ab33b6e2ad2b2be9ac40b653040bc8459bbfe4b48f/python_calamine-0.6.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:f7bfaf556391841ea59d0d0a63c5af7b5285ab260103656e65f55384b31b2010", size = 1105843, upload-time = "2025-11-26T10:47:29.848Z" },
{ url = "https://files.pythonhosted.org/packages/cd/80/231c1f02d3d5adfde8c1f324da2c7907b63adb6f9ef36c3fd7db5b5fe083/python_calamine-0.6.1-cp313-cp313t-win_amd64.whl", hash = "sha256:a46ff8aa52ea3ef7851d6b5fd496be72a10db4f2d8942b42ecb0634ff9c1e441", size = 746797, upload-time = "2025-11-26T10:47:31.333Z" },
{ url = "https://files.pythonhosted.org/packages/88/2d/8c18519847dd53227c472231bcca37086027dd54b40ae13c48da7bacea53/python_calamine-0.6.1-cp313-cp313t-win_arm64.whl", hash = "sha256:7ac72743c3b2398ed55b9130482db097da8cb80d61b4b7aaf4008c7831ac11d3", size = 711966, upload-time = "2025-11-26T10:47:32.995Z" },
{ url = "https://files.pythonhosted.org/packages/66/89/974515fe4e871fc8ff2495ebd1a59585fe56956b83096bd8f17c76716951/python_calamine-0.6.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:957412de027ef6c05da0ad687c7a5111229108c1c81780a94ea64ca6afa10074", size = 874587, upload-time = "2025-11-26T10:47:34.823Z" },
{ url = "https://files.pythonhosted.org/packages/9f/1c/185a871429bcd19a00d0df8a5f5a6469dfd5d5e86039d43df6d98b913cd1/python_calamine-0.6.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:5393b60b81e2c7d6f54b26bca8fb47c032bc35531ea3bb38ae5ffdefd6ba2b6d", size = 851804, upload-time = "2025-11-26T10:47:36.809Z" },
{ url = "https://files.pythonhosted.org/packages/16/f0/a1b18653d621efac176ae63b3b4b4fdcf2b9d8706ffec75b0d4dbf02c1d2/python_calamine-0.6.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:efdf70f647fe51638f4a2d0efb0644f132eb2bc32b0268f2c8477e23d56302f4", size = 925164, upload-time = "2025-11-26T10:47:38.622Z" },
{ url = "https://files.pythonhosted.org/packages/e4/4e/1ad2bcea9bbd9e5eed89626391d63759c800cd9064e13dd8f17d9084ddbf/python_calamine-0.6.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8bf893d3526af30d0e4152de54621cf440d5d9fe99882adac02803a9f870da76", size = 908880, upload-time = "2025-11-26T10:47:40.239Z" },
{ url = "https://files.pythonhosted.org/packages/e5/bb/bd5fe13c89f2e39f439f6f3535f34c3d29fb5280fa7e6a6b9f101547a1eb/python_calamine-0.6.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2905f241beff9945b1c4a3622ddc9cf604b1825a26683b35a8f97533c983b228", size = 1077935, upload-time = "2025-11-26T10:47:41.738Z" },
{ url = "https://files.pythonhosted.org/packages/98/8d/fde8575220ecbbf1a3a3eeb6c9fd96288bfadf1eb9fca4eb89ebfb81ce8e/python_calamine-0.6.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39a722be084690516e0bf6260cc452cf783ef72f01a18c0d1daf428dc88cf090", size = 961729, upload-time = "2025-11-26T10:47:43.238Z" },
{ url = "https://files.pythonhosted.org/packages/a7/75/d6da93f82e07359710bb472822e4e4f964bc712a16a86b009f97679ea0c0/python_calamine-0.6.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33e9180c7018ecaf5d8648b6a9c54381d467bf622dccc5d8fa90ae727b21ca46", size = 931109, upload-time = "2025-11-26T10:47:44.855Z" },
{ url = "https://files.pythonhosted.org/packages/58/79/abdacdf1ffec109ebb52eae3edbb110de3350d54c2a6232e3d88acabc8ec/python_calamine-0.6.1-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:18d00677bd9f2fad3924d1101d95ac0057f98ebde406034d5782c1f14d4f6c64", size = 972567, upload-time = "2025-11-26T10:47:46.424Z" },
{ url = "https://files.pythonhosted.org/packages/56/36/b7aa35eab36515216759be0fa2f6702ec1ac20168f239d220a0027c3c2f4/python_calamine-0.6.1-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:df8c6bdfc6eefbeae35a8f9fdfbf85d954f944b9c8aea8e43e1cdde1d50eb686", size = 1108588, upload-time = "2025-11-26T10:47:48.019Z" },
{ url = "https://files.pythonhosted.org/packages/19/d1/33c947f2541006f6d196bf7b9f1d5211592c36398027381b27c69dea8a6f/python_calamine-0.6.1-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:96107062d9e5f696e5b15b4c67b40acc136607bc880c2368797051e26478bd9e", size = 1175173, upload-time = "2025-11-26T10:47:49.631Z" },
{ url = "https://files.pythonhosted.org/packages/cf/84/46ca9e32572ea0c8ba0fbe489c7a15dc0af0d266331e3e0ae44a7d841767/python_calamine-0.6.1-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:c3d9f2f5f5712dc5c59707a1211781339738b9ede7611c049995327e26e99f6d", size = 1107963, upload-time = "2025-11-26T10:47:51.638Z" },
{ url = "https://files.pythonhosted.org/packages/6e/d7/043fbe723313ab52d3e7f81465287d507a3237d442ac913ed168172dc9f2/python_calamine-0.6.1-cp314-cp314-win32.whl", hash = "sha256:46563dd5424a7e0e6d8845bf4263455364749517493690a7af8c98c7803d7348", size = 694668, upload-time = "2025-11-26T10:47:54.028Z" },
{ url = "https://files.pythonhosted.org/packages/e9/93/5690f52c267dbcde420a2db0e39158eb78ae85083137db2bda3387232116/python_calamine-0.6.1-cp314-cp314-win_amd64.whl", hash = "sha256:8fdff080b3c46527d90f8d8c593400d39f02c126bd4ed477b845603f86524b52", size = 744792, upload-time = "2025-11-26T10:47:55.488Z" },
{ url = "https://files.pythonhosted.org/packages/30/4b/360c6cfd78bee2707d1f294bd74ecb2662abfc9ee9786a373869403c5737/python_calamine-0.6.1-cp314-cp314-win_arm64.whl", hash = "sha256:d8d7a18a2385d7302f4d82ff2789765e725afa95339f35e33b27d43ef7914e91", size = 714327, upload-time = "2025-11-26T10:47:57.035Z" },
{ url = "https://files.pythonhosted.org/packages/18/26/d0f619823b511606490359d8b7f2090f17233373eac5fd9ad7bb5bab01a8/python_calamine-0.6.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:c863c5f447fab38d72f272ab388e9e38552a1e034446c97a358008397d290fca", size = 874069, upload-time = "2025-11-26T10:47:58.686Z" },
{ url = "https://files.pythonhosted.org/packages/f6/76/a0687797d3ee024611fb4ba9e3d658742bcfed10ab979c6ba8cb7028c225/python_calamine-0.6.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a20b752042ab833724d4118ae107072b9b575142dc7e9c142989c3613c0b7094", size = 852456, upload-time = "2025-11-26T10:48:00.325Z" },
{ url = "https://files.pythonhosted.org/packages/01/09/6ebea8e51791fb2fe6d9651f0de54adae20fdb7eb9b9654897c855b7a939/python_calamine-0.6.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:350b02f2101132e9faf04784849370eabfc4d65b070fe76f07cbe46deee67850", size = 923253, upload-time = "2025-11-26T10:48:01.894Z" },
{ url = "https://files.pythonhosted.org/packages/54/63/a32eaca9cb65608109ec393a2ebcef5e9fad7c6cfc7b464a5f6cf1b595ba/python_calamine-0.6.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ec23faed1922a1e1c966fe1f09a573de4921303b97304bda82f5d764c55f905b", size = 909063, upload-time = "2025-11-26T10:48:03.759Z" },
{ url = "https://files.pythonhosted.org/packages/90/cc/64a81e3ebd0d8fe79b2120f748db7dcd733abe11a9d97d00921ab60c02c4/python_calamine-0.6.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:acd14ea56bf194d6da8103d5b3c16fcafed666843d3ad4ae77d1efbb04912de5", size = 1070734, upload-time = "2025-11-26T10:48:05.362Z" },
{ url = "https://files.pythonhosted.org/packages/c4/a9/04c29089240763f559ab69be6794fe4209acf16306c051fe0fc4afb40f8a/python_calamine-0.6.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e06444e75411a7a5cff3ee5b4c7f831897b549cc720b9a66740be1045980e634", size = 960622, upload-time = "2025-11-26T10:48:06.935Z" },
{ url = "https://files.pythonhosted.org/packages/19/3e/9659b179b9e28b7895f32d0b0f0a09474b263fe001abaf1009b51b1b7b9c/python_calamine-0.6.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acb4e4277b94d3e07d6045de2b2b1995cd093399f54dacc441acdb86ec4e6a4f", size = 929758, upload-time = "2025-11-26T10:48:08.56Z" },
{ url = "https://files.pythonhosted.org/packages/45/43/4cb1603b1452ecb3b1a34863b193fce54dc2b048b961a51652d2116a5998/python_calamine-0.6.1-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1f722f72abb43fc2eabf2e74472ec2a30a6fbcf90836927da430d36a0fe26c83", size = 971930, upload-time = "2025-11-26T10:48:10.212Z" },
{ url = "https://files.pythonhosted.org/packages/80/d8/939fb61b1a085a8f96a2e3e86872c23f23377070dc582ba0d1066cbc973b/python_calamine-0.6.1-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:ac3e464ab5df1ef1a2eff0932a2c5431a35c41b4c7dd8030fd76b4abba53a11c", size = 1106265, upload-time = "2025-11-26T10:48:12.107Z" },
{ url = "https://files.pythonhosted.org/packages/7d/d8/22103aab600f89ab99d8b9538e92b37f4e6e520a8caceb73e421cb6b996b/python_calamine-0.6.1-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:ee671cb13e1e68f4669e85ca8cc365dcc62a1a023d288c1b3feeab98512a63f5", size = 1175335, upload-time = "2025-11-26T10:48:13.655Z" },
{ url = "https://files.pythonhosted.org/packages/69/cf/950bf18c38964f84639fe530162c40aea23f1473eeb78668096211984e56/python_calamine-0.6.1-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:3019d81aea47e8fea6c08a2c5310faeef1d3119e2b11409f1aae86b4dc5aaff3", size = 1104826, upload-time = "2025-11-26T10:48:15.41Z" },
{ url = "https://files.pythonhosted.org/packages/0c/37/ea8e77509b9ca8ea1e70f4660b854e4d38b84c76aba4ee7c973423a613ba/python_calamine-0.6.1-cp314-cp314t-win_amd64.whl", hash = "sha256:89d11e9022bc1aec124d5a5bc5a34e703a6b7e22171558231e05c84ac56ec79b", size = 745873, upload-time = "2025-11-26T10:48:17.028Z" },
{ url = "https://files.pythonhosted.org/packages/f4/99/6a2be914635f50ccd9296fcb39f7566f354d28ca20acc93085ce610e9d23/python_calamine-0.6.1-cp314-cp314t-win_arm64.whl", hash = "sha256:a57ad2e1feb443ef0b197b7717200f786c3e3a3412bf88a9bfef0792ab848f58", size = 711796, upload-time = "2025-11-26T10:48:18.57Z" },
]
[[package]]
name = "python-dateutil"
version = "2.9.0.post0"