Em um ambiente de produção, é importante garantir a segurança dos dados sensíveis, como as informações de cartões de crédito. Uma das formas de realizar isso é utilizando criptografia para armazenar esses dados de forma que, mesmo que o banco de dados seja comprometido, as informações não possam ser acessadas ou lidas facilmente.

Uma abordagem comum para isso é o uso de uma chave de criptografia, que pode ser armazenada em um serviço externo, que ofereça rotação de chaves, ou gerada no momento da inicialização, dependendo das necessidades de segurança do negócio. No exemplo a seguir, vamos demonstrar como criptografar e descriptografar informações sensíveis de cartões de crédito usando o módulo de segurança.

Primeiramente, é necessário criar funções para criptografar e descriptografar os dados do cartão de crédito. A criptografia pode ser feita utilizando a biblioteca de criptografia Fernet, que oferece criptografia simétrica, o que significa que a mesma chave é usada tanto para criptografar quanto para descriptografar os dados.

python
def encrypt_credit_card_info(card_info: str) -> str:
return cypher_suite.encrypt(card_info.encode()).decode() def decrypt_credit_card_info(encrypted_card_info: str) -> str: return cypher_suite.decrypt(encrypted_card_info.encode()).decode()

Essas funções podem ser utilizadas ao gravar e ler informações de cartão de crédito no banco de dados. Para armazenar essas informações de forma segura, o próximo passo é criar uma operação de armazenamento no módulo de segurança. O código a seguir mostra como armazenar um número de cartão de crédito e seu CVV de forma criptografada:

python
async def store_credit_card_info( db_session: AsyncSession, card_number: str, card_holder_name: str, expiration_date: str, cvv: str ): encrypted_card_number = encrypt_credit_card_info(card_number) encrypted_cvv = encrypt_credit_card_info(cvv) credit_card = CreditCard( number=encrypted_card_number, card_holder_name=card_holder_name, expiration_date=expiration_date, cvv=encrypted_cvv, ) async with db_session.begin(): db_session.add(credit_card) await db_session.flush() credit_card_id = credit_card.id await db_session.commit() return credit_card_id

Cada vez que essa função for chamada, os dados de cartão de crédito serão armazenados de forma segura, com os números do cartão e o CVV criptografados. Para recuperar essas informações, é necessário descriptografá-las antes de exibi-las ou usá-las.

A função a seguir mostra como buscar as informações de cartão de crédito no banco de dados e descriptografá-las:

python
async def retrieve_credit_card_info(
db_session: AsyncSession, credit_card_id: int ): query = select(CreditCard).where(CreditCard.id == credit_card_id) async with db_session as session: result = await session.execute(query) credit_card = result.scalars().first() credit_card_number = decrypt_credit_card_info(credit_card.number) cvv = decrypt_credit_card_info(credit_card.cvv) card_holder = credit_card.card_holder_name expiry = credit_card.expiration_date return { "card_number": credit_card_number, "card_holder_name": card_holder, "expiration_date": expiry, "cvv": cvv }

Essas funções básicas permitem que informações sensíveis sejam armazenadas e recuperadas de forma segura em um banco de dados SQL, utilizando criptografia para garantir a privacidade dos dados. Para completar a implementação, recomenda-se a criação de testes unitários para verificar a segurança do armazenamento e recuperação dos dados de cartão de crédito.

Um exemplo de exercício seria escrever testes unitários que garantam que os dados de cartão de crédito estão sendo armazenados de forma segura, com os números de cartão e o CVV devidamente criptografados. Esses testes podem ser realizados

Como Gerenciar Relacionamentos em Bancos de Dados NoSQL Usando MongoDB

No desenvolvimento de sistemas modernos, a escolha do banco de dados é crucial para determinar como as informações serão armazenadas e acessadas. Em particular, os bancos de dados NoSQL, como o MongoDB, oferecem flexibilidade e desempenho para cenários que envolvem dados semi-estruturados ou em constante evolução. No entanto, a ausência de relacionamentos explícitos, como chaves estrangeiras ou junções, representa um desafio significativo ao modelar dados relacionados. Nesse contexto, é essencial entender as duas principais abordagens para gerenciar relacionamentos: embedding (incorporação) e referencing (referência).

Quando lidamos com dados relacionados no MongoDB, temos basicamente duas opções: embutir os dados dentro de um único documento ou armazenar uma referência ao outro documento. A escolha entre essas abordagens depende de fatores como a estrutura do banco de dados, a frequência das atualizações e as necessidades de consulta.

Embedding: Armazenamento de Dados Relacionados no Mesmo Documento

A abordagem de embedding é útil quando as entidades relacionadas são frequentemente acessadas juntas e a consistência dos dados é importante. Nesse caso, os dados de um documento são armazenados dentro de outro, formando um relacionamento dentro do mesmo objeto. Isso é particularmente vantajoso para dados que não mudam com frequência e para otimizar o desempenho de leitura em operações de alta frequência.

Um exemplo típico de embedding pode ser encontrado na música e no álbum em uma plataforma de streaming. Ao armazenar informações de um álbum diretamente dentro de um documento de música, evitamos consultas complexas e melhoramos a performance nas leituras. No entanto, esse modelo tem suas limitações. Por exemplo, se o álbum mudar, seria necessário atualizar todos os documentos de músicas associadas a ele, o que pode causar inconsistências e sobrecarga no banco de dados se houver muitos dados.

Um exemplo de como isso poderia ser implementado no MongoDB seria:

json
{
"title": "Título da Música", "artist": "Nome do Artista", "genre": "Gênero Musical", "album": { "title": "Título do Álbum", "release_year": 2017 } }

Nessa estrutura, a informação sobre o álbum está embutida diretamente no documento da música. Quando uma música é criada, os dados do álbum são adicionados de forma imediata e o MongoDB lida com a persistência do documento.

Referencing: Relacionamentos com IDs de Referência

Por outro lado, a abordagem de referencing se aplica quando as entidades relacionadas são mais independentes e podem ser reutilizadas em vários contextos. Essa abordagem é adequada para situações em que os dados relacionados podem ser alterados frequentemente ou compartilhados entre várias instâncias. Em vez de duplicar os dados, armazenamos uma referência, como um ID, ao documento relacionado.

Esse modelo é amplamente utilizado para cenários onde se tem uma relação muitos-para-muitos, como no caso de playlists e músicas. Em uma playlist, várias músicas podem ser referenciadas e, além disso, uma música pode aparecer em várias playlists. Para implementar essa abordagem, criamos um documento de playlist que mantém apenas referências aos documentos de música, em vez de incorporar os dados de cada música diretamente.

No MongoDB, um exemplo de como isso pode ser estruturado seria o seguinte:

json
{
"name": "Minha Playlist", "songs": ["song_id1", "song_id2", "song_id3"] }

Neste exemplo, a playlist contém uma lista de IDs de músicas. Para obter todas as músicas de uma playlist, seria necessário fazer uma consulta separada ao banco para recuperar os documentos de música correspondentes aos IDs armazenados na playlist. Embora esse modelo adicione complexidade ao processo de leitura, ele proporciona uma flexibilidade muito maior, pois as músicas podem ser facilmente atualizadas ou compartilhadas entre várias playlists sem duplicação de dados.

Performance e Flexibilidade: Embedding vs. Referencing

A principal vantagem do embedding é a eficiência em leituras rápidas, já que todos os dados necessários estão contidos em um único documento. Isso reduz a complexidade das consultas e melhora a performance quando é necessário acessar os dados rapidamente. Porém, quando os dados embutidos se tornam grandes ou são atualizados com frequência, pode haver problemas com limitações de tamanho de documento e a necessidade de atualizar múltiplos documentos.

Por outro lado, o referencing oferece maior flexibilidade, especialmente em cenários com dados que são usados em diferentes contextos ou atualizados com frequência. Embora as consultas sejam mais complexas (envolvendo mais de uma operação para recuperar dados relacionados), essa abordagem é mais escalável, pois permite que os dados sejam mantidos em documentos independentes, que podem ser alterados de forma isolada sem afetar outras instâncias.

Integrando o FastAPI com MongoDB

No contexto de APIs modernas, como as construídas com FastAPI, o MongoDB pode ser integrado de forma simples e eficiente para gerenciar dados. Ao implementar endpoints que interagem com o banco de dados, podemos permitir a criação, leitura, atualização e exclusão de documentos. Por exemplo, ao atualizar informações sobre uma música, o FastAPI pode interagir diretamente com o MongoDB para modificar apenas os campos desejados, sem a necessidade de modificar outros dados no banco.

Quando utilizamos essas duas abordagens, embedding e referencing, em conjunto com uma plataforma como o FastAPI, podemos criar sistemas robustos e eficientes para manipulação de dados relacionados. No entanto, é essencial entender as implicações de desempenho e flexibilidade de cada abordagem, bem como escolher a mais adequada para o cenário do projeto.

Além disso, ao trabalhar com MongoDB, sempre é importante considerar as limitações e boas práticas do banco de dados, como o tamanho máximo de documentos e a forma como as consultas podem ser otimizadas para reduzir a carga de leitura.

Como a Injeção de Dependência Melhora a Modularidade e Testabilidade em FastAPI

O uso de injeção de dependência em frameworks modernos como FastAPI é uma prática essencial para a construção de aplicações escaláveis e de fácil manutenção. FastAPI, sendo um framework baseado em Python, permite o gerenciamento eficiente de dependências por meio do objeto Depends, possibilitando que funções e serviços externos sejam injetados diretamente nos endpoints de maneira modular. Ao utilizar o conceito de injeção de dependência, conseguimos isolar funcionalidades específicas, melhorar a legibilidade do código e facilitar os testes.

Em FastAPI, a injeção de dependência é realizada por meio de funções que retornam valores necessários para o processamento de uma solicitação. O exemplo mais básico envolve a criação de um endpoint que depende de parâmetros de consulta, como datas de início e fim. O objeto Query, que é utilizado para gerenciar parâmetros de consulta, pode ser integrado facilmente com a função Depends. Isso permite que a aplicação tenha parâmetros definidos de maneira explícita, sem necessidade de repetições no corpo do código. A seguir, temos um exemplo básico de como a injeção de dependência é configurada:

python
from fastapi import Depends
@app.get("/v1/trips") def get_tours(time_range: Annotated[time_range, Depends()]): start, end = time_range message = f"Request trips from {start}" if end: return f"{message} to {end}" return message

Aqui, o parâmetro time_range é resolvido antes que a função get_tours seja executada, facilitando a reutilização da lógica de obtenção do intervalo de datas.

Ao desenvolver a aplicação, a possibilidade de substituir ou ajustar a lógica das dependências de maneira simples, como no caso de testes, torna-se uma grande vantagem. No exemplo a seguir, mostramos como a injeção de dependência pode ser facilmente sobreposta durante a execução de testes, usando o dependency_overrides para garantir que a lógica de produção não interfira nas verificações específicas dos testes:

python
from datetime import date from fastapi.testclient import TestClient from app.main import app def test_get_v1_trips_endpoint(): client = TestClient(app) app.dependency_overrides[time_range] = lambda: (date.fromisoformat("2024-02-01"), None) response = client.get("/v1/trips") assert response.json() == "Request trips from 2024-02-01"

Esse mecanismo de substituição de dependências é útil para garantir que a lógica de testes não afete a base de dados de produção ou outros serviços externos. Além disso, permite simular comportamentos complexos, como a verificação de lógica de negócios, sem a necessidade de interagir com a infraestrutura real.

A principal vantagem do uso de injeção de dependência em FastAPI reside na capacidade de modularizar o código. Dependências podem ser reutilizadas em diversos endpoints, tornando o código mais limpo e menos redundante. Além disso, a abordagem facilita a manutenção e expansão da aplicação, já que mudanças em uma dependência são centralizadas, impactando apenas a parte da aplicação que utiliza essa dependência.

Outro aspecto importante que vale a pena considerar é o uso de dependências aninhadas. FastAPI permite criar dependências que, por sua vez, dependem de outras dependências, formando uma hierarquia que pode ser resolvida automaticamente. Isso é particularmente útil quando se trabalha com serviços que exigem múltiplos parâmetros, como autenticação e validação, além da lógica de negócios.

No caso de uma aplicação que precisa lidar com diferentes tipos de viagens, como cruzeiros, viagens curtas ou estadias em resorts, podemos criar uma função de dependência para selecionar a categoria da viagem e, ao mesmo tempo, verificar a validade de um cupom de desconto para essa categoria. O uso de dependências aninhadas torna o código ainda mais modular, ao garantir que cada função seja responsável apenas por uma parte da lógica de negócios.

python
@app.get("/v2/trips/{category}") def get_trips_by_category( category: Annotated[str, Depends(select_category)], discount_applicable: Annotated[bool, Depends(check_coupon_validity)], ): category = category.replace("-", " ").title() message = f"You requested {category} trips." if discount_applicable: message += "\nThe coupon code is valid! You will get a discount!" return message

Ao utilizar dependências dessa maneira, o FastAPI não só resolve de forma eficiente as dependências, mas também permite que o código permaneça limpo e fácil de testar, já que as dependências podem ser facilmente substituídas ou simuladas durante os testes.

Além disso, a injeção de dependências permite a separação de responsabilidades de forma clara. Em vez de escrever código repetido para validar parâmetros ou configurar lógicas de autenticação, essas responsabilidades podem ser abstraídas para funções independentes, que são injetadas quando necessário. Isso contribui para uma arquitetura mais limpa, onde cada componente é responsável apenas por uma parte específica do processamento da requisição.

Outro aspecto relevante é o uso do Annotated do módulo typing, que melhora a legibilidade e organização do código. Com ele, podemos fornecer mais informações sobre os parâmetros das dependências, como descrições e valores possíveis, o que resulta em documentação automática e mais completa gerada pelo FastAPI. A prática de utilizar Annotated está em evolução dentro do FastAPI, mas já oferece um meio eficiente para lidar com dependências e seus metadados.

Por fim, a capacidade de escrever código assíncrono ou síncrono dentro das dependências oferece flexibilidade. Em contextos em que a dependência precisa realizar operações que exigem I/O, como acessar um banco de dados ou fazer chamadas externas, é possível usar a palavra-chave async def, permitindo que o FastAPI gerencie automaticamente a execução assíncrona, sem necessidade de intervenções manuais.