From 21f5d19b0e9f1c985ea7464f092605d0187b5966 Mon Sep 17 00:00:00 2001 From: Bento Machado Date: Sat, 29 Aug 2026 17:51:36 -0300 Subject: [PATCH 1/2] =?UTF-8?q?refactor(pii):=20remove=20PII=20de=20partic?= =?UTF-8?q?ipante=20n=C3=A3o=20utilizada=20do=20modelo=20de=20dados?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove cpf, phone, city e a geolocalização de check-in das entidades, do request em lote, da resposta tech-order, dos mappers e do repositório de participantes — nenhum desses campos é usado para produzir, entregar ou validar um certificado. Também remove participant_document (CPF) das respostas das APIs de consulta/listagem e os métodos de busca por cpf/cidade. Testes e README atualizados. --- README.md | 17 -------- src/application/dto/fetch_certificate_dto.py | 1 - .../dto/list_user_certificates_dto.py | 1 - src/application/list_user_certificates.py | 1 - src/application/mapper/tech_order.py | 18 +-------- .../strategy/fetch_certificate_strategy.py | 1 - src/domain/entity/certificate.py | 3 -- src/domain/entity/order.py | 5 --- src/domain/entity/participant.py | 6 +-- src/domain/entity/product.py | 3 -- .../repository/participant_repository.py | 19 +-------- src/domain/response/tech_floripa.py | 5 --- .../repository/participant_repository_impl.py | 39 ------------------- src/main/handler/certificate.py | 2 - .../http_types/create_certificates.py | 5 --- .../http_types/fetch_certificate.py | 1 - .../http_types/list_user_certificates.py | 1 - tests/test_dynamodb_repository_derivations.py | 11 ------ tests/test_list_user_certificates.py | 3 -- 19 files changed, 5 insertions(+), 137 deletions(-) diff --git a/README.md b/README.md index 94d293a..83e7bf9 100644 --- a/README.md +++ b/README.md @@ -66,17 +66,12 @@ Cria múltiplos certificados a partir de uma lista de dados recebida diretamente "first_name": "João", "last_name": "Silva", "email": "joao.silva@example.com", - "phone": "(48) 99999-9999", - "cpf": "123.456.789-00", - "city": "Florianópolis", "product_id": 5678, "product_name": "Workshop de Python Avançado", "certificate_details": "In recognition of their participation in the Workshop de Python Avançado, held on January 15, 2025, at IFSC – Câmpus Florianópolis, Brazil, with a total duration of 8 hours.", "certificate_logo": "https://example.com/logo.png", "certificate_background": "https://example.com/background.png", "order_date": "2025-01-10 14:30:00", - "checkin_latitude": "-27.5667", - "checkin_longitude": "-48.5156", "time_checkin": "2025-01-15 09:00:00" }, { @@ -84,17 +79,12 @@ Cria múltiplos certificados a partir de uma lista de dados recebida diretamente "first_name": "Maria", "last_name": "Santos", "email": "maria.santos@example.com", - "phone": "(48) 88888-8888", - "cpf": "987.654.321-00", - "city": "São José", "product_id": 5678, "product_name": "Workshop de Python Avançado", "certificate_details": "In recognition of their participation in the Workshop de Python Avançado, held on January 15, 2025, at IFSC – Câmpus Florianópolis, Brazil, with a total duration of 8 hours.", "certificate_logo": "https://example.com/logo.png", "certificate_background": "https://example.com/background.png", "order_date": "2025-01-10 14:35:00", - "checkin_latitude": "-27.5667", - "checkin_longitude": "-48.5156", "time_checkin": "2025-01-15 09:05:00" } ] @@ -105,9 +95,6 @@ Cria múltiplos certificados a partir de uma lista de dados recebida diretamente - `first_name`: Primeiro nome do participante (string) - `last_name`: Sobrenome do participante (string) - `email`: Email do participante (string) - - `phone`: Telefone do participante (string) - - `cpf`: CPF do participante (string, pode ser vazio) - - `city`: Cidade do participante (string) - `product_id`: ID do produto (integer) - `product_name`: Nome do produto (string) - `certificate_details`: Detalhes do certificado (string) @@ -115,8 +102,6 @@ Cria múltiplos certificados a partir de uma lista de dados recebida diretamente - `certificate_background`: URL do background do certificado (string) - `order_date`: Data do pedido no formato "YYYY-MM-DD HH:MM:SS" (string) - **Campos opcionais:** - - `checkin_latitude`: Latitude do check-in (string, opcional) - - `checkin_longitude`: Longitude do check-in (string, opcional) - `time_checkin`: Data e hora do check-in no formato "YYYY-MM-DD HH:MM:SS" (string, opcional) - **Nota:** Certificados sem `time_checkin` serão marcados como inválidos e não serão processados. - **Saída (sucesso):** @@ -155,7 +140,6 @@ Consulta certificados com base em diferentes critérios. "product_id": "integer", "participant_name": "string", "participant_email": "string", - "participant_document": "string", "certificate_url": "string", "created_at": "string", "updated_at": "string", @@ -196,7 +180,6 @@ Lista os certificados de um usuário a partir do e-mail informado no path. "product_id": 456, "participant_name": "string", "participant_email": "string", - "participant_document": "string", "certificate_url": "string", "created_at": "2025-01-20T10:30:45", "updated_at": "2025-01-20T10:30:45", diff --git a/src/application/dto/fetch_certificate_dto.py b/src/application/dto/fetch_certificate_dto.py index 019db27..f5dd169 100644 --- a/src/application/dto/fetch_certificate_dto.py +++ b/src/application/dto/fetch_certificate_dto.py @@ -25,7 +25,6 @@ class FetchCertificateResponseDto(BaseModel): product_id: Optional[int] = None participant_name: Optional[str] = None participant_email: Optional[str] = None - participant_document: Optional[str] = None certificate_url: Optional[str] = None certificate_key: Optional[str] = None created_at: Optional[str] = None diff --git a/src/application/dto/list_user_certificates_dto.py b/src/application/dto/list_user_certificates_dto.py index ff4b574..e3eb5dd 100644 --- a/src/application/dto/list_user_certificates_dto.py +++ b/src/application/dto/list_user_certificates_dto.py @@ -14,7 +14,6 @@ class UserCertificateItemDto(BaseModel): product_id: Optional[int] = None participant_name: Optional[str] = None participant_email: Optional[str] = None - participant_document: Optional[str] = None certificate_url: Optional[str] = None created_at: Optional[str] = None updated_at: Optional[str] = None diff --git a/src/application/list_user_certificates.py b/src/application/list_user_certificates.py index 052f853..fa0ee66 100644 --- a/src/application/list_user_certificates.py +++ b/src/application/list_user_certificates.py @@ -54,7 +54,6 @@ def _to_item_dto(self, certificate: Certificate) -> UserCertificateItemDto: product_id=certificate.product_id, participant_name=f"{certificate.participant_first_name or ''} {certificate.participant_last_name or ''}".strip(), participant_email=certificate.participant_email, - participant_document=certificate.participant_cpf, certificate_url=certificate.certificate_url, created_at=certificate.generated_date, updated_at=certificate.generated_date, diff --git a/src/application/mapper/tech_order.py b/src/application/mapper/tech_order.py index bec2ab5..881b0c8 100644 --- a/src/application/mapper/tech_order.py +++ b/src/application/mapper/tech_order.py @@ -13,16 +13,11 @@ def to_entity(tech_order_response: TechOrdersResponse) -> Order: participant_first_name=tech_order_response.first_name, participant_last_name=tech_order_response.last_name, participant_email=tech_order_response.email, - participant_phone=tech_order_response.phone, - participant_cpf=tech_order_response.cpf, - participant_city=tech_order_response.city, product_id=tech_order_response.product_id, product_name=tech_order_response.product_name, certificate_details=tech_order_response.certificate_details, certificate_logo=tech_order_response.certificate_logo, certificate_background=tech_order_response.certificate_background, - checkin_latitude=tech_order_response.checkin_latitude, - checkin_longitude=tech_order_response.checkin_longitude, time_checkin=tech_order_response.time_checkin ) @@ -35,9 +30,6 @@ def to_entity(tech_product_response: TechOrdersResponse) -> Product: certificate_details=tech_product_response.certificate_details, certificate_logo=tech_product_response.certificate_logo, certificate_background=tech_product_response.certificate_background, - checkin_latitude=tech_product_response.checkin_latitude, - checkin_longitude=tech_product_response.checkin_longitude, - time_checkin=tech_product_response.time_checkin ) class TechParticipantMapper: @@ -46,10 +38,7 @@ def to_entity(tech_order_response: TechOrdersResponse) -> Order: return Participant( first_name=tech_order_response.first_name, last_name=tech_order_response.last_name, - email=tech_order_response.email, - phone=tech_order_response.phone, - cpf=tech_order_response.cpf, - city=tech_order_response.city + email=tech_order_response.email ) class CertificateMapper: @@ -65,8 +54,5 @@ def to_entity(tech_order_response: TechOrdersResponse) -> Certificate: certificate_background=tech_order_response.certificate_background, participant_email=tech_order_response.email, participant_first_name=tech_order_response.first_name, - participant_last_name=tech_order_response.last_name, - participant_cpf=tech_order_response.cpf, - participant_phone=tech_order_response.phone, - participant_city=tech_order_response.city + participant_last_name=tech_order_response.last_name ) \ No newline at end of file diff --git a/src/application/strategy/fetch_certificate_strategy.py b/src/application/strategy/fetch_certificate_strategy.py index f9b293b..f6cb6ae 100644 --- a/src/application/strategy/fetch_certificate_strategy.py +++ b/src/application/strategy/fetch_certificate_strategy.py @@ -53,7 +53,6 @@ def execute(self, request: FetchCertificateRequestDto) -> List[FetchCertificateR product_id=certificate.product_id, participant_name=f"{certificate.participant_first_name or ''} {certificate.participant_last_name or ''}".strip(), participant_email=certificate.participant_email, - participant_document=certificate.participant_cpf, certificate_url=certificate.certificate_url, certificate_key=certificate.certificate_key, created_at=certificate.generated_date, diff --git a/src/domain/entity/certificate.py b/src/domain/entity/certificate.py index a38bc4a..67acaf3 100644 --- a/src/domain/entity/certificate.py +++ b/src/domain/entity/certificate.py @@ -18,6 +18,3 @@ class Certificate(BaseModel): participant_email: Optional[str] participant_first_name: Optional[str] participant_last_name: Optional[str] - participant_cpf: Optional[str] - participant_phone: Optional[str] - participant_city: Optional[str] diff --git a/src/domain/entity/order.py b/src/domain/entity/order.py index 3f03398..2569bf5 100644 --- a/src/domain/entity/order.py +++ b/src/domain/entity/order.py @@ -8,13 +8,8 @@ class Order(BaseModel): certificate_details: str certificate_logo: str certificate_background: str - checkin_latitude: str - checkin_longitude: str time_checkin: str participant_email: str participant_first_name: str participant_last_name: str - participant_cpf: str - participant_phone: str - participant_city: str diff --git a/src/domain/entity/participant.py b/src/domain/entity/participant.py index 8a15af5..19373dc 100644 --- a/src/domain/entity/participant.py +++ b/src/domain/entity/participant.py @@ -1,12 +1,8 @@ from pydantic import BaseModel, Field import uuid -from typing import Optional class Participant(BaseModel): id: uuid.UUID = Field(default_factory=uuid.uuid4) first_name: str last_name: str - email: str - phone: Optional[str] = None - cpf: Optional[str] = None - city: Optional[str] = None \ No newline at end of file + email: str \ No newline at end of file diff --git a/src/domain/entity/product.py b/src/domain/entity/product.py index 4353ca7..661a935 100644 --- a/src/domain/entity/product.py +++ b/src/domain/entity/product.py @@ -7,8 +7,5 @@ class Product(BaseModel): certificate_details: str certificate_logo: Optional[str] = None certificate_background: Optional[str] = None - checkin_latitude: Optional[str] = None - checkin_longitude: Optional[str] = None - time_checkin: Optional[str] = None \ No newline at end of file diff --git a/src/domain/repository/participant_repository.py b/src/domain/repository/participant_repository.py index 2027fd0..0ee1d9a 100644 --- a/src/domain/repository/participant_repository.py +++ b/src/domain/repository/participant_repository.py @@ -1,5 +1,5 @@ from abc import abstractmethod -from typing import List, Optional +from typing import Optional from src.domain.entity.participant import Participant from src.domain.repository.base_repository import BaseRepository @@ -13,23 +13,8 @@ class ParticipantRepository(BaseRepository[Participant]): def get_by_email(self, email: str) -> Optional[Participant]: """Busca participante por email""" pass - - @abstractmethod - def get_by_cpf(self, cpf: str) -> Optional[Participant]: - """Busca participante por CPF""" - pass - - @abstractmethod - def get_by_city(self, city: str) -> List[Participant]: - """Busca participantes por cidade""" - pass - + @abstractmethod def email_exists(self, email: str) -> bool: """Verifica se um email já existe""" pass - - @abstractmethod - def cpf_exists(self, cpf: str) -> bool: - """Verifica se um CPF já existe""" - pass diff --git a/src/domain/response/tech_floripa.py b/src/domain/response/tech_floripa.py index 4b3b004..c7942cf 100644 --- a/src/domain/response/tech_floripa.py +++ b/src/domain/response/tech_floripa.py @@ -6,17 +6,12 @@ class TechOrdersResponse(BaseModel): first_name: str last_name: str email: str - phone: str - cpf: str - city: str product_id: int product_name: str certificate_details: str certificate_logo: str certificate_background: str order_date: str - checkin_latitude: Optional[str] - checkin_longitude: Optional[str] time_checkin: Optional[str] diff --git a/src/infrastructure/repository/participant_repository_impl.py b/src/infrastructure/repository/participant_repository_impl.py index a299a48..e5ad7dd 100644 --- a/src/infrastructure/repository/participant_repository_impl.py +++ b/src/infrastructure/repository/participant_repository_impl.py @@ -132,36 +132,6 @@ def get_by_email(self, email: str) -> Optional[Participant]: logger.error(f"Erro ao buscar participante por email {email}: {str(e)}") raise - def get_by_cpf(self, cpf: str) -> Optional[Participant]: - try: - items = self.dynamodb_service.query_table( - self.table_name, - "cpf = :cpf", - {":cpf": cpf}, - index_name="participants_by_cpf_idx", - ) - if items: - return Participant(**items[0]) - return None - - except Exception as e: - logger.error(f"Erro ao buscar participante por CPF {cpf}: {str(e)}") - raise - - def get_by_city(self, city: str) -> List[Participant]: - try: - items = self.dynamodb_service.query_table( - self.table_name, - "city = :city", - {":city": city}, - index_name="participants_by_city_idx", - ) - return [Participant(**item) for item in items] - - except Exception as e: - logger.error(f"Erro ao buscar participantes por cidade {city}: {str(e)}") - raise - def email_exists(self, email: str) -> bool: try: participant = self.get_by_email(email) @@ -170,12 +140,3 @@ def email_exists(self, email: str) -> bool: except Exception as e: logger.error(f"Erro ao verificar existência do email {email}: {str(e)}") return False - - def cpf_exists(self, cpf: str) -> bool: - try: - participant = self.get_by_cpf(cpf) - return participant is not None - - except Exception as e: - logger.error(f"Erro ao verificar existência do CPF {cpf}: {str(e)}") - return False diff --git a/src/main/handler/certificate.py b/src/main/handler/certificate.py index faf25ba..0386662 100644 --- a/src/main/handler/certificate.py +++ b/src/main/handler/certificate.py @@ -84,7 +84,6 @@ def fetch_certificate_handler(request: FetchCertificateRequest) -> List[FetchCer product_id=app_response.product_id, participant_name=app_response.participant_name, participant_email=app_response.participant_email, - participant_document=app_response.participant_document, certificate_url=app_response.certificate_url, created_at=app_response.created_at, updated_at=app_response.updated_at, @@ -138,7 +137,6 @@ def list_user_certificates_handler( product_id=item.product_id, participant_name=item.participant_name, participant_email=item.participant_email, - participant_document=item.participant_document, certificate_url=item.certificate_url, created_at=item.created_at, updated_at=item.updated_at, diff --git a/src/main/presentation/http_types/create_certificates.py b/src/main/presentation/http_types/create_certificates.py index c5bafab..4cf4826 100644 --- a/src/main/presentation/http_types/create_certificates.py +++ b/src/main/presentation/http_types/create_certificates.py @@ -8,17 +8,12 @@ class CertificateItemRequest(BaseModel): first_name: str last_name: str email: str - phone: str - cpf: str - city: str product_id: int product_name: str certificate_details: str certificate_logo: str certificate_background: str order_date: str - checkin_latitude: Optional[str] = None - checkin_longitude: Optional[str] = None time_checkin: Optional[str] = None diff --git a/src/main/presentation/http_types/fetch_certificate.py b/src/main/presentation/http_types/fetch_certificate.py index c555d13..b662e3f 100644 --- a/src/main/presentation/http_types/fetch_certificate.py +++ b/src/main/presentation/http_types/fetch_certificate.py @@ -14,7 +14,6 @@ class FetchCertificateResponse(BaseModel): product_id: Optional[int] = None participant_name: Optional[str] = None participant_email: Optional[str] = None - participant_document: Optional[str] = None certificate_url: Optional[str] = None created_at: Optional[str] = None updated_at: Optional[str] = None diff --git a/src/main/presentation/http_types/list_user_certificates.py b/src/main/presentation/http_types/list_user_certificates.py index 96e7761..6a1cbd4 100644 --- a/src/main/presentation/http_types/list_user_certificates.py +++ b/src/main/presentation/http_types/list_user_certificates.py @@ -15,7 +15,6 @@ class UserCertificateItemResponse(BaseModel): product_id: Optional[int] = None participant_name: Optional[str] = None participant_email: Optional[str] = None - participant_document: Optional[str] = None certificate_url: Optional[str] = None created_at: Optional[str] = None updated_at: Optional[str] = None diff --git a/tests/test_dynamodb_repository_derivations.py b/tests/test_dynamodb_repository_derivations.py index 469e023..0d91582 100644 --- a/tests/test_dynamodb_repository_derivations.py +++ b/tests/test_dynamodb_repository_derivations.py @@ -58,9 +58,6 @@ def test_certificate_prepare_item_adds_query_keys(self): participant_email=" User+Test@Example.com ", participant_first_name="User", participant_last_name="One", - participant_cpf="12345678900", - participant_phone="48999999999", - participant_city="Florianopolis", ) item = repository._prepare_item(certificate) @@ -80,15 +77,10 @@ def test_order_prepare_item_adds_month_and_sort_key(self): certificate_details="Detalhes", certificate_logo="logo.png", certificate_background="background.png", - checkin_latitude="-27.0", - checkin_longitude="-48.0", time_checkin="13:00", participant_email=" Test@Example.com ", participant_first_name="Test", participant_last_name="User", - participant_cpf="12345678900", - participant_phone="48999999999", - participant_city="Florianopolis", ) item = repository._prepare_item(order) @@ -105,9 +97,6 @@ def test_product_prepare_item_adds_boolean_flags(self): certificate_details="Detalhes", certificate_logo="logo.png", certificate_background=None, - checkin_latitude=None, - checkin_longitude=None, - time_checkin=None, ) item = repository._prepare_item(product) diff --git a/tests/test_list_user_certificates.py b/tests/test_list_user_certificates.py index 7cb4eef..70114f0 100644 --- a/tests/test_list_user_certificates.py +++ b/tests/test_list_user_certificates.py @@ -126,9 +126,6 @@ def _certificate(self, generated_date, success, order_id): participant_email=self.email, participant_first_name="User", participant_last_name=str(order_id), - participant_cpf="12345678900", - participant_phone="48999999999", - participant_city="Florianopolis", ) From c5f4e6afac9f990864e8177658696f82f6feadbd Mon Sep 17 00:00:00 2001 From: Bento Machado Date: Sat, 29 Aug 2026 18:05:31 -0300 Subject: [PATCH 2/2] feat(pii): adiciona script de limpeza de PII no DynamoDB MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adiciona scripts/scrub_pii.py para remover os atributos de PII já armazenados nos itens existentes (cpf, phone, city e geolocalização de check-in), com os nomes de atributo específicos de cada tabela (participants, orders, certificates). Roda em dry-run por padrão e exige --apply mais o nome explícito de cada tabela — não há default apontando para produção. Deve ser executado após a remoção dos índices cpf/city. Inclui testes da montagem da expressão REMOVE. --- scripts/scrub_pii.py | 211 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 211 insertions(+) create mode 100644 scripts/scrub_pii.py diff --git a/scripts/scrub_pii.py b/scripts/scrub_pii.py new file mode 100644 index 0000000..18f1d25 --- /dev/null +++ b/scripts/scrub_pii.py @@ -0,0 +1,211 @@ +""" +Limpeza (scrub) de PII já armazenada em itens existentes do DynamoDB. + +Remove os atributos de PII que não são mais usados para gerar, entregar ou +validar um certificado. Os nomes dos atributos diferem por tabela: + + - participants: cpf, phone, city + - orders: participant_cpf, participant_phone, participant_city, + checkin_latitude, checkin_longitude + - certificates: participant_cpf, participant_phone, participant_city + +Segurança: + - Roda em DRY-RUN por padrão (apenas mostra o que faria, não escreve nada). + - Para aplicar de verdade é preciso passar --apply E o nome explícito de cada + tabela. NÃO existe default apontando para produção. + - Deve ser executado DEPOIS de remover os índices cpf/city (ver + py-certify-infra), pois um GSI exige que o atributo-chave exista nos itens. + +Uso: + # dry-run (padrão) — só participants + python scripts/scrub_pii.py --participants-table + + # aplicar de verdade nas três tabelas + python scripts/scrub_pii.py \ + --participants-table \ + --orders-table \ + --certificates-table \ + --apply +""" + +from __future__ import annotations + +import argparse +import logging +import os +import sys +from typing import Optional + +logger = logging.getLogger("scrub_pii") + +# Atributos de PII a remover, por entidade/tabela. +ATTRS_POR_ENTIDADE: dict[str, list[str]] = { + "participants": ["cpf", "phone", "city"], + "orders": [ + "participant_cpf", + "participant_phone", + "participant_city", + "checkin_latitude", + "checkin_longitude", + ], + "certificates": ["participant_cpf", "participant_phone", "participant_city"], +} + + +def montar_remocao( + item: dict, atributos_alvo: list[str] +) -> Optional[tuple[str, dict[str, str]]]: + """ + Monta a UpdateExpression de REMOVE apenas para os atributos-alvo que + realmente existem no item. Usa ExpressionAttributeNames para evitar + conflito com palavras reservadas do DynamoDB. + + Retorna (update_expression, expression_attribute_names) ou None se o item + não tiver nenhum dos atributos-alvo. + """ + presentes = [attr for attr in atributos_alvo if attr in item] + if not presentes: + return None + + nomes = {f"#a{i}": attr for i, attr in enumerate(presentes)} + update_expression = "REMOVE " + ", ".join(nomes.keys()) + return update_expression, nomes + + +def _chaves_da_tabela(client, table_name: str) -> list[str]: + """Retorna os nomes das colunas-chave (hash/range) da tabela.""" + descricao = client.describe_table(TableName=table_name) + return [k["AttributeName"] for k in descricao["Table"]["KeySchema"]] + + +def _projecao(chaves: list[str], atributos_alvo: list[str]) -> tuple[str, dict[str, str]]: + """ + Monta uma ProjectionExpression que busca apenas as chaves + os atributos-alvo + (evita puxar o item inteiro, minimizando o manuseio de PII). + """ + campos = list(dict.fromkeys([*chaves, *atributos_alvo])) + nomes = {f"#p{i}": campo for i, campo in enumerate(campos)} + return ", ".join(nomes.keys()), nomes + + +def limpar_tabela( + client, table_name: str, entidade: str, aplicar: bool, limite: Optional[int] +) -> tuple[int, int]: + """ + Varre a tabela e remove os atributos de PII dos itens que os possuem. + + Retorna (itens_varridos, itens_afetados). + """ + atributos_alvo = ATTRS_POR_ENTIDADE[entidade] + chaves = _chaves_da_tabela(client, table_name) + proj_expr, proj_nomes = _projecao(chaves, atributos_alvo) + + varridos = 0 + afetados = 0 + paginator = client.get_paginator("scan") + for pagina in paginator.paginate( + TableName=table_name, + ProjectionExpression=proj_expr, + ExpressionAttributeNames=proj_nomes, + ): + for item_dynamo in pagina.get("Items", []): + varridos += 1 + # item_dynamo vem no formato low-level ({"attr": {"S": "..."}}). Para + # decidir a presença basta olhar as chaves do dict. + remocao = montar_remocao(item_dynamo, atributos_alvo) + if remocao is None: + continue + + afetados += 1 + update_expression, nomes = remocao + chave = {k: item_dynamo[k] for k in chaves} + removidos = list(nomes.values()) + + if aplicar: + client.update_item( + TableName=table_name, + Key=chave, + UpdateExpression=update_expression, + ExpressionAttributeNames=nomes, + ) + logger.info("[%s] removido %s de %s", table_name, removidos, chave) + else: + logger.info( + "[dry-run][%s] removeria %s de %s", table_name, removidos, chave + ) + + if limite is not None and varridos >= limite: + return varridos, afetados + + return varridos, afetados + + +def main(argv: Optional[list[str]] = None) -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--participants-table") + parser.add_argument("--orders-table") + parser.add_argument("--certificates-table") + parser.add_argument("--region", default=os.environ.get("AWS_REGION", "us-east-1")) + parser.add_argument( + "--apply", + action="store_true", + help="Aplica de verdade. Sem esta flag roda em dry-run.", + ) + parser.add_argument( + "--limit", type=int, default=None, help="Para após N itens (para testes)." + ) + args = parser.parse_args(argv) + + logging.basicConfig(level=logging.INFO, format="%(message)s") + + alvos = { + "participants": args.participants_table, + "orders": args.orders_table, + "certificates": args.certificates_table, + } + alvos = {entidade: nome for entidade, nome in alvos.items() if nome} + + if not alvos: + parser.error( + "informe ao menos uma tabela (--participants-table / --orders-table " + "/ --certificates-table). Não há default apontando para produção." + ) + + if args.apply: + logger.warning( + "*** MODO --apply: alterações serão gravadas em %s ***", list(alvos.values()) + ) + else: + logger.info("Modo DRY-RUN (nada será gravado). Use --apply para efetivar.") + + import boto3 # import tardio: mantém as funções puras testáveis sem boto3 + + client = boto3.client("dynamodb", region_name=args.region) + + total_varridos = 0 + total_afetados = 0 + for entidade, table_name in alvos.items(): + varridos, afetados = limpar_tabela( + client, table_name, entidade, args.apply, args.limit + ) + logger.info( + "%s: %d itens varridos, %d itens %s", + table_name, + varridos, + afetados, + "atualizados" if args.apply else "seriam atualizados", + ) + total_varridos += varridos + total_afetados += afetados + + logger.info( + "TOTAL: %d varridos, %d %s", + total_varridos, + total_afetados, + "atualizados" if args.apply else "seriam atualizados", + ) + return 0 + + +if __name__ == "__main__": + sys.exit(main())