diff --git a/api.saladeaula.digital/app/routes/orgs/users/batch_jobs.py b/api.saladeaula.digital/app/routes/orgs/users/batch_jobs.py new file mode 100644 index 0000000..80e1331 --- /dev/null +++ b/api.saladeaula.digital/app/routes/orgs/users/batch_jobs.py @@ -0,0 +1,14 @@ +from aws_lambda_powertools import Logger +from aws_lambda_powertools.event_handler.api_gateway import Router +from layercake.dynamodb import DynamoDBPersistenceLayer + +from boto3clients import dynamodb_client +from config import USER_TABLE + +logger = Logger(__name__) +router = Router() +dyn = DynamoDBPersistenceLayer(USER_TABLE, dynamodb_client) + + +@router.post('//users/batch-jobs') +def batch_jobs(org_id: str): ... diff --git a/apps/admin.saladeaula.digital/app/routes/_.$orgid.courses._index/route.tsx b/apps/admin.saladeaula.digital/app/routes/_.$orgid.courses._index/route.tsx index dffdfb5..bbebe54 100644 --- a/apps/admin.saladeaula.digital/app/routes/_.$orgid.courses._index/route.tsx +++ b/apps/admin.saladeaula.digital/app/routes/_.$orgid.courses._index/route.tsx @@ -206,14 +206,17 @@ function Course({
  • {currency.format(metadata__unit_price)} {custom_pricing && ( - {currency.format(custom_pricing)} + + {currency.format(custom_pricing)} + )}
  • diff --git a/apps/admin.saladeaula.digital/app/routes/_.$orgid.enrollments._index/columns.tsx b/apps/admin.saladeaula.digital/app/routes/_.$orgid.enrollments._index/columns.tsx index c24aefc..537d487 100644 --- a/apps/admin.saladeaula.digital/app/routes/_.$orgid.enrollments._index/columns.tsx +++ b/apps/admin.saladeaula.digital/app/routes/_.$orgid.enrollments._index/columns.tsx @@ -1,6 +1,6 @@ 'use client' -import type { CellContext, ColumnDef } from '@tanstack/react-table' +import type { ColumnDef } from '@tanstack/react-table' import { useRequest, useToggle } from 'ahooks' import { CircleXIcon, @@ -13,7 +13,10 @@ import type { ComponentProps, MouseEvent } from 'react' import { toast } from 'sonner' import { Abbr } from '@repo/ui/components/abbr' -import { DataTableColumnHeader } from '@repo/ui/components/data-table' +import { + DataTableColumnDatetime, + DataTableColumnHeader +} from '@repo/ui/components/data-table' import { AlertDialog, AlertDialogAction, @@ -121,7 +124,6 @@ export const columns: ColumnDef[] = [ enableHiding: false, cell: ({ row }) => { const { name } = row.getValue('course') as { name: string } - return {name} } }, @@ -159,43 +161,53 @@ export const columns: ColumnDef[] = [ }, { accessorKey: 'created_at', - header: ({ column }) => , meta: { title: 'Cadastrado em' }, enableSorting: true, enableHiding: true, - cell: cellDate + header: ({ column }) => , + cell: ({ row, column }) => ( + + ) }, { accessorKey: 'started_at', - header: ({ column }) => , meta: { title: 'Iniciado em' }, enableSorting: true, enableHiding: true, - cell: cellDate + header: ({ column }) => , + cell: ({ row, column }) => ( + + ) }, { accessorKey: 'completed_at', - header: ({ column }) => , meta: { title: 'Concluído em' }, enableSorting: true, enableHiding: true, - cell: cellDate + header: ({ column }) => , + cell: ({ row, column }) => ( + + ) }, { accessorKey: 'failed_at', - header: ({ column }) => , meta: { title: 'Reprovado em' }, enableSorting: true, enableHiding: true, - cell: cellDate + header: ({ column }) => , + cell: ({ row, column }) => ( + + ) }, { accessorKey: 'canceled_at', - header: ({ column }) => , meta: { title: 'Cancelado em' }, enableSorting: true, enableHiding: true, - cell: cellDate + header: ({ column }) => , + cell: ({ row, column }) => ( + + ) }, { id: 'actions', @@ -203,20 +215,6 @@ export const columns: ColumnDef[] = [ } ] -function cellDate({ - row: { original }, - cell: { column } -}: CellContext) { - const accessorKey = column.columnDef.accessorKey as keyof TData - const value = original?.[accessorKey] - - if (value) { - return formatted.format(new Date(value as string)) - } - - return <> -} - async function getEnrollment(id: string) { const r = await fetch(`/~/api/enrollments/${id}`, { method: 'GET' diff --git a/apps/admin.saladeaula.digital/app/routes/_.$orgid.orders._index/columns.tsx b/apps/admin.saladeaula.digital/app/routes/_.$orgid.orders._index/columns.tsx index a4905ee..8aaa089 100644 --- a/apps/admin.saladeaula.digital/app/routes/_.$orgid.orders._index/columns.tsx +++ b/apps/admin.saladeaula.digital/app/routes/_.$orgid.orders._index/columns.tsx @@ -1,5 +1,10 @@ 'use client' +import { + DataTableColumnDatetime, + DataTableColumnCurrency, + DataTableColumnHeader +} from '@repo/ui/components/data-table' import { type ColumnDef } from '@tanstack/react-table' // This type is used to define the shape of our data. @@ -12,14 +17,6 @@ export type Order = { name: string } -const formatted = new Intl.DateTimeFormat('pt-BR', { - day: '2-digit', - month: '2-digit', - year: 'numeric', - hour: '2-digit', - minute: '2-digit' -}) - export const columns: ColumnDef[] = [ { accessorKey: 'payment_method', @@ -32,43 +29,35 @@ export const columns: ColumnDef[] = [ { accessorKey: 'total', header: 'Valor total', - cell: ({ row }) => { - const amount = parseFloat(row.getValue('total')) - const formatted = new Intl.NumberFormat('pt-BR', { - style: 'currency', - currency: 'BRL' - }).format(amount) - - return
    {formatted}
    - } + cell: ({ row, column }) => ( + + ) }, { - header: 'Comprado em', - cell: ({ row }) => { - const createdAt = new Date(row.original.create_date) - return formatted.format(createdAt) - } + accessorKey: 'create_date', + enableSorting: true, + meta: { title: 'Comprado em' }, + header: ({ column }) => , + cell: ({ row, column }) => ( + + ) }, { - header: 'Vencimento em', - cell: ({ row }) => { - try { - const dueDate = new Date(row.original.due_date) - return formatted.format(dueDate) - } catch { - return 'N/A' - } - } + accessorKey: 'due_date', + enableSorting: true, + meta: { title: 'Vencimento em' }, + header: ({ column }) => , + cell: ({ row, column }) => ( + + ) }, { - header: 'Pago em', - cell: ({ row }) => { - if (row.original.payment_date) { - const createdAt = new Date(row.original.payment_date) - return formatted.format(createdAt) - } - - return <> - } + accessorKey: 'payment_date', + enableSorting: true, + meta: { title: 'Pago em' }, + header: ({ column }) => , + cell: ({ row, column }) => ( + + ) } ] diff --git a/apps/admin.saladeaula.digital/app/routes/_.$orgid.orders._index/route.tsx b/apps/admin.saladeaula.digital/app/routes/_.$orgid.orders._index/route.tsx index d6c119d..59321b7 100644 --- a/apps/admin.saladeaula.digital/app/routes/_.$orgid.orders._index/route.tsx +++ b/apps/admin.saladeaula.digital/app/routes/_.$orgid.orders._index/route.tsx @@ -2,6 +2,7 @@ import type { Route } from './+types/route' import { Suspense } from 'react' import { Await } from 'react-router' +import { MeiliSearchFilterBuilder } from 'meilisearch-helper' import { DataTable } from '@repo/ui/components/data-table' import { Skeleton } from '@repo/ui/components/skeleton' @@ -16,18 +17,25 @@ export function meta({}: Route.MetaArgs) { export async function loader({ params, context, request }: Route.LoaderArgs) { const { searchParams } = new URL(request.url) const { orgid } = params + const query = searchParams.get('q') || '' + const sort = searchParams.get('sort') || 'create_date:desc' const page = Number(searchParams.get('p')) + 1 const hitsPerPage = Number(searchParams.get('perPage')) || 25 + let builder = new MeiliSearchFilterBuilder().where('tenant_id', '=', orgid) + + const orders = createSearch({ + index: 'betaeducacao-prod-orders', + filter: builder.build(), + sort: [sort], + query, + page, + hitsPerPage, + env: context.cloudflare.env + }) + return { - data: createSearch({ - index: 'betaeducacao-prod-orders', - sort: ['create_date:desc'], - filter: `tenant_id = ${orgid}`, - page, - hitsPerPage, - env: context.cloudflare.env - }) + data: orders } } @@ -48,7 +56,7 @@ export default function Route({ loaderData: { data } }) { {({ hits, page, hitsPerPage, totalHits }) => { return ( [] = [ + { + id: 'select', + header: ({ table }) => ( + table.toggleAllPageRowsSelected(!!value)} + className="cursor-pointer" + aria-label="Selecionar tudo" + /> + ), + cell: ({ row }) => ( + row.toggleSelected(!!value)} + className="cursor-pointer" + aria-label="Selecionar linha" + /> + ) + }, { header: 'Colaborador', cell: ({ row }) => { @@ -78,38 +99,29 @@ export const columns: ColumnDef[] = [ } }, { + accessorKey: 'cpf', header: 'CPF', - cell: ({ row }) => { - const { cpf } = row.original - - if (cpf) { - return <>{formatCPF(cpf)} - } - - return <> - } + cell: ({ row, column }) => ( + + ) }, { - header: 'Último accesso', - cell: ({ row }) => { - // Post-migration: rename `lastLogin` to `last_login` - if (row.original?.lastLogin) { - const lastLogin = new Date(row.original.lastLogin) - return formatted.format(lastLogin) - } - - return <> - } + accessorKey: 'createDate', + enableSorting: true, + meta: { title: 'Cadastrado em' }, + header: ({ column }) => , + cell: ({ row, column }) => ( + + ) }, { - header: 'Cadastrado em', - meta: { - className: 'w-1/12' - }, - cell: ({ row }) => { - const created_at = new Date(row.original.createDate) - return formatted.format(created_at) - } + accessorKey: 'lastLogin', + enableSorting: true, + meta: { title: 'Último acesso' }, + header: ({ column }) => , + cell: ({ row, column }) => ( + + ) }, { id: 'actions', diff --git a/apps/admin.saladeaula.digital/app/routes/_.$orgid.users._index/route.tsx b/apps/admin.saladeaula.digital/app/routes/_.$orgid.users._index/route.tsx index 1448418..2935df0 100644 --- a/apps/admin.saladeaula.digital/app/routes/_.$orgid.users._index/route.tsx +++ b/apps/admin.saladeaula.digital/app/routes/_.$orgid.users._index/route.tsx @@ -3,6 +3,7 @@ import type { Route } from './+types/route' import { PlusIcon } from 'lucide-react' import { Suspense } from 'react' import { Await, Link, useSearchParams } from 'react-router' +import { MeiliSearchFilterBuilder } from 'meilisearch-helper' import { DataTable } from '@repo/ui/components/data-table' import { SearchForm } from '@repo/ui/components/search-form' @@ -27,13 +28,16 @@ export async function loader({ params, context, request }: Route.LoaderArgs) { const { searchParams } = new URL(request.url) const { orgid } = params const query = searchParams.get('q') || '' + const sort = searchParams.get('sort') || 'createDate:desc' const page = Number(searchParams.get('p')) + 1 const hitsPerPage = Number(searchParams.get('perPage')) || 25 + let builder = new MeiliSearchFilterBuilder().where('tenant_id', '=', orgid) + const users = createSearch({ index: 'betaeducacao-prod-users_d2o3r5gmm4it7j', - sort: ['createDate:desc', 'create_date:desc'], - filter: `tenant_id = ${orgid}`, + filter: builder.build(), + sort: [sort], query, page, hitsPerPage, @@ -61,7 +65,7 @@ export default function Route({ loaderData: { data } }) { {({ hits, page, hitsPerPage, totalHits }) => { return ( str | dict | set | list: match data: case datetime(): return data.isoformat() + + case date(): + return data.isoformat() + case UUID(): return str(data) + case IPv4Address(): return str(data) + case tuple() | list(): + if not data: + return [] + serialized = [_serialize_to_basic_types(v) for v in data] - if any(isinstance(v, dict) for v in serialized): + if any(isinstance(v, (dict, list)) for v in serialized): return serialized - return set(serialized) + try: + return set(serialized) + except TypeError: + return serialized + + case set(): + if not data: + return [] + + return set(_serialize_to_basic_types(v) for v in data) + case dict(): - return {k: _serialize_to_basic_types(v) for k, v in data.items()} + return { + k: _serialize_to_basic_types(v) + for k, v in data.items() + if v is not None + } + case _: return data def serialize(data: dict) -> dict: return { - k: serializer.serialize(_serialize_to_basic_types(v)) for k, v in data.items() + k: serializer.serialize(_serialize_to_basic_types(v)) + for k, v in data.items() + if v is not None } diff --git a/layercake/pyproject.toml b/layercake/pyproject.toml index 899d82a..d41d962 100644 --- a/layercake/pyproject.toml +++ b/layercake/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "layercake" -version = "0.11.1" +version = "0.11.2" description = "Packages shared dependencies to optimize deployment and ensure consistency across functions." readme = "README.md" authors = [ @@ -28,6 +28,7 @@ dependencies = [ "joserfc>=1.2.2", "python-multipart>=0.0.20", "authlib>=1.6.5", + "python-calamine>=0.5.4", ] [dependency-groups] diff --git a/layercake/tests/test_dynamodb.py b/layercake/tests/test_dynamodb.py index ce13cc4..2d52834 100644 --- a/layercake/tests/test_dynamodb.py +++ b/layercake/tests/test_dynamodb.py @@ -1,6 +1,7 @@ from datetime import datetime from decimal import Decimal from ipaddress import IPv4Address +from uuid import UUID import pytest from layercake.dateutils import ttl @@ -68,6 +69,22 @@ def test_serialize(): } +def test_serialize_uuid(): + uuid = UUID('12345678-1234-5678-1234-567812345678') + assert serialize({'id': uuid}) == { + 'id': {'S': '12345678-1234-5678-1234-567812345678'} + } + + +def test_serialize_pairs(): + pairs = [(1, 2), (3, 4), (5, 6)] + expected = serialize({'pairs': pairs}) + + assert expected == { + 'pairs': {'L': [{'NS': ['1', '2']}, {'NS': ['3', '4']}, {'NS': ['5', '6']}]} + } + + def test_composekey(): key = ComposeKey(('122', 'abc'), prefix='schedules', delimiter=':') assert key == 'schedules:122:abc' diff --git a/layercake/uv.lock b/layercake/uv.lock index de6a492..d23bda4 100644 --- a/layercake/uv.lock +++ b/layercake/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 2 +revision = 3 requires-python = ">=3.12" resolution-markers = [ "python_full_version >= '3.14'", @@ -675,7 +675,7 @@ wheels = [ [[package]] name = "layercake" -version = "0.10.1" +version = "0.11.2" source = { editable = "." } dependencies = [ { name = "arnparse" }, @@ -692,6 +692,7 @@ dependencies = [ { name = "pycpfcnpj" }, { name = "pydantic", extra = ["email"] }, { name = "pydantic-extra-types" }, + { name = "python-calamine" }, { name = "python-multipart" }, { name = "pytz" }, { name = "requests" }, @@ -727,6 +728,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" }, @@ -1274,6 +1276,81 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/bc/16/4ea354101abb1287856baa4af2732be351c7bee728065aed451b678153fd/pytest_cov-6.2.1-py3-none-any.whl", hash = "sha256:f5bc4c23f42f1cdd23c70b1dab1bbaef4fc505ba950d53e0081d0730dd7e86d5", size = 24644, upload-time = "2025-06-12T10:47:45.932Z" }, ] +[[package]] +name = "python-calamine" +version = "0.5.4" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ff/60/b1ace7a0fd636581b3bb27f1011cb7b2fe4d507b58401c4d328cfcb5c849/python_calamine-0.5.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:4d711f91283d28f19feb111ed666764de69e6d2a0201df8f84e81a238f68d193", size = 850087, upload-time = "2025-10-21T07:11:17.002Z" }, + { url = "https://files.pythonhosted.org/packages/7f/32/32ca71ce50f9b7c7d6e7ec5fcc579a97ddd8b8ce314fe143ba2a19441dc7/python_calamine-0.5.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ed67afd3adedb5bcfb428cf1f2d7dfd936dea9fe979ab631194495ab092973ba", size = 825659, upload-time = "2025-10-21T07:11:18.248Z" }, + { url = "https://files.pythonhosted.org/packages/63/c5/27ba71a9da2a09be9ff2f0dac522769956c8c89d6516565b21c9c78bfae6/python_calamine-0.5.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:13662895dac487315ccce25ea272a1ea7e7ac05d899cde4e33d59d6c43274c54", size = 897332, upload-time = "2025-10-21T07:11:19.89Z" }, + { url = "https://files.pythonhosted.org/packages/5a/e7/c4be6ff8e8899ace98cacc9604a2dd1abc4901839b733addfb6ef32c22ba/python_calamine-0.5.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:23e354755583cfaa824ddcbe8b099c5c7ac19bf5179320426e7a88eea2f14bc5", size = 886885, upload-time = "2025-10-21T07:11:21.912Z" }, + { url = "https://files.pythonhosted.org/packages/38/24/80258fb041435021efa10d0b528df6842e442585e48cbf130e73fed2529b/python_calamine-0.5.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4e1bc3f22107dcbdeb32d4d3c5c1e8831d3c85d4b004a8606dd779721b29843d", size = 1043907, upload-time = "2025-10-21T07:11:23.3Z" }, + { url = "https://files.pythonhosted.org/packages/f2/20/157340787d03ef6113a967fd8f84218e867ba4c2f7fc58cc645d8665a61a/python_calamine-0.5.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:182b314117e47dbd952adaa2b19c515555083a48d6f9146f46faaabd9dab2f81", size = 942376, upload-time = "2025-10-21T07:11:24.866Z" }, + { url = "https://files.pythonhosted.org/packages/98/f5/aec030f567ee14c60b6fc9028a78767687f484071cb080f7cfa328d6496e/python_calamine-0.5.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8f882e092ab23f72ea07e2e48f5f2efb1885c1836fb949f22fd4540ae11742e", size = 906455, upload-time = "2025-10-21T07:11:26.203Z" }, + { url = "https://files.pythonhosted.org/packages/29/58/4affc0d1389f837439ad45f400f3792e48030b75868ec757e88cb35d7626/python_calamine-0.5.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:62a9b4b7b9bd99d03373e58884dfb60d5a1c292c8e04e11f8b7420b77a46813e", size = 948132, upload-time = "2025-10-21T07:11:27.507Z" }, + { url = "https://files.pythonhosted.org/packages/b4/2e/70ed04f39e682a9116730f56b7fbb54453244ccc1c3dae0662d4819f1c1d/python_calamine-0.5.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:98bb011d33c0e2d183ff30ab3d96792c3493f56f67a7aa2fcadad9a03539e79b", size = 1077436, upload-time = "2025-10-21T07:11:28.801Z" }, + { url = "https://files.pythonhosted.org/packages/cb/ce/806f8ce06b5bb9db33007f85045c304cda410970e7aa07d08f6eaee67913/python_calamine-0.5.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:6b218a95489ff2f1cc1de0bba2a16fcc82981254bbb23f31d41d29191282b9ad", size = 1150570, upload-time = "2025-10-21T07:11:30.237Z" }, + { url = "https://files.pythonhosted.org/packages/18/da/61f13c8d107783128c1063cf52ca9cacdc064c58d58d3cf49c1728ce8296/python_calamine-0.5.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:e8296a4872dbe834205d25d26dd6cfcb33ee9da721668d81b21adc25a07c07e4", size = 1080286, upload-time = "2025-10-21T07:11:31.564Z" }, + { url = "https://files.pythonhosted.org/packages/99/85/c5612a63292eb7d0648b17c5ff32ad5d6c6f3e1d78825f01af5c765f4d3f/python_calamine-0.5.4-cp312-cp312-win32.whl", hash = "sha256:cebb9c88983ae676c60c8c02aa29a9fe13563f240579e66de5c71b969ace5fd9", size = 676617, upload-time = "2025-10-21T07:11:32.833Z" }, + { url = "https://files.pythonhosted.org/packages/bb/18/5a037942de8a8df0c805224b2fba06df6d25c1be3c9484ba9db1ca4f3ee6/python_calamine-0.5.4-cp312-cp312-win_amd64.whl", hash = "sha256:15abd7aff98fde36d7df91ac051e86e66e5d5326a7fa98d54697afe95a613501", size = 721464, upload-time = "2025-10-21T07:11:34.383Z" }, + { url = "https://files.pythonhosted.org/packages/d1/8b/89ca17b44bcd8be5d0e8378d87b880ae17a837573553bd2147cceca7e759/python_calamine-0.5.4-cp312-cp312-win_arm64.whl", hash = "sha256:1cef0d0fc936974020a24acf1509ed2a285b30a4e1adf346c057112072e84251", size = 687268, upload-time = "2025-10-21T07:11:36.324Z" }, + { url = "https://files.pythonhosted.org/packages/60/82/0a6581f05916e2c09a418b5624661cb51dc0b8bd10dd0e8613b90bf785ad/python_calamine-0.5.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:46b258594314f89b9b92c6919865eabf501391d000794e70dc7a6b24e7bda9c6", size = 849926, upload-time = "2025-10-21T07:11:37.835Z" }, + { url = "https://files.pythonhosted.org/packages/25/ca/1d4698b2de6e5d9efc712bd4c018125021eaf9a0f20559a35654b17f1e7f/python_calamine-0.5.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:feea9a85683e66b4e87b381812086210e90521914d6960c45f30bedb9e186ffe", size = 825321, upload-time = "2025-10-21T07:11:39.299Z" }, + { url = "https://files.pythonhosted.org/packages/13/dd/09bd18c8ad6327bc03de2e3ce7c2150d0e605f8aa538615a6fc8b25b2f52/python_calamine-0.5.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd64ab500d7c1eb415776d722c4cda7d60fd373642f159946b5f03ae55dd246a", size = 897213, upload-time = "2025-10-21T07:11:40.801Z" }, + { url = "https://files.pythonhosted.org/packages/67/80/6cd2f358b96451dbfe40ff88e50ed875264e366cea01d1ec51aa46afc55a/python_calamine-0.5.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c15a09a24e8c2de4adc0f039c05dc37b85e8a3fd0befa8b8fcb8a61f13837965", size = 887237, upload-time = "2025-10-21T07:11:42.149Z" }, + { url = "https://files.pythonhosted.org/packages/2c/1f/5abdf618c402c586c7d8e02664b2a4d85619e3b67c75f63c535fd819eb42/python_calamine-0.5.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3d956ab6a36afe3fabe0f3aeac86b4e6c16f8c1bc0e3fa0b57d0eb3e66e40c91", size = 1044372, upload-time = "2025-10-21T07:11:43.566Z" }, + { url = "https://files.pythonhosted.org/packages/82/10/164fed6f46c469f6e3a5c17f2864c8b028109f6d5da928f6aa34e0fbd396/python_calamine-0.5.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:94840783be59659e367ae4f1c59fffcc54ad7f7f6935cbfbaa6879e6633c5a52", size = 942187, upload-time = "2025-10-21T07:11:45.347Z" }, + { url = "https://files.pythonhosted.org/packages/43/4f/a5f167a95ef57c3e37fe8ae0a41745061442f44e4c0c4395d70c8740e453/python_calamine-0.5.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8304fc19322f05dc0af78851ca47255a088a9c0cc3874648b42038e7f27ff2f", size = 905766, upload-time = "2025-10-21T07:11:46.972Z" }, + { url = "https://files.pythonhosted.org/packages/07/5c/2804120184a0b4b1510e6274e7c29f461bd80bae1935ad26ea68f4c31a6c/python_calamine-0.5.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e0ee4a18b1341600111d756c6d5d30546729b8961e0c552b4d63fc40dcd609d7", size = 948683, upload-time = "2025-10-21T07:11:48.846Z" }, + { url = "https://files.pythonhosted.org/packages/7d/7a/a0ec3339be0e0c4288fac04bf754c3a9c7d3c863e167359764384031469c/python_calamine-0.5.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:b5d81afbad55fd78146bad8bc31d55793fe3fdff5e49afab00c704f5f567d330", size = 1077564, upload-time = "2025-10-21T07:11:50.333Z" }, + { url = "https://files.pythonhosted.org/packages/8d/9c/78dd74b3cb2614c556014c205d63966043d62fe2e0a4570ccbf5a926bf18/python_calamine-0.5.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:c71c51211ce24db640099c60bccc2c93d58639664e8fb69db48a35ed3b272f8e", size = 1150587, upload-time = "2025-10-21T07:11:52.133Z" }, + { url = "https://files.pythonhosted.org/packages/c9/82/24bca60640366251fb5eb6ffa0199ad05aa638d7d228dc4ba338e9dd9835/python_calamine-0.5.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:c64dec92cb1f094298e601ad10ceb6bc15668f5ae24a7e852589f8c0fdb346d2", size = 1080031, upload-time = "2025-10-21T07:11:53.664Z" }, + { url = "https://files.pythonhosted.org/packages/20/97/7696c0d36f99fc6ab9770632655dd67389953b4d94e3394c280520db5e23/python_calamine-0.5.4-cp313-cp313-win32.whl", hash = "sha256:5f64e3f2166001a98c3f4218eac96fa24f96f9f9badad4b8a86d9a77e81284ad", size = 676927, upload-time = "2025-10-21T07:11:55.131Z" }, + { url = "https://files.pythonhosted.org/packages/4a/de/e9a1c650ba446f46e880f1bf07744c3dbc709b8f0285cf6db091bbe7f30d/python_calamine-0.5.4-cp313-cp313-win_amd64.whl", hash = "sha256:b0858c907ac3e4000ab7f4422899559e412fe4a71dba3d7c96f9ecb1cf03a9ce", size = 721241, upload-time = "2025-10-21T07:11:56.597Z" }, + { url = "https://files.pythonhosted.org/packages/d7/58/0a6483cfc5bffd3df8a76c4041aa6396566cd0dddf180055064074fc6e77/python_calamine-0.5.4-cp313-cp313-win_arm64.whl", hash = "sha256:2df6c552546f36702ae2a78f9ffeab5ecf638f27eece2737735c3fd4080d2809", size = 687761, upload-time = "2025-10-21T07:11:57.885Z" }, + { url = "https://files.pythonhosted.org/packages/df/c6/cbfb8050adb339fd604f9465aa67824f6da63ee74adb88bbad907f17397c/python_calamine-0.5.4-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:7bf110052f62dcb16c507b741b5ab637b9b2e89b25406cb1bd795b2f1207439d", size = 848476, upload-time = "2025-10-21T07:11:59.651Z" }, + { url = "https://files.pythonhosted.org/packages/2a/ab/888592578ee23cf7377009db7a396b73f011df5cd6e7627667cdc862a813/python_calamine-0.5.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:800763dcb01d3752450a6ee204bc22e661a20221e40490f85fff1c98ad96c2e9", size = 823829, upload-time = "2025-10-21T07:12:01.03Z" }, + { url = "https://files.pythonhosted.org/packages/e0/22/5dbbb506462f8ce9e7445905fa0efba73a25341d2bdd7f0da0b9c8c5cd99/python_calamine-0.5.4-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f40f2596f2ec8085343e67e73ad5321f18e36e6c2f7b15980201aec03666cf4c", size = 895812, upload-time = "2025-10-21T07:12:02.466Z" }, + { url = "https://files.pythonhosted.org/packages/23/b9/f839641ebe781cf7e82d2b58d0c3a609686f83516a946298627f20f5fc9f/python_calamine-0.5.4-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:859b1e8586cf9944edfa32ba1679be2b40407d67c8c071a97429ea4a79adcd08", size = 886707, upload-time = "2025-10-21T07:12:03.874Z" }, + { url = "https://files.pythonhosted.org/packages/98/cf/d74743dc72128248ce598aa9eb2e82457166c380b48493f46ca001d429cf/python_calamine-0.5.4-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3221b145e52d708597b74832ff517adf9153b959aa17d05d2e7fc259855c6c25", size = 1042868, upload-time = "2025-10-21T07:12:05.362Z" }, + { url = "https://files.pythonhosted.org/packages/c3/d6/55b061c7cf7e6c06279af4abf83aef01168f2a902446c79393cfecfc1a06/python_calamine-0.5.4-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0294d8e677f85a178c74a5952da668a35dd0522e7852f5a398aae01a9577fd0d", size = 941310, upload-time = "2025-10-21T07:12:06.866Z" }, + { url = "https://files.pythonhosted.org/packages/28/d7/457adac7eae82584ce36860ba9073e4e9492195fee6f4b41397733a92604/python_calamine-0.5.4-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:713df8fd08d71030bf7677712f4764e306e379e06c05f7656fed42e7cd256602", size = 904649, upload-time = "2025-10-21T07:12:08.851Z" }, + { url = "https://files.pythonhosted.org/packages/fa/ad/0dbb38d992245a71630c93d928d3e1b5581c98e92d214d1ec80da0036c65/python_calamine-0.5.4-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:adc83cd98e58fecdedce7209bad98452b2702cc3cecb8e9066e0db198b939bb5", size = 944747, upload-time = "2025-10-21T07:12:10.288Z" }, + { url = "https://files.pythonhosted.org/packages/69/99/dcb7f5a7149afefcdfb5c1d2d0fb9b086df5dc228d54e693875b0797c680/python_calamine-0.5.4-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:c70ed54297ca49bb449df00a5e6f317df1162e042a65dd3fbeb9c9a6d85cb354", size = 1075868, upload-time = "2025-10-21T07:12:11.817Z" }, + { url = "https://files.pythonhosted.org/packages/33/19/c2145b5912fadf495d66ae96bb2735340fea1183844843fe975837c315a6/python_calamine-0.5.4-cp313-cp313t-musllinux_1_1_armv7l.whl", hash = "sha256:78baabfc04a918efcc44e61385526143fd773317fc263ee59a5aa8909854bae3", size = 1149999, upload-time = "2025-10-21T07:12:13.381Z" }, + { url = "https://files.pythonhosted.org/packages/33/e5/6787068c97978212ae7b71d6d6e4785474ac0c496f01c50d04866b66d72e/python_calamine-0.5.4-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:a12aa39963eaae84a1ae70fbd49171bcd901fff87c93095bd80760cb0107220c", size = 1078902, upload-time = "2025-10-21T07:12:15.202Z" }, + { url = "https://files.pythonhosted.org/packages/30/99/21c377f9173af146553569f672ef8989017f1dafa80ec912930ccbaaab0c/python_calamine-0.5.4-cp313-cp313t-win_amd64.whl", hash = "sha256:7c46c472299781bf51bcf550d81fe812363e3ca13535023bd2764145fbc52823", size = 722243, upload-time = "2025-10-21T07:12:16.62Z" }, + { url = "https://files.pythonhosted.org/packages/45/91/a7d2eb4b5f34d34b6ed8d217dee91b1d5224d15905ca8870cf62858d2b25/python_calamine-0.5.4-cp313-cp313t-win_arm64.whl", hash = "sha256:e6b1a6f969207e3729366ee2ff1b5143a9b201e59af0d2708e51a39ef702652f", size = 684569, upload-time = "2025-10-21T07:12:18.401Z" }, + { url = "https://files.pythonhosted.org/packages/d1/89/0b9dc4dc7ebadd088b9558bd8e09a02ac0a11edd772b77f47c4c66dd2a22/python_calamine-0.5.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:79c493cc53ca4d728a758600291ceefdec6b705a199ce75f946c8f8858102d51", size = 850140, upload-time = "2025-10-21T07:12:19.853Z" }, + { url = "https://files.pythonhosted.org/packages/a4/c2/379f43ad7944b8d200045c0a9c2783b3e6aac1015ad0a490996754ebf855/python_calamine-0.5.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a6001164afb03ec12725c5c8e975b73c6b6491381b03f28e5a88226e2e7473d7", size = 824651, upload-time = "2025-10-21T07:12:21.404Z" }, + { url = "https://files.pythonhosted.org/packages/d5/4f/c484f6f0d99d14631de9e065bdf7932fe573f7b6f0bf79d6b3c0219595d7/python_calamine-0.5.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:656cb61bd306687486a45947f632cd5afef63beb78da2c73ac59ab66aa455f7e", size = 897554, upload-time = "2025-10-21T07:12:23.733Z" }, + { url = "https://files.pythonhosted.org/packages/e5/eb/1966d0fde74ca7023678eacd128a14a4c136dc287a9f1ec21ed2236f43d4/python_calamine-0.5.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6aa79ff3770fc88732b35f00c4f3ac884bc2b5289e7893484a8d8d4790e67c7a", size = 887612, upload-time = "2025-10-21T07:12:25.25Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2a/50a4d29139ef6f67cc29b7bb2d821253f032bdbfa451faba986fc3ce1bf8/python_calamine-0.5.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2908be3d273ff2756893840b5bfeb07a444c193f55a2f2343d55870df5d228dc", size = 1046417, upload-time = "2025-10-21T07:12:26.747Z" }, + { url = "https://files.pythonhosted.org/packages/e7/3f/4130952e2646867f6a8c3f0cda8a7834a95b720fd557115ce722d96250c9/python_calamine-0.5.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fbcda9f0c195584bede0518597380e9431dcacd298c5f6b627bae1a38510ff25", size = 944118, upload-time = "2025-10-21T07:12:28.494Z" }, + { url = "https://files.pythonhosted.org/packages/27/f8/64fc1688c833ed5e79f3d657908f616909c03a4936eed8320519c6d5ffc2/python_calamine-0.5.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a78f0c8853ce544b640e9a6994690c434be7a3e9189b4f49536669d220180a63", size = 906103, upload-time = "2025-10-21T07:12:30.201Z" }, + { url = "https://files.pythonhosted.org/packages/b0/13/9ef73a559f492651e3588e6ecbeaf82cb91cdb084eb05b9a70f50ab857b7/python_calamine-0.5.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ba6f1181dcad2f6ec7da0ea6272bf68b59ce2135800db06374b083cac599780e", size = 947955, upload-time = "2025-10-21T07:12:32.035Z" }, + { url = "https://files.pythonhosted.org/packages/8e/8d/e303b70fe8c6fa64179633445a5bf424a23153459ddcaff861300e5c2221/python_calamine-0.5.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:eea735f76e5a06efc91fe8907bca03741e71febcadd8621c6ea48df7b4a64be3", size = 1077823, upload-time = "2025-10-21T07:12:33.568Z" }, + { url = "https://files.pythonhosted.org/packages/6e/ce/8e9b85b7488488a7c3c673ae727ba6eb4c73f97d81acb250048f8e223196/python_calamine-0.5.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:2d138e5a960ae7a8fc91674252cf2d7387a5cef2892ebdccf3eea2756e1ced0c", size = 1150733, upload-time = "2025-10-21T07:12:35.097Z" }, + { url = "https://files.pythonhosted.org/packages/37/e0/ca4ad49b693d165b87de068ad78c9aca35a8657a5695cbcb212426e29bd9/python_calamine-0.5.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:8ad42673f5c0bb2d30b17b2ec3de5e8eae6dde4097650332c507b4146c63bb9c", size = 1080697, upload-time = "2025-10-21T07:12:36.679Z" }, + { url = "https://files.pythonhosted.org/packages/2a/62/1065dbf7c554bd80ba976d60278525750c0ff0feb56812f76b6531b67f21/python_calamine-0.5.4-cp314-cp314-win32.whl", hash = "sha256:36918496befbeeddc653e1499c090923dcf803d2633eb8bd473a9d21bdd06e79", size = 677184, upload-time = "2025-10-21T07:12:38.295Z" }, + { url = "https://files.pythonhosted.org/packages/e0/2f/f21bffb13712434168f7125f733fb728f723d79262a5acb90328a13fbf11/python_calamine-0.5.4-cp314-cp314-win_amd64.whl", hash = "sha256:bc01a7c03d302d11721a0ca00f67b71ebec125abab414f604bb03749b8c3557e", size = 722692, upload-time = "2025-10-21T07:12:39.764Z" }, + { url = "https://files.pythonhosted.org/packages/e9/b5/7214e8105b5165653cf49c9edec17db9d2551645be1a332bf09013908bc2/python_calamine-0.5.4-cp314-cp314-win_arm64.whl", hash = "sha256:8ab116aa7aea8bb3823f7a00c95bea08940db995556d287b6c1e51f3e83b3570", size = 686400, upload-time = "2025-10-21T07:12:41.371Z" }, + { url = "https://files.pythonhosted.org/packages/47/91/6815256d05940608c92e4d9467db04b9eab6124d8a9bd37f5c967157ead6/python_calamine-0.5.4-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:bc004d1da2779aea2b6782d18d977f8e1121e3a245c331db545f69fc2ae5cad0", size = 848400, upload-time = "2025-10-21T07:12:43.22Z" }, + { url = "https://files.pythonhosted.org/packages/0b/2c/fee8ffaac4a2385e9522c0f0febb690499a00fb99c0c953e7cd4bcdc6695/python_calamine-0.5.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5fb8c85acf5ccfe961023de01ce3a36839e310b5d9dc9aac9db01f350fbd3cec", size = 825000, upload-time = "2025-10-21T07:12:45.008Z" }, + { url = "https://files.pythonhosted.org/packages/a0/4d/61eeddde208958518cbf9ab76f387c379bd56019c029ea5fcc6cf3b96044/python_calamine-0.5.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e9dd48379eabc27c2bb73356fd5d1df48a46caf94433d4f60bdd38ad416a6f46", size = 896022, upload-time = "2025-10-21T07:12:46.503Z" }, + { url = "https://files.pythonhosted.org/packages/90/87/9ae23a3c2a7d2891c04436d0d7ed9984cb0f7145c96f6f8b36a345c7cc95/python_calamine-0.5.4-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:da3c2aa81de7cb20834b5326f326ba91a58123f10845864c3911e9dd819b9271", size = 887206, upload-time = "2025-10-21T07:12:48.446Z" }, + { url = "https://files.pythonhosted.org/packages/13/23/9289c350b8d7976295d01474f17a22fb9a42695dc403aa0f735a4e008791/python_calamine-0.5.4-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c9c09cd413e69f3366bdb73fc525c02963f29ca01da5a2ef9abed5486bba0e6a", size = 1042372, upload-time = "2025-10-21T07:12:50.04Z" }, + { url = "https://files.pythonhosted.org/packages/da/66/cd2c8ec4090d1cfd0875e7a45a7a7d55a9670b18daaad45845360d4def2c/python_calamine-0.5.4-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b678e11378b991e551d1260e21099cd9c5cffa4c83f816cba0aa05e9023d0f06", size = 941589, upload-time = "2025-10-21T07:12:51.635Z" }, + { url = "https://files.pythonhosted.org/packages/7d/d5/6a8199af0efe83945beb3df5a0556d658108cbf71b2cc449f3b5106afaef/python_calamine-0.5.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c7397781c4aedf70c5e4adcd31e2209035f4eb78fcb8ed887d252965e924530", size = 904284, upload-time = "2025-10-21T07:12:53.184Z" }, + { url = "https://files.pythonhosted.org/packages/93/0d/a419be4b036207ca61e5bbd15225f9637348a7c5c353d009ee0af5d38e90/python_calamine-0.5.4-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9062677c5c1ca9f16dd0d29875a9ffa841fe6b230a7c03b3ed92146fc42572fd", size = 945532, upload-time = "2025-10-21T07:12:54.692Z" }, + { url = "https://files.pythonhosted.org/packages/a1/eb/4b39fc8d42a13578b4cc695d0e1e84bd5d87087444c27f667e1d7e756f4f/python_calamine-0.5.4-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:0cd2881eadb30fddb84abe4fccb1544c6ba15aec45fe833a5691f5b0c8eeaec1", size = 1075965, upload-time = "2025-10-21T07:12:56.247Z" }, + { url = "https://files.pythonhosted.org/packages/eb/a5/d9d286986a192afd35056cbb53ca6979c09a584ca8ae9c2ab818141a9dde/python_calamine-0.5.4-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:6d077520c78530ad610fc1dc94463e618df8600d071409d8aa1bc195b9759f6f", size = 1150192, upload-time = "2025-10-21T07:12:58.236Z" }, + { url = "https://files.pythonhosted.org/packages/d9/2c/37612d97cf969adf39dbad04c14e8c35aedc8e6476b8e97cb5a5c2ed2b76/python_calamine-0.5.4-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:1ba09027e12a495b4e3eda4a7c59bb38d058e1941382bb2cc2e3a2a7bd12d3ba", size = 1078532, upload-time = "2025-10-21T07:13:00.123Z" }, + { url = "https://files.pythonhosted.org/packages/b1/2b/f6913d5cfc35c7d9c76df9fbabf00cbc5ddc525abc1e1dc55d5a57a059aa/python_calamine-0.5.4-cp314-cp314t-win_amd64.whl", hash = "sha256:a45f72a0ae0184c6ae99deefba735fdf82f858bcbf25caeb14366d45b18f23ea", size = 722451, upload-time = "2025-10-21T07:13:01.902Z" }, + { url = "https://files.pythonhosted.org/packages/88/0c/b6bf7a7033b0f0143e1494f0f6803f63ec8755dc30f054775434fe06d310/python_calamine-0.5.4-cp314-cp314t-win_arm64.whl", hash = "sha256:1ec345f20f0ea6e525e8d5a6dbb68065d374bc1feaf5bb479a93e2ed1d4db9ae", size = 684875, upload-time = "2025-10-21T07:13:03.308Z" }, +] + [[package]] name = "python-dateutil" version = "2.9.0.post0" diff --git a/packages/ui/src/components/data-table/column-cpfcnpj.tsx b/packages/ui/src/components/data-table/column-cpfcnpj.tsx new file mode 100644 index 0000000..1891fd4 --- /dev/null +++ b/packages/ui/src/components/data-table/column-cpfcnpj.tsx @@ -0,0 +1,40 @@ +import type { Row, Column } from '@tanstack/react-table' +import { + formatCPF, + formatCNPJ, + isValidCPF, + isValidCNPJ +} from '@brazilian-utils/brazilian-utils' + +interface DataTableColumnDatetimeProps + extends React.HTMLAttributes { + row: Row + column: Column +} + +const formatted = new Intl.DateTimeFormat('pt-BR', { + day: '2-digit', + month: '2-digit', + year: 'numeric', + hour: '2-digit', + minute: '2-digit' +}) + +export function DataTableColumnCpfCnpj({ + row, + column +}: DataTableColumnDatetimeProps) { + // @ts-ignore + const { accessorKey } = column.columnDef + const value = row.getValue(accessorKey) as string + + if (isValidCPF(value)) { + return formatCPF(value) + } + + if (isValidCNPJ(value)) { + return formatCNPJ(value) + } + + return <> +} diff --git a/packages/ui/src/components/data-table/column-currency.tsx b/packages/ui/src/components/data-table/column-currency.tsx new file mode 100644 index 0000000..489ba29 --- /dev/null +++ b/packages/ui/src/components/data-table/column-currency.tsx @@ -0,0 +1,27 @@ +import type { Row, Column } from '@tanstack/react-table' + +interface DataTableColumnDatetimeProps + extends React.HTMLAttributes { + row: Row + column: Column +} + +const formatted = new Intl.NumberFormat('pt-BR', { + style: 'currency', + currency: 'BRL' +}) + +export function DataTableColumnCurrency({ + row, + column +}: DataTableColumnDatetimeProps) { + // @ts-ignore + const { accessorKey } = column.columnDef + const value = row.getValue(accessorKey) as number + + if (value) { + return formatted.format(value) + } + + return <> +} diff --git a/packages/ui/src/components/data-table/column-datetime.tsx b/packages/ui/src/components/data-table/column-datetime.tsx index e69de29..2329de2 100644 --- a/packages/ui/src/components/data-table/column-datetime.tsx +++ b/packages/ui/src/components/data-table/column-datetime.tsx @@ -0,0 +1,30 @@ +import type { Row, Column } from '@tanstack/react-table' + +interface DataTableColumnDatetimeProps + extends React.HTMLAttributes { + row: Row + column: Column +} + +const formatted = new Intl.DateTimeFormat('pt-BR', { + day: '2-digit', + month: '2-digit', + year: 'numeric', + hour: '2-digit', + minute: '2-digit' +}) + +export function DataTableColumnDatetime({ + row, + column +}: DataTableColumnDatetimeProps) { + // @ts-ignore + const { accessorKey } = column.columnDef + const value = row.getValue(accessorKey) + + if (value) { + return formatted.format(new Date(value as string)) + } + + return <> +} diff --git a/packages/ui/src/components/data-table/column-header.tsx b/packages/ui/src/components/data-table/column-header.tsx index fe9e75f..d8e1a92 100644 --- a/packages/ui/src/components/data-table/column-header.tsx +++ b/packages/ui/src/components/data-table/column-header.tsx @@ -32,6 +32,7 @@ export function DataTableColumnHeader({ onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')} > {title} + {column.getIsSorted() === 'desc' ? ( ) : column.getIsSorted() === 'asc' ? ( diff --git a/packages/ui/src/components/data-table/data-table.tsx b/packages/ui/src/components/data-table/data-table.tsx index fa0d381..4f9e8ff 100644 --- a/packages/ui/src/components/data-table/data-table.tsx +++ b/packages/ui/src/components/data-table/data-table.tsx @@ -256,7 +256,7 @@ export function DataTable({ {row.getVisibleCells().map((cell) => ( bool: + new_image = event.detail['new_image'] + csvfile = new_image['s3_uri'] + _, _, start_byte, _, end_byte = new_image['sk'].split('#') + header = SimpleNamespace( + **{ + column_name: int(idx) + for idx, column_name in ( + column.split(':') for column in new_image['columns'] + ) + } + ) + + data = _get_s3_object_range( + csvfile, + start_byte=start_byte, + end_byte=end_byte, + s3_client=s3_client, + ) + reader = csv.reader(data) + users = [ + { + 'name': row[header.name], + 'email': row[header.email], + 'cpf': row[header.cpf], + } + for row in reader + ] + ctx = {'org': new_image['org']} + # Key pattern `FILE#{file}` + sk = new_image['file_sk'] + + with ( + dyn.transact_writer() as transact, + processor(records=users, handler=_create_user, context=ctx) as batch, + ): + result = batch.process() + + for r in result: + transact.put( + item={ + 'id': new_image['id'], + 'sk': f'REPORTING#{sk}#ITEM#{secrets.token_urlsafe(16)}', + 'input': r.input_record, + 'status': r.status.value.upper(), + 'error': r.cause.get('type') if r.cause else None, + } + ) + + transact.update( + key=KeyPair( + pk=new_image['id'], + sk=sk, + ), + update_expr='ADD progress :progress', + expr_attr_values={ + ':progress': new_image['weight'], + }, + ) + transact.delete( + key=KeyPair( + pk=new_image['id'], + sk=new_image['sk'], + ) + ) + + return True + + +def _create_user(rawuser: dict, context: dict) -> None: + now_ = now() + user_id = uuid4() + org = Org(**context['org']) + user = User(**rawuser) + + with dyn.transact_writer() as transact: + transact.put( + item={ + **user.model_dump(), + 'id': user_id, + 'sk': '0', + 'email_verified': False, + 'tenant_id': {org.id}, + # Post-migration: uncomment the folloing line + # 'org_id': {org.id}, + 'created_at': now_, + }, + ) + transact.put( + item={ + 'id': user_id, + # Post-migration: rename `emails` to `EMAIL` + 'sk': f'emails#{user.email}', + 'email_verified': False, + 'email_primary': True, + 'created_at': now_, + } + ) + transact.put( + item={ + # Post-migration: rename `cpf` to `CPF` + 'id': 'cpf', + 'sk': user.cpf, + 'created_at': now_, + }, + cond_expr='attribute_not_exists(sk)', + exc_cls=CPFConflictError, + ) + transact.put( + item={ + # Post-migration: rename `email` to `EMAIL` + 'id': 'email', + 'sk': user.email, + 'created_at': now_, + }, + cond_expr='attribute_not_exists(sk)', + exc_cls=EmailConflictError, + ) + transact.put( + item={ + 'id': user_id, + 'sk': f'orgs#{org.id}', + # Post-migration: uncomment the following line + # pk=f'ORG#{org.id}', + 'name': org.name, + 'cnpj': org.cnpj, + 'created_at': now_, + } + ) + transact.put( + item={ + 'id': f'orgmembers#{org.id}', + # Post-migration: uncomment the following line + # pk=f'MEMBER#ORG#{org_id}', + 'sk': user_id, + 'created_at': now_, + } + ) + + +def _get_s3_object_range( + s3_uri: str, + *, + start_byte: int, + end_byte: int, + s3_client: S3Client, +) -> StringIO: + bucket, key = s3_uri.replace('s3://', '').split('/', 1) + + r = s3_client.get_object( + Bucket=bucket, + Key=key, + Range=f'bytes={start_byte}-{end_byte}', + ) + + return StringIO(r['Body'].read().decode('utf-8')) diff --git a/users-events/app/events/batch/csv_into_chunks.py b/users-events/app/events/batch/csv_into_chunks.py index ef20e78..1fe0ac7 100644 --- a/users-events/app/events/batch/csv_into_chunks.py +++ b/users-events/app/events/batch/csv_into_chunks.py @@ -1,22 +1,58 @@ +from decimal import Decimal + from aws_lambda_powertools.utilities.data_classes import ( EventBridgeEvent, event_source, ) from aws_lambda_powertools.utilities.typing import LambdaContext +from layercake.dateutils import now +from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair -from boto3clients import s3_client -from config import CHUNK_SIZE +from boto3clients import dynamodb_client, s3_client +from config import CHUNK_SIZE, USER_TABLE from csv_utils import byte_ranges +dyn = DynamoDBPersistenceLayer(USER_TABLE, dynamodb_client) transport_params = {'client': s3_client} @event_source(data_class=EventBridgeEvent) def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool: + now_ = now() new_image = event.detail['new_image'] csvfile = new_image['s3_uri'] - pairs = byte_ranges(csvfile, CHUNK_SIZE, transport_params=transport_params) + chunks = byte_ranges(csvfile, CHUNK_SIZE, transport_params=transport_params) + total_chunks = len(chunks) + weight_per_chunk = round(100 / total_chunks, 2) + weights = [weight_per_chunk] * total_chunks + # Fix last value to balance total + weights[-1] = round(100 - sum(weights[:-1]), 2) - print(pairs) + with dyn.transact_writer() as transact: + transact.update( + key=KeyPair(new_image['id'], new_image['sk']), + update_expr='SET total_chunks = :total_chunks, \ + progress = :progress, \ + started_at = :now', + expr_attr_values={ + ':total_chunks': total_chunks, + ':progress': 0, + ':now': now_, + }, + ) + + for (start, end), weight in zip(chunks, weights): + transact.put( + item={ + 'id': new_image['id'], + 'sk': f'CHUNK#START#{start}#END#{end}', + 'file_sk': new_image['sk'], + 's3_uri': new_image['s3_uri'], + 'columns': new_image['columns'], + 'weight': Decimal(str(weight)), + 'org': new_image['org'], + 'created_at': now_, + } + ) return True diff --git a/users-events/app/events/batch/mask_as_completed.py b/users-events/app/events/batch/mask_as_completed.py new file mode 100644 index 0000000..3fb9fb4 --- /dev/null +++ b/users-events/app/events/batch/mask_as_completed.py @@ -0,0 +1,14 @@ +from aws_lambda_powertools.utilities.data_classes import ( + EventBridgeEvent, + event_source, +) +from aws_lambda_powertools.utilities.typing import LambdaContext + +from boto3clients import s3_client + +transport_params = {'client': s3_client} + + +@event_source(data_class=EventBridgeEvent) +def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool: + return True diff --git a/users-events/app/events/batch/read_csv_chunk.py b/users-events/app/events/batch/read_csv_chunk.py deleted file mode 100644 index d7657f1..0000000 --- a/users-events/app/events/batch/read_csv_chunk.py +++ /dev/null @@ -1,55 +0,0 @@ -import csv -from io import StringIO -from typing import TYPE_CHECKING - -from aws_lambda_powertools.utilities.data_classes import ( - EventBridgeEvent, - event_source, -) -from aws_lambda_powertools.utilities.typing import LambdaContext - -from boto3clients import s3_client - -if TYPE_CHECKING: - from mypy_boto3_s3.client import S3Client -else: - S3Client = object - -transport_params = {'client': s3_client} - - -@event_source(data_class=EventBridgeEvent) -def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool: - new_image = event.detail['new_image'] - csvfile = new_image['s3_uri'] - - data = _get_s3_object_range( - csvfile, - start_byte=new_image['start_byte'], - end_byte=new_image['end_byte'], - s3_client=s3_client, - ) - reader = csv.reader(data) - - for x in reader: - print(x) - - return True - - -def _get_s3_object_range( - s3_uri: str, - *, - start_byte: int, - end_byte: int, - s3_client: S3Client, -) -> StringIO: - bucket, key = s3_uri.replace('s3://', '').split('/', 1) - - response = s3_client.get_object( - Bucket=bucket, - Key=key, - Range=f'bytes={start_byte}-{end_byte}', - ) - - return StringIO(response['Body'].read().decode('utf-8')) diff --git a/users-events/cf.py b/users-events/cf.py deleted file mode 100644 index d2d8e64..0000000 --- a/users-events/cf.py +++ /dev/null @@ -1,62 +0,0 @@ -# /// script -# dependencies = [ -# "cloudflare" -# ] -# /// - -from cloudflare import Cloudflare - -CLOUDFLARE_ACCOUNT_ID = '5436b62470020c04b434ad31c3e4cf4e' -CLOUDFLARE_API_TOKEN = 'gFndkBJCzH4pRX7mKXokdWfw1xhm8-9FHfvLfhwa' - - -client = Cloudflare(api_token=CLOUDFLARE_API_TOKEN) - -assistant = """ -You are a data analysis assistant specialized in identifying Brazilian -personal data from CSV files. - -These CSV files may or may not include headers. - -Your task is to analyze the content and identify only three possible -data types: 'name', 'cpf', and 'email'. - -Ignore all other fields. -""" - -csv_content = """ -,RICARDO GALLES BONET,ricardo.bonet@fanucamerica.com,424.430.528-93,NR-10 (RECICLAGEM) -,RULIO SIEFERT SERA,rulio.sera@fanucamerica.com,063.916.859-08,NR-10 (RECICLAGEM) -,MACIEL FERREIRA BOMFIM,maciel.bomfim@fanucamerica.com,334.547.088-85,NR-10 (RECICLAGEM) -,JAIME EDUARDO GALVEZ AVILES,jaime.galvez@fanucamerica.com,280.238.818-50,NR-12 -,JAIME EDUARDO GALVEZ AVILES,jaime.galvez@fanucamerica.com,280.238.818-50,NR-35 (RECICLAGEM) -,HIGOR MACHADO SILVA,higor.silva@fanucamerica.com,419.879.878-88,NR-12 -,LÁZARO SOUZA DIAS,lazaro.dias@fanucamerica.com,067.179.825-19,NR-12 -,JOÃO PEDRO AGUIAR GALASSO,joao.pedro@fanucamerica.com,570.403.588-40,NR-12 -""" - -prompt = f""" -Here is a CSV sample: - -{csv_content} - -Your task is to: -- Detect which columns most likely contain "name", "cpf", or "email". -- Skip any category that is not present in the data. -- Return ONLY a valid Python list of tuples, like: -[('name', index), ('cpf', index), ('email', index)] -- Use the column index that most likely matches each data type, -based on frequency and data format. -- Don't include explanations, code, or any additional text. -""" - -r = client.ai.run( - model_name='@cf/meta/llama-3-8b-instruct', - account_id=CLOUDFLARE_ACCOUNT_ID, - messages=[ - {'role': 'system', 'content': assistant}, - {'role': 'user', 'content': prompt}, - ], -) - -print(r) diff --git a/users-events/template.yaml b/users-events/template.yaml index 8c92888..a71737a 100644 --- a/users-events/template.yaml +++ b/users-events/template.yaml @@ -33,13 +33,15 @@ Resources: Properties: RetentionInDays: 90 - EventCsvChunksFunction: + EventCsvIntoChunksFunction: Type: AWS::Serverless::Function Properties: - Handler: events.batch.csv_chunks.lambda_handler + Handler: events.batch.csv_into_chunks.lambda_handler LoggingConfig: LogGroup: !Ref EventLog Policies: + - DynamoDBCrudPolicy: + TableName: !Ref UserTable - S3CrudPolicy: BucketName: !Ref BucketName Events: @@ -50,8 +52,35 @@ Resources: resources: [!Ref UserTable] detail: new_image: + id: + - prefix: BATCH_JOB#ORG# sk: - - prefix: BATCH_JOB#ORG + - prefix: FILE# + status: [PENDING] + + EventChunksIntoUsersFunction: + Type: AWS::Serverless::Function + Properties: + Handler: events.batch.chunks_into_user.lambda_handler + LoggingConfig: + LogGroup: !Ref EventLog + Policies: + - DynamoDBCrudPolicy: + TableName: !Ref UserTable + - S3CrudPolicy: + BucketName: !Ref BucketName + Events: + DynamoDBEvent: + Type: EventBridgeRule + Properties: + Pattern: + resources: [!Ref UserTable] + detail: + new_image: + id: + - prefix: BATCH_JOB#ORG# + sk: + - prefix: CHUNK#START# EventEmailReceivingFunction: Type: AWS::Serverless::Function diff --git a/users-events/tests/events/batch/test_chunks_into_users.py b/users-events/tests/events/batch/test_chunks_into_users.py new file mode 100644 index 0000000..04a8d0b --- /dev/null +++ b/users-events/tests/events/batch/test_chunks_into_users.py @@ -0,0 +1,47 @@ +import pprint + +from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair + +import events.batch.chunks_into_users as app + + +def test_chunk_csv( + dynamodb_persistence_layer: DynamoDBPersistenceLayer, + lambda_context, +): + pk = 'BATCH_JOB#ORG#1411844c-10d6-456e-959d-e91775145461' + file_sk = 'FILE#2025-11-13T16:04:53.024743' + event = { + 'detail': { + 'new_image': { + 'id': pk, + 'sk': 'CHUNK#START#0#END#4885', + 'weight': 100, + 'created_at': '2025-11-20T19:00:41.896001-03:00', + 'file_sk': file_sk, + 's3_uri': 's3://saladeaula.digital/samples/users.csv', + 'columns': { + '1:name', + '2:email', + '3:cpf', + }, + 'org': { + 'id': '1411844c-10d6-456e-959d-e91775145461', + 'name': 'EDUSEG', + 'cnpj': '15608435000190', + }, + }, + }, + } + + assert app.lambda_handler(event, lambda_context) # type: ignore + + r = dynamodb_persistence_layer.collection.query( + KeyPair( + pk=pk, + sk=f'REPORTING#{file_sk}', + ), + limit=100, + ) + pprint.pp(r['items']) + assert 26 == len(r['items']) diff --git a/users-events/tests/events/batch/test_csv_into_chunks.py b/users-events/tests/events/batch/test_csv_into_chunks.py index 33b73f6..3a03872 100644 --- a/users-events/tests/events/batch/test_csv_into_chunks.py +++ b/users-events/tests/events/batch/test_csv_into_chunks.py @@ -1,13 +1,37 @@ +from layercake.dateutils import now +from layercake.dynamodb import DynamoDBPersistenceLayer, PartitionKey + import events.batch.csv_into_chunks as app -def test_chunk_csv(lambda_context): +def test_chunk_csv( + dynamodb_persistence_layer: DynamoDBPersistenceLayer, + lambda_context, +): + pk = 'BATCH_JOB#ORG#1411844c-10d6-456e-959d-e91775145461' + sk = 'FILE#2025-11-13T16:04:53.024743' event = { 'detail': { 'new_image': { + 'id': pk, + 'sk': sk, 's3_uri': 's3://saladeaula.digital/samples/large_users.csv', + 'columns': { + '1:email', + '2:cpf', + '3:name', + }, + 'org': { + 'id': '1411844c-10d6-456e-959d-e91775145461', + 'name': 'EDUSEG', + 'cnpj': '15608435000190', + }, + 'created_at': now(), }, }, } app.lambda_handler(event, lambda_context) # type: ignore + + r = dynamodb_persistence_layer.collection.query(PartitionKey(pk), limit=100) + assert len(r['items']) == 67 diff --git a/users-events/tests/events/batch/test_excel_to_csv.py b/users-events/tests/events/batch/test_excel_to_csv.py new file mode 100644 index 0000000..bb63f67 --- /dev/null +++ b/users-events/tests/events/batch/test_excel_to_csv.py @@ -0,0 +1,13 @@ +import events.batch.excel_to_csv as app + + +def test_excel_to_csv(lambda_context): + event = { + 'detail': { + 'new_image': { + 's3_uri': 's3://saladeaula.digital/samples/large_users.csv', + }, + }, + } + + assert app.lambda_handler(event, lambda_context) # type: ignore diff --git a/users-events/tests/seeds.jsonl b/users-events/tests/seeds.jsonl index b704442..c8fee8a 100644 --- a/users-events/tests/seeds.jsonl +++ b/users-events/tests/seeds.jsonl @@ -1,4 +1,11 @@ {"id": {"S": "cJtK9SsnJhKPyxESe7g3DG"}, "sk": {"S": "0"}, "name": {"S": "EDUSEG"}} {"id": {"S": "cJtK9SsnJhKPyxESe7g3DG"}, "sk": {"S": "admins#5OxmMjL-ujoR5IMGegQz"}, "name": {"S": "Sérgio R Siqueira"}, "email": {"S": "sergio@somosbeta.com.br"}} {"id": {"S": "cnpj"}, "sk": {"S": "15608435000190"}, "user_id": {"S": "cJtK9SsnJhKPyxESe7g3DG"}} -{"id": {"S": "email"}, "sk": {"S": "org+15608435000190@users.noreply.saladeaula.digital"}, "user_id": {"S": "cJtK9SsnJhKPyxESe7g3DG"}} \ No newline at end of file +{"id": {"S": "email"}, "sk": {"S": "org+15608435000190@users.noreply.saladeaula.digital"}, "user_id": {"S": "cJtK9SsnJhKPyxESe7g3DG"}} + + +{"id": "BATCH_JOB#ORG#1411844c-10d6-456e-959d-e91775145461", "sk": "FILE#2025-11-13T16:04:53.024743", "progress": 0, "s3_uri": "s3://saladeaula.digital/samples/large_users.csv"} +{"id": "BATCH_JOB#ORG#1411844c-10d6-456e-959d-e91775145461", "sk": "CHUNK#START#0#END#3847", "weight": 25, "file_sk": "FILE#2025-11-13T16:04:53.024743", "s3_uri": "s3://saladeaula.digital/samples/large_users.csv"} +{"id": "BATCH_JOB#ORG#1411844c-10d6-456e-959d-e91775145461", "sk": "CHUNK#START#3848#END#7925", "weight": 25, "file_sk": "FILE#2025-11-13T16:04:53.024743", "s3_uri": "s3://saladeaula.digital/samples/large_users.csv"} +{"id": "BATCH_JOB#ORG#1411844c-10d6-456e-959d-e91775145461", "sk": "CHUNK#START#7926#END#11866", "weight": 25, "file_sk": "FILE#2025-11-13T16:04:53.024743", "s3_uri": "s3://saladeaula.digital/samples/large_users.csv"} +{"id": "BATCH_JOB#ORG#1411844c-10d6-456e-959d-e91775145461", "sk": "CHUNK#START#11867#END#15913", "weight": 25, "file_sk": "FILE#2025-11-13T16:04:53.024743", "s3_uri": "s3://saladeaula.digital/samples/large_users.csv"} diff --git a/users-events/tests/test_csv_utils.py b/users-events/tests/test_csv_utils.py index 2647818..4e50e20 100644 --- a/users-events/tests/test_csv_utils.py +++ b/users-events/tests/test_csv_utils.py @@ -9,8 +9,8 @@ def test_detect_delimiter(): def test_byte_ranges(): csvpath = 'tests/samples/users.csv' ranges = byte_ranges(csvpath, 10) - *_, pair = ranges - start_byte, end_byte = pair + *_, chunk = ranges + start_byte, end_byte = chunk assert ranges == [(0, 808), (809, 1655), (1656, 2303)] diff --git a/users-events/uv.lock b/users-events/uv.lock index d682098..c7a333b 100644 --- a/users-events/uv.lock +++ b/users-events/uv.lock @@ -472,7 +472,7 @@ wheels = [ [[package]] name = "layercake" -version = "0.11.1" +version = "0.11.2" source = { directory = "../layercake" } dependencies = [ { name = "arnparse" }, @@ -489,6 +489,7 @@ dependencies = [ { name = "pycpfcnpj" }, { name = "pydantic", extra = ["email"] }, { name = "pydantic-extra-types" }, + { name = "python-calamine" }, { name = "python-multipart" }, { name = "pytz" }, { name = "requests" }, @@ -513,6 +514,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" }, @@ -830,6 +832,67 @@ 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.5.4" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/60/82/0a6581f05916e2c09a418b5624661cb51dc0b8bd10dd0e8613b90bf785ad/python_calamine-0.5.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:46b258594314f89b9b92c6919865eabf501391d000794e70dc7a6b24e7bda9c6", size = 849926, upload-time = "2025-10-21T07:11:37.835Z" }, + { url = "https://files.pythonhosted.org/packages/25/ca/1d4698b2de6e5d9efc712bd4c018125021eaf9a0f20559a35654b17f1e7f/python_calamine-0.5.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:feea9a85683e66b4e87b381812086210e90521914d6960c45f30bedb9e186ffe", size = 825321, upload-time = "2025-10-21T07:11:39.299Z" }, + { url = "https://files.pythonhosted.org/packages/13/dd/09bd18c8ad6327bc03de2e3ce7c2150d0e605f8aa538615a6fc8b25b2f52/python_calamine-0.5.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd64ab500d7c1eb415776d722c4cda7d60fd373642f159946b5f03ae55dd246a", size = 897213, upload-time = "2025-10-21T07:11:40.801Z" }, + { url = "https://files.pythonhosted.org/packages/67/80/6cd2f358b96451dbfe40ff88e50ed875264e366cea01d1ec51aa46afc55a/python_calamine-0.5.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c15a09a24e8c2de4adc0f039c05dc37b85e8a3fd0befa8b8fcb8a61f13837965", size = 887237, upload-time = "2025-10-21T07:11:42.149Z" }, + { url = "https://files.pythonhosted.org/packages/2c/1f/5abdf618c402c586c7d8e02664b2a4d85619e3b67c75f63c535fd819eb42/python_calamine-0.5.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3d956ab6a36afe3fabe0f3aeac86b4e6c16f8c1bc0e3fa0b57d0eb3e66e40c91", size = 1044372, upload-time = "2025-10-21T07:11:43.566Z" }, + { url = "https://files.pythonhosted.org/packages/82/10/164fed6f46c469f6e3a5c17f2864c8b028109f6d5da928f6aa34e0fbd396/python_calamine-0.5.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:94840783be59659e367ae4f1c59fffcc54ad7f7f6935cbfbaa6879e6633c5a52", size = 942187, upload-time = "2025-10-21T07:11:45.347Z" }, + { url = "https://files.pythonhosted.org/packages/43/4f/a5f167a95ef57c3e37fe8ae0a41745061442f44e4c0c4395d70c8740e453/python_calamine-0.5.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8304fc19322f05dc0af78851ca47255a088a9c0cc3874648b42038e7f27ff2f", size = 905766, upload-time = "2025-10-21T07:11:46.972Z" }, + { url = "https://files.pythonhosted.org/packages/07/5c/2804120184a0b4b1510e6274e7c29f461bd80bae1935ad26ea68f4c31a6c/python_calamine-0.5.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e0ee4a18b1341600111d756c6d5d30546729b8961e0c552b4d63fc40dcd609d7", size = 948683, upload-time = "2025-10-21T07:11:48.846Z" }, + { url = "https://files.pythonhosted.org/packages/7d/7a/a0ec3339be0e0c4288fac04bf754c3a9c7d3c863e167359764384031469c/python_calamine-0.5.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:b5d81afbad55fd78146bad8bc31d55793fe3fdff5e49afab00c704f5f567d330", size = 1077564, upload-time = "2025-10-21T07:11:50.333Z" }, + { url = "https://files.pythonhosted.org/packages/8d/9c/78dd74b3cb2614c556014c205d63966043d62fe2e0a4570ccbf5a926bf18/python_calamine-0.5.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:c71c51211ce24db640099c60bccc2c93d58639664e8fb69db48a35ed3b272f8e", size = 1150587, upload-time = "2025-10-21T07:11:52.133Z" }, + { url = "https://files.pythonhosted.org/packages/c9/82/24bca60640366251fb5eb6ffa0199ad05aa638d7d228dc4ba338e9dd9835/python_calamine-0.5.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:c64dec92cb1f094298e601ad10ceb6bc15668f5ae24a7e852589f8c0fdb346d2", size = 1080031, upload-time = "2025-10-21T07:11:53.664Z" }, + { url = "https://files.pythonhosted.org/packages/20/97/7696c0d36f99fc6ab9770632655dd67389953b4d94e3394c280520db5e23/python_calamine-0.5.4-cp313-cp313-win32.whl", hash = "sha256:5f64e3f2166001a98c3f4218eac96fa24f96f9f9badad4b8a86d9a77e81284ad", size = 676927, upload-time = "2025-10-21T07:11:55.131Z" }, + { url = "https://files.pythonhosted.org/packages/4a/de/e9a1c650ba446f46e880f1bf07744c3dbc709b8f0285cf6db091bbe7f30d/python_calamine-0.5.4-cp313-cp313-win_amd64.whl", hash = "sha256:b0858c907ac3e4000ab7f4422899559e412fe4a71dba3d7c96f9ecb1cf03a9ce", size = 721241, upload-time = "2025-10-21T07:11:56.597Z" }, + { url = "https://files.pythonhosted.org/packages/d7/58/0a6483cfc5bffd3df8a76c4041aa6396566cd0dddf180055064074fc6e77/python_calamine-0.5.4-cp313-cp313-win_arm64.whl", hash = "sha256:2df6c552546f36702ae2a78f9ffeab5ecf638f27eece2737735c3fd4080d2809", size = 687761, upload-time = "2025-10-21T07:11:57.885Z" }, + { url = "https://files.pythonhosted.org/packages/df/c6/cbfb8050adb339fd604f9465aa67824f6da63ee74adb88bbad907f17397c/python_calamine-0.5.4-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:7bf110052f62dcb16c507b741b5ab637b9b2e89b25406cb1bd795b2f1207439d", size = 848476, upload-time = "2025-10-21T07:11:59.651Z" }, + { url = "https://files.pythonhosted.org/packages/2a/ab/888592578ee23cf7377009db7a396b73f011df5cd6e7627667cdc862a813/python_calamine-0.5.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:800763dcb01d3752450a6ee204bc22e661a20221e40490f85fff1c98ad96c2e9", size = 823829, upload-time = "2025-10-21T07:12:01.03Z" }, + { url = "https://files.pythonhosted.org/packages/e0/22/5dbbb506462f8ce9e7445905fa0efba73a25341d2bdd7f0da0b9c8c5cd99/python_calamine-0.5.4-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f40f2596f2ec8085343e67e73ad5321f18e36e6c2f7b15980201aec03666cf4c", size = 895812, upload-time = "2025-10-21T07:12:02.466Z" }, + { url = "https://files.pythonhosted.org/packages/23/b9/f839641ebe781cf7e82d2b58d0c3a609686f83516a946298627f20f5fc9f/python_calamine-0.5.4-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:859b1e8586cf9944edfa32ba1679be2b40407d67c8c071a97429ea4a79adcd08", size = 886707, upload-time = "2025-10-21T07:12:03.874Z" }, + { url = "https://files.pythonhosted.org/packages/98/cf/d74743dc72128248ce598aa9eb2e82457166c380b48493f46ca001d429cf/python_calamine-0.5.4-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3221b145e52d708597b74832ff517adf9153b959aa17d05d2e7fc259855c6c25", size = 1042868, upload-time = "2025-10-21T07:12:05.362Z" }, + { url = "https://files.pythonhosted.org/packages/c3/d6/55b061c7cf7e6c06279af4abf83aef01168f2a902446c79393cfecfc1a06/python_calamine-0.5.4-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0294d8e677f85a178c74a5952da668a35dd0522e7852f5a398aae01a9577fd0d", size = 941310, upload-time = "2025-10-21T07:12:06.866Z" }, + { url = "https://files.pythonhosted.org/packages/28/d7/457adac7eae82584ce36860ba9073e4e9492195fee6f4b41397733a92604/python_calamine-0.5.4-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:713df8fd08d71030bf7677712f4764e306e379e06c05f7656fed42e7cd256602", size = 904649, upload-time = "2025-10-21T07:12:08.851Z" }, + { url = "https://files.pythonhosted.org/packages/fa/ad/0dbb38d992245a71630c93d928d3e1b5581c98e92d214d1ec80da0036c65/python_calamine-0.5.4-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:adc83cd98e58fecdedce7209bad98452b2702cc3cecb8e9066e0db198b939bb5", size = 944747, upload-time = "2025-10-21T07:12:10.288Z" }, + { url = "https://files.pythonhosted.org/packages/69/99/dcb7f5a7149afefcdfb5c1d2d0fb9b086df5dc228d54e693875b0797c680/python_calamine-0.5.4-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:c70ed54297ca49bb449df00a5e6f317df1162e042a65dd3fbeb9c9a6d85cb354", size = 1075868, upload-time = "2025-10-21T07:12:11.817Z" }, + { url = "https://files.pythonhosted.org/packages/33/19/c2145b5912fadf495d66ae96bb2735340fea1183844843fe975837c315a6/python_calamine-0.5.4-cp313-cp313t-musllinux_1_1_armv7l.whl", hash = "sha256:78baabfc04a918efcc44e61385526143fd773317fc263ee59a5aa8909854bae3", size = 1149999, upload-time = "2025-10-21T07:12:13.381Z" }, + { url = "https://files.pythonhosted.org/packages/33/e5/6787068c97978212ae7b71d6d6e4785474ac0c496f01c50d04866b66d72e/python_calamine-0.5.4-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:a12aa39963eaae84a1ae70fbd49171bcd901fff87c93095bd80760cb0107220c", size = 1078902, upload-time = "2025-10-21T07:12:15.202Z" }, + { url = "https://files.pythonhosted.org/packages/30/99/21c377f9173af146553569f672ef8989017f1dafa80ec912930ccbaaab0c/python_calamine-0.5.4-cp313-cp313t-win_amd64.whl", hash = "sha256:7c46c472299781bf51bcf550d81fe812363e3ca13535023bd2764145fbc52823", size = 722243, upload-time = "2025-10-21T07:12:16.62Z" }, + { url = "https://files.pythonhosted.org/packages/45/91/a7d2eb4b5f34d34b6ed8d217dee91b1d5224d15905ca8870cf62858d2b25/python_calamine-0.5.4-cp313-cp313t-win_arm64.whl", hash = "sha256:e6b1a6f969207e3729366ee2ff1b5143a9b201e59af0d2708e51a39ef702652f", size = 684569, upload-time = "2025-10-21T07:12:18.401Z" }, + { url = "https://files.pythonhosted.org/packages/d1/89/0b9dc4dc7ebadd088b9558bd8e09a02ac0a11edd772b77f47c4c66dd2a22/python_calamine-0.5.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:79c493cc53ca4d728a758600291ceefdec6b705a199ce75f946c8f8858102d51", size = 850140, upload-time = "2025-10-21T07:12:19.853Z" }, + { url = "https://files.pythonhosted.org/packages/a4/c2/379f43ad7944b8d200045c0a9c2783b3e6aac1015ad0a490996754ebf855/python_calamine-0.5.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a6001164afb03ec12725c5c8e975b73c6b6491381b03f28e5a88226e2e7473d7", size = 824651, upload-time = "2025-10-21T07:12:21.404Z" }, + { url = "https://files.pythonhosted.org/packages/d5/4f/c484f6f0d99d14631de9e065bdf7932fe573f7b6f0bf79d6b3c0219595d7/python_calamine-0.5.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:656cb61bd306687486a45947f632cd5afef63beb78da2c73ac59ab66aa455f7e", size = 897554, upload-time = "2025-10-21T07:12:23.733Z" }, + { url = "https://files.pythonhosted.org/packages/e5/eb/1966d0fde74ca7023678eacd128a14a4c136dc287a9f1ec21ed2236f43d4/python_calamine-0.5.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6aa79ff3770fc88732b35f00c4f3ac884bc2b5289e7893484a8d8d4790e67c7a", size = 887612, upload-time = "2025-10-21T07:12:25.25Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2a/50a4d29139ef6f67cc29b7bb2d821253f032bdbfa451faba986fc3ce1bf8/python_calamine-0.5.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2908be3d273ff2756893840b5bfeb07a444c193f55a2f2343d55870df5d228dc", size = 1046417, upload-time = "2025-10-21T07:12:26.747Z" }, + { url = "https://files.pythonhosted.org/packages/e7/3f/4130952e2646867f6a8c3f0cda8a7834a95b720fd557115ce722d96250c9/python_calamine-0.5.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fbcda9f0c195584bede0518597380e9431dcacd298c5f6b627bae1a38510ff25", size = 944118, upload-time = "2025-10-21T07:12:28.494Z" }, + { url = "https://files.pythonhosted.org/packages/27/f8/64fc1688c833ed5e79f3d657908f616909c03a4936eed8320519c6d5ffc2/python_calamine-0.5.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a78f0c8853ce544b640e9a6994690c434be7a3e9189b4f49536669d220180a63", size = 906103, upload-time = "2025-10-21T07:12:30.201Z" }, + { url = "https://files.pythonhosted.org/packages/b0/13/9ef73a559f492651e3588e6ecbeaf82cb91cdb084eb05b9a70f50ab857b7/python_calamine-0.5.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ba6f1181dcad2f6ec7da0ea6272bf68b59ce2135800db06374b083cac599780e", size = 947955, upload-time = "2025-10-21T07:12:32.035Z" }, + { url = "https://files.pythonhosted.org/packages/8e/8d/e303b70fe8c6fa64179633445a5bf424a23153459ddcaff861300e5c2221/python_calamine-0.5.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:eea735f76e5a06efc91fe8907bca03741e71febcadd8621c6ea48df7b4a64be3", size = 1077823, upload-time = "2025-10-21T07:12:33.568Z" }, + { url = "https://files.pythonhosted.org/packages/6e/ce/8e9b85b7488488a7c3c673ae727ba6eb4c73f97d81acb250048f8e223196/python_calamine-0.5.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:2d138e5a960ae7a8fc91674252cf2d7387a5cef2892ebdccf3eea2756e1ced0c", size = 1150733, upload-time = "2025-10-21T07:12:35.097Z" }, + { url = "https://files.pythonhosted.org/packages/37/e0/ca4ad49b693d165b87de068ad78c9aca35a8657a5695cbcb212426e29bd9/python_calamine-0.5.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:8ad42673f5c0bb2d30b17b2ec3de5e8eae6dde4097650332c507b4146c63bb9c", size = 1080697, upload-time = "2025-10-21T07:12:36.679Z" }, + { url = "https://files.pythonhosted.org/packages/2a/62/1065dbf7c554bd80ba976d60278525750c0ff0feb56812f76b6531b67f21/python_calamine-0.5.4-cp314-cp314-win32.whl", hash = "sha256:36918496befbeeddc653e1499c090923dcf803d2633eb8bd473a9d21bdd06e79", size = 677184, upload-time = "2025-10-21T07:12:38.295Z" }, + { url = "https://files.pythonhosted.org/packages/e0/2f/f21bffb13712434168f7125f733fb728f723d79262a5acb90328a13fbf11/python_calamine-0.5.4-cp314-cp314-win_amd64.whl", hash = "sha256:bc01a7c03d302d11721a0ca00f67b71ebec125abab414f604bb03749b8c3557e", size = 722692, upload-time = "2025-10-21T07:12:39.764Z" }, + { url = "https://files.pythonhosted.org/packages/e9/b5/7214e8105b5165653cf49c9edec17db9d2551645be1a332bf09013908bc2/python_calamine-0.5.4-cp314-cp314-win_arm64.whl", hash = "sha256:8ab116aa7aea8bb3823f7a00c95bea08940db995556d287b6c1e51f3e83b3570", size = 686400, upload-time = "2025-10-21T07:12:41.371Z" }, + { url = "https://files.pythonhosted.org/packages/47/91/6815256d05940608c92e4d9467db04b9eab6124d8a9bd37f5c967157ead6/python_calamine-0.5.4-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:bc004d1da2779aea2b6782d18d977f8e1121e3a245c331db545f69fc2ae5cad0", size = 848400, upload-time = "2025-10-21T07:12:43.22Z" }, + { url = "https://files.pythonhosted.org/packages/0b/2c/fee8ffaac4a2385e9522c0f0febb690499a00fb99c0c953e7cd4bcdc6695/python_calamine-0.5.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5fb8c85acf5ccfe961023de01ce3a36839e310b5d9dc9aac9db01f350fbd3cec", size = 825000, upload-time = "2025-10-21T07:12:45.008Z" }, + { url = "https://files.pythonhosted.org/packages/a0/4d/61eeddde208958518cbf9ab76f387c379bd56019c029ea5fcc6cf3b96044/python_calamine-0.5.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e9dd48379eabc27c2bb73356fd5d1df48a46caf94433d4f60bdd38ad416a6f46", size = 896022, upload-time = "2025-10-21T07:12:46.503Z" }, + { url = "https://files.pythonhosted.org/packages/90/87/9ae23a3c2a7d2891c04436d0d7ed9984cb0f7145c96f6f8b36a345c7cc95/python_calamine-0.5.4-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:da3c2aa81de7cb20834b5326f326ba91a58123f10845864c3911e9dd819b9271", size = 887206, upload-time = "2025-10-21T07:12:48.446Z" }, + { url = "https://files.pythonhosted.org/packages/13/23/9289c350b8d7976295d01474f17a22fb9a42695dc403aa0f735a4e008791/python_calamine-0.5.4-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c9c09cd413e69f3366bdb73fc525c02963f29ca01da5a2ef9abed5486bba0e6a", size = 1042372, upload-time = "2025-10-21T07:12:50.04Z" }, + { url = "https://files.pythonhosted.org/packages/da/66/cd2c8ec4090d1cfd0875e7a45a7a7d55a9670b18daaad45845360d4def2c/python_calamine-0.5.4-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b678e11378b991e551d1260e21099cd9c5cffa4c83f816cba0aa05e9023d0f06", size = 941589, upload-time = "2025-10-21T07:12:51.635Z" }, + { url = "https://files.pythonhosted.org/packages/7d/d5/6a8199af0efe83945beb3df5a0556d658108cbf71b2cc449f3b5106afaef/python_calamine-0.5.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c7397781c4aedf70c5e4adcd31e2209035f4eb78fcb8ed887d252965e924530", size = 904284, upload-time = "2025-10-21T07:12:53.184Z" }, + { url = "https://files.pythonhosted.org/packages/93/0d/a419be4b036207ca61e5bbd15225f9637348a7c5c353d009ee0af5d38e90/python_calamine-0.5.4-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9062677c5c1ca9f16dd0d29875a9ffa841fe6b230a7c03b3ed92146fc42572fd", size = 945532, upload-time = "2025-10-21T07:12:54.692Z" }, + { url = "https://files.pythonhosted.org/packages/a1/eb/4b39fc8d42a13578b4cc695d0e1e84bd5d87087444c27f667e1d7e756f4f/python_calamine-0.5.4-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:0cd2881eadb30fddb84abe4fccb1544c6ba15aec45fe833a5691f5b0c8eeaec1", size = 1075965, upload-time = "2025-10-21T07:12:56.247Z" }, + { url = "https://files.pythonhosted.org/packages/eb/a5/d9d286986a192afd35056cbb53ca6979c09a584ca8ae9c2ab818141a9dde/python_calamine-0.5.4-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:6d077520c78530ad610fc1dc94463e618df8600d071409d8aa1bc195b9759f6f", size = 1150192, upload-time = "2025-10-21T07:12:58.236Z" }, + { url = "https://files.pythonhosted.org/packages/d9/2c/37612d97cf969adf39dbad04c14e8c35aedc8e6476b8e97cb5a5c2ed2b76/python_calamine-0.5.4-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:1ba09027e12a495b4e3eda4a7c59bb38d058e1941382bb2cc2e3a2a7bd12d3ba", size = 1078532, upload-time = "2025-10-21T07:13:00.123Z" }, + { url = "https://files.pythonhosted.org/packages/b1/2b/f6913d5cfc35c7d9c76df9fbabf00cbc5ddc525abc1e1dc55d5a57a059aa/python_calamine-0.5.4-cp314-cp314t-win_amd64.whl", hash = "sha256:a45f72a0ae0184c6ae99deefba735fdf82f858bcbf25caeb14366d45b18f23ea", size = 722451, upload-time = "2025-10-21T07:13:01.902Z" }, + { url = "https://files.pythonhosted.org/packages/88/0c/b6bf7a7033b0f0143e1494f0f6803f63ec8755dc30f054775434fe06d310/python_calamine-0.5.4-cp314-cp314t-win_arm64.whl", hash = "sha256:1ec345f20f0ea6e525e8d5a6dbb68065d374bc1feaf5bb479a93e2ed1d4db9ae", size = 684875, upload-time = "2025-10-21T07:13:03.308Z" }, +] + [[package]] name = "python-dateutil" version = "2.9.0.post0"