add lookup
This commit is contained in:
@@ -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({
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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')
|
||||
]),
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
97
apps/id.saladeaula.digital/app/routes/register/cpf.tsx
Normal file
97
apps/id.saladeaula.digital/app/routes/register/cpf.tsx
Normal 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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
39
apps/id.saladeaula.digital/app/routes/register/data.ts
Normal file
39
apps/id.saladeaula.digital/app/routes/register/data.ts
Normal 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)
|
||||
@@ -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">
|
||||
Já 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>
|
||||
)
|
||||
}
|
||||
50
apps/id.saladeaula.digital/app/routes/register/layout.tsx
Normal file
50
apps/id.saladeaula.digital/app/routes/register/layout.tsx
Normal 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">
|
||||
Já 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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -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 =
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
12
id.saladeaula.digital/app/routes/forgot.py
Normal file
12
id.saladeaula.digital/app/routes/forgot.py
Normal 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 {}
|
||||
49
id.saladeaula.digital/app/routes/lookup.py
Normal file
49
id.saladeaula.digital/app/routes/lookup.py
Normal 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
|
||||
@@ -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 {}
|
||||
|
||||
14
id.saladeaula.digital/app/routes/reset.py
Normal file
14
id.saladeaula.digital/app/routes/reset.py
Normal 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 {}
|
||||
@@ -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:
|
||||
|
||||
30
id.saladeaula.digital/tests/routes/test_register.py
Normal file
30
id.saladeaula.digital/tests/routes/test_register.py
Normal 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
|
||||
@@ -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 {}),
|
||||
|
||||
@@ -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
76
orders-events/uv.lock
generated
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user