Criando uma API Pronta para Produção com FastAPI - PT.5

Nos Capítulos Anteriores …
Como dito anteriormente, hoje vamos configurar o SQLAlchemy!!
Essa é a parte 5 do nosso projeto do EconoWallet e se você quiser verificar o que já fizemos até o momento, acesse os links abaixo:
Sem enrolação, vamos logo ao que interessa!!!
Reorganizando o projeto
Antes de configurar o ORM, vamos ajustar algumas coisas no nosso diretório. Como estamos seguindo para um estágio mais maduro da aplicação e que você já compreende o básico de configuração do FastAPI, precisaremos agora separar melhor as competências dentro do projeto.
|
Veja que agora nós temos alguns diretórios novos✨ que irão comportar diferentes lógicas no nosso projeto:
- api: todas as nossas rotas da api.
- database: toda a lógica de configuração do SQLAlchemy.
- models: toda a especificação de como deverão ser nossas tabelas.
Nós também adicionamos um arquivo chamado status.py
, esse irá conter a rota de status da nossa aplicação, que antes estava no nosso arquivo main.py
. Veja como está nosso main.py
e nosso status.py
:
|
Aqui nós não configuramos mais diretamente as rotas no main.py
, elas agora ficam em diretórios separados, e sempre na inicialização da aplicação é carregado todas as rotas ao chamar a função create_application
. Dentro dessa função nós adicionamos um include_router
que faz algumas coisas interessantes:
- inclui todas as rotas de um determinado arquivo.
- adiciona uma tag para separar melhor no contexto de exibição no swager.
- adiciona um prefixo à rota: eu normalmente utilizo
/api/v1
pelo motivo de que fica fácil alterar para uma versão 2 caso a mesma venha a existir.
Um decorator muito legal do FastAPI é @app.on_event
, onde você pode configurar métodos que irão rodar sempre que a aplicação inicializar ou finalizar. Iremos adicionar mais coisas nesse event, mas por enquanto apenas estamos printando um log no console.
|
Veja que aqui nós temos uma modificação da rota /ping para /status e ela é definida por @router.get e não mais @app.get. Eu adicionei também uma variável chamada APP_VERSION
que é responsável por manter atualizado o registro da verssão da aplicação.
Executamos assim nosso workflow de teste, para saber se tudo está funcionando:
|
Ao acessar http://127.0.0.1:8004/docs/ você deverá ver uma tela igual a de baixo:
E nossa árvore de diretórios deve estar como no seguinte snippet:
|
Configurando o SQLAlchemy no Projeto
No intuito de ter uma tabela inicial bem simples no nosso banco de dados, apenas para iniciar a configuração do SQLAlchemy, vamos iniciar o processo criando um model. Como dito no post anterior, estamos no que estou chamando de primeiro cenário do SQLAlchemy, onde precisamos da classe declarative_base
antes de iniciar a configuração do model.
Antes de tudo, primeiro iremos instalar o SQLAlchemy: pipenv install sqlalchemy==1.4.20
. Agora, vamos criar dois novos arquivos:
|
O modelbase.py
é onde eu normalmente coloco a declarative_base
, sendo assim, é desse arquivo que nossos models irão buscar a classe mãe.
|
Podemos agora criar o model. Um model é uma abstração de tudo que você faria se estivesse criando uma tabela diretamente no banco, com a diferença de que aqui a gente tem uma abordagem python-like. Cada tipo de ORM tem uma sintaxe própria, e no SQLAlchemy a gente tem a configuração como no arquivo abaixo:
|
Pronto, nosso primeiro model está criado. Como você pode ver, é basicamente uma classe que herda da declarative_base
, e nosso dever é configurar como vai ser cada coluna da tabela. Diferentemente de outros ORMs como o django
, aqui nós precisamos explicitamente determinar a coluna de id.
Para as outras colunas, nós temos formato de string e data. Sendo que nenhuma delas podem ser atribuídos valores nulos, ou seja, são campos obrigatórios da nossa tabela. Veja que eu já estou adicionando alguns indexes, que basicamente permitem que a consulta à essas colunas seja feita de forma mais rápida.
Até agora nós já temos alguns elementos do primeiro cenário do SQLAlchemy, porém, para criar as tabelas no banco e expor uma session, nós iremos precisar criar um novo arquivo: touch project/app/database/database_session.py
.
|
Calma, vou traduzir o que está acontecendo aqui:
- Criamos uma função para inicializar uma variável global chamada
__factory
, essa é responsável por expor uma conexão com o banco para toda a aplicação. - Por meio do
create_session()
essa variável é acessada e a session é enfim estabelecida. - Setamos uma forma padrão de acessar essa session por meio da função
get_db()
, que expõe a session e garante que a mesma vai ser finalizada (finaly
), aconteça o que acontecer.
Eu ainda não comentei sobre variáveis de ambiente, mas vai ser algo que precisaremos fazer aqui. Uma variável de ambiente é basicamente um valor atribuído dinamicamente que pode afetar o modo como alguns processos irão se comportar em seu projeto.
Em python, normalmente acessamos essas variáveis utilizando a lib os
, builtin da linguagem, por meio do comando: os.environ.get("NOME_DA_VARIAVEL")
.
Alerta Boas Práticas 🚨
Gostaria de pontuar aqui a importância de utilizarmos variáveis de ambiente em nossos projetos. Normalmente projetos reais possuem informações sensíveis que pessoas externas ao projeto não podem ter acesso, ou informações que precisam ser alteradas dinamicamente, como:
- Senha do banco
- Token de um bucket
- Token de alguma API de terceiro
- Senha de login em outro serviço …
É extremamente indicado não hard coded essas informações diretamente no código. Ao invés disso, que coloquemos as mesmas como variáveis de ambiente em um arquivo separado e oculto do repositório onde você está versionando o seu projeto. Vamos ver então como fazer isso!!
Configurando Variáveis de Ambiente
Graças ao Docker, é simples configurar variáveis de ambiente para nosso projeto. Primeiro, vamos criar alguns arquivos:
|
Dentro do .dev
é onde você vai colocar todas as suas variáveis de ambiente:
ENVIRONMENT=dev
TESTING=0
DATABASE_URL=mysql://user:password@db:3306/econowallet
SQL_DATABASE=econowallet
SQL_USER=user
SQL_PASSWORD=password
SQL_HOST=db
SQL_PORT=3306
E agora, atualizamos nosso .gitignore
:
|
Show! Agora, localmente teremos como testar nossa aplicação, e quando a mesma for funcionar em um ambiente de produção, essas variáveis de ambiente serão substituídas dinamicamente para funcionar com as configurações de produção 😍.
Por fim, dentro do docker-compose.yml
nós iremos apontar onde o arquivo de variáveis de ambiente está localizado:
|
Inicializando em Conjunto com o SQLAlchemy
Lembrando que precisamos inicializar uma sessão global por meio do global_init()
que vimos anteriormente, vamos então fazer uso de uma feature do FastAPI que vimos anteriormente, @app.on_event()
:
|
Sendo assim, sempre que o app inicializar, vamos iniciar as configurações do SQLAlchemy em conjunto!!
Estamos quaseee lá!
Bug do MySQL 🐛
Se você tentar buildar o projeto, provavelmente irá se deparar com um problema onde o serviço web não irá conseguir inicializar pois não vai ter encontrado um banco MySQL disponível. Eu já busquei explicação do por que disso acontecer, mesmo especificando o depends_on: db
no docker-compose.yml
o serviço web tenta inicializar primeiro, e como não encontra um banco online, temos uma mensagem de erro.
Para driblar isso, vamos utilizar um pouquinho de conhecimento de bash e adicionar o que chamamos de entrypoint
para garantir que nosso serviço web apenas siga em frente após conectar com o banco.
Crie um arquivo chamado local-entrypoint.sh
e copie o seguinte código:
|
Nesse script é executado um loop que finaliza apenas quando o MySQL estiver disponível. Precisamos agora apontar nosso Dockerfile
para esse entrypoint.
# Dockerfile
FROM python:3.9.0-slim-buster
WORKDIR /usr/src/app
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
RUN apt-get update \
&& apt-get -y install netcat gcc \
# mysql dependencies
&& apt-get -y install default-libmysqlclient-dev build-essential \
&& apt-get clean
RUN pip install --upgrade pip \
&& pip install pipenv
COPY ./Pipfile .
COPY ./Pipfile.lock .
RUN pipenv install --deploy --system
COPY . .
# entrypoint
COPY ./local-entrypoint.sh /usr/src/app/local-entrypoint.sh
RUN chmod +x /usr/src/app/local-entrypoint.sh
CMD ["/bin/bash", "-c", "./local-entrypoint.sh"]
Nosso docker-compose.yml
também precisará ser alterado:
|
Aqui tivemos duas mudanças:
- nosso volume não é referente ao
.project
e sim a pasta root.
(caso contrário olocal-entrypoint.sh
não será encontrado). - removemos o
command
que estava inicializando app, e jogamos para dentro do entrypoint.
Por fim, garanta a instalação do drive do MySQL e do pacote mysql-connector-python
:
|
Refatoração 🔨
No início do projeto nós fizemos uma configuração no PyCharm para considerar o diretório project como root. Porém, teremos que retornar para o diretório anterior (efetuando os mesmos passos link de como fazer.
E além disso, iremos alterar todas as referências nos nossos arquivos de volta para project.app
… Os arquivos que precisarão sofrer essa refatoração são:
status.py
database_session.py
modelbase.py
register.py
main.py
Sorry!
Agora sim, vamos rebuildar a aplicação e observar no DBeaver se veremos uma nova tabela (como especificado nosso model).
|
🎊 🎉 Agora temos nossa tabela materializada no banco e podemos iniciar as transações!! 🎊 🎉
Próximo Capítulo…
Na próxima etapa, iremos configurar quais rotas teremos na aplicação. Assim como que as rotas interagem com o banco de dados 👋🏽.
Você pode acompanhar o repositório do projeto no link abaixo: