add datetable cnfcnpj

This commit is contained in:
2025-11-24 11:45:53 -03:00
parent 5b1ba9e9c7
commit 78ad183e61
29 changed files with 828 additions and 255 deletions

View File

@@ -0,0 +1,207 @@
import csv
import secrets
from io import StringIO
from types import SimpleNamespace
from typing import TYPE_CHECKING
from uuid import uuid4
from aws_lambda_powertools.utilities.data_classes import (
EventBridgeEvent,
event_source,
)
from aws_lambda_powertools.utilities.typing import LambdaContext
from layercake.batch import BatchProcessor
from layercake.dateutils import now
from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair
from layercake.extra_types import CnpjStr, CpfStr, NameStr
from pydantic import BaseModel, EmailStr, Field
from boto3clients import dynamodb_client, s3_client
from config import USER_TABLE
if TYPE_CHECKING:
from mypy_boto3_s3.client import S3Client
else:
S3Client = object
dyn = DynamoDBPersistenceLayer(USER_TABLE, dynamodb_client)
transport_params = {'client': s3_client}
processor = BatchProcessor()
class Org(BaseModel):
id: str | None = Field(default=None, exclude=True)
name: str
cnpj: CnpjStr
class User(BaseModel):
name: NameStr
cpf: CpfStr
email: EmailStr
class CPFConflictError(Exception): ...
class EmailConflictError(Exception): ...
@event_source(data_class=EventBridgeEvent)
def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> 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'))

View File

@@ -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

View File

@@ -0,0 +1,14 @@
from aws_lambda_powertools.utilities.data_classes import (
EventBridgeEvent,
event_source,
)
from aws_lambda_powertools.utilities.typing import LambdaContext
from boto3clients import s3_client
transport_params = {'client': s3_client}
@event_source(data_class=EventBridgeEvent)
def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool:
return True

View File

@@ -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'))

View File

@@ -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)

View File

@@ -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

View File

@@ -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'])

View File

@@ -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

View File

@@ -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

View File

@@ -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"}}
{"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"}

View File

@@ -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)]

65
users-events/uv.lock generated
View File

@@ -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"