O objetivo do projeto é entregar variações de médias móveis simples, de 20, 50 e 200 dias, das moedas Bitcoin e Etherium que são listadas no Mercado Bitcoin via api Rest. Para isso temos que calcular diariamente a média móvel simples das moedas sendo isso feito por workers desenvolvidos utilizando o Celery.
Principais dependências:
- Django
- Django Ninja (async)
- Celery
Compatibilidade:
- Python 3.10.3
- Django 3.2.12
- Sobre o projeto
- Primeiros passos para executar o projeto localmente
- Modo de desenvolvimento
- Modo de produção
- Scripts de carga inicial no banco de dados
- Docker
- Celery
- Testes
- Logs
- Correlation ID
- Criando um novo aplicativo Django
- Changelog e versionamento do código
- Migrate e migration
- Commits
O projeto está dividido em três aplicações: api web rest, worker e beat.
A api rest é o ponto de entrada principal que expõe as rotas de acesso externo.
A rota principal da API é a de indicadores de média móvel simples. Ela é responsável por filtrar as variações das médias no banco de dados.
Para evitar alto número de requisições ao banco de dados, essa rota tem um cache com duração de 10 minutos.
curl --location --request GET 'http://localhost:8000/v1/indicators/BRLBTC/mms?range=20&from=1622469710&to=1622401310'
Os parâmetros que podem ser informados são:
Parâmetro | Local | Tipo | Obrigatório | Opções | Descrição |
---|---|---|---|---|---|
pair | path | texto | Sim | BRLBTC, BRLETH | Pair da moeda que deve ser pesquisado. |
range | query | número | Sim | 20, 50, 200 | Quantidade de dias da média móvel. |
from | query | número | Sim | Data inicial de pesquisa. | |
to | query | número | Não | Data final de pesquisa. Padrão é o dia anterior. | |
precision | query | texto | Não | 1d | Precisão da média móvel. Padrão é 1d. |
Mais informações podem ser obtidos na documentação: http://localhost:8000/v1/docs
O beat é uma aplicação schedule, de tempos em tempos ele chama as tasks que está programado. Essa configuração pode ser feita pelo Django Admin ou pelo próprio settings do projeto.
Se acessar src/project/core/settings/base.py
e procurar pela variável de ambiente
CELERY_BEAT_SCHEDULE verá que existe uma configuração para ele. Essa
configuração diz que a task task_beat_select_pairs_to_mms deve ser chamada
a cada uma hora. Com isso a cada uma hora o beat irá publicar uma mensagem
na fila indicator-mms-select-pairs e o worker irá consumir essa mensagem
e começar a executar a task.
O worker é uma aplicação que consome uma ou mais filas e redireciona a mensagem da fila para a task responsável. Atualmente existem duas tasks no projeto:
- task_beat_select_pairs_to_mms
Essa task consome a fila indicator-mms-select-pairs e é responsável diariamente por distribuir os pairs das moedas para a task que calcula a média móvel simples.
Para garantir que a task não será executada mais de uma vez no dia, existe um cache lock com duração de 24 horas e caso a task já tenha sido executada no dia, a executação atual é descartada. Se ocorrer erro na primeira execução do dia o cache lock não é setado.
- task_calculate_simple_moving_average
Essa task consome a fila indicator-mms-calculate e é responsável por calcular a média móvel simples de cada pair que ela recebe.
A task recebe três parâmetros para execução:
- pair: pair da moeda;
- precision: precisão para cálculo da média móvel simples, por enquanto é somente 1 dia;
- datetime_started: data e hora que a mensagem foi colocada na fila da task;
Assim que a task começa a processar ela cria um cache lock para garantir que outro worker não irá processar o mesmo pair que ela esta processando e esse cache lock é removido assim que a task é finalizada.
Ela identifica os parâmetros para cálculo da média e faz uma request para a API de Candles a fim de buscar os dados de fechamento do pair. Assim que recebe o retorno da API ela começa a calcular a média móvel simples salvando os dados no banco de dados.
Caso haja algum erro durante o processamento da task, é definido um retry de 30 minutos. Esse retry pode ocorrer inúmeras vezes ao dia e caso a data inicial de processamento task for menor que a data de processamento atual da task o processamento do pair é descartado.
1- Configure o ambiente virtual:
python -m venv venv
2- Ative o ambiente virtual:
source venv/bin/activate
3- Execute o comando para instalar as dependências:
make dependencies
O modo de desenvolvimento por padrão utiliza o Postgres como banco de dados e Redis para cache e filas do Celery.
1- Copie o arquivo .env-sample e renomeie para .env.development. Aproveite e faça os ajustes necessários nas variáveis de ambiente:
cp .env-sample .env.development
2- Execute o comando para criar as tabelas no banco de dados. Ao executar o comando abaixo será criado os containers docker com as dependências:
make migrate
3- Para acessar o Django Admin é necessário criar o super usuário e isso pode ser feito com o seguinte comando:
make superuser
4- Agora basta executar o comando abaixo para iniciar o aplicativo. O comando abaixo irá subir as dependências no docker e irá executar a aplicação no shell.
make run
Você também pode executar a aplicação em um container docker. Consulte a seção Docker.
Após executar os comandos acima, você poderá acessar a documentação e o painel administrativo:
http://localhost:8000/admin
http://localhost:8000/ping
http://localhost:8000/v1/docs
O modo de produção utilizamos o servidor gunicorn junto com o servidor ASGI uvicorn.
Para implantar em produção, as seguintes variáveis de ambiente devem ser definidas na aplicação da api, worker e beat:
export SIMPLE_SETTINGS=project.core.settings.production
export GUNICORN_WORKERS=1
export CELERY_WORKER_CONCURRENCY=1
export SECRET_KEY="your_key_here"
export DATABASE_URL="sqlite:///db.sqlite3"
export DATABASE_READ_URL="sqlite:///db.sqlite3"
export CELERY_BROKER_URL="redis://127.0.0.1:6379/1"
export REDIS_URL=redis://127.0.0.1:6379/0
export REDIS_URL_LOCK=redis://127.0.0.1:6379/0
Opcional:
export ALLOWED_HOSTS="*;"
Se esta é a primeira vez que executa o aplicativo em um banco de dados de produção, você deve aplicar a migração e criar o super usuário.
Como opcional você pode desativar o Django Admin setando a variável ADMIN_ENABLED como false. Caso queira desativar a documentação você deve setar docs_url igual a None no NinjaAPI em project.urls.
Consulte o arquivo core/settings/base.py
para ver todas as variáveis de ambiente disponíveis.
Se a primeira vez que o projeto está sendo executado normalmente queremos fazer uma carga inicial no banco de dados.
Os scripts são configurados no management
do Django. Veja que no app src/project/apps/indicators/mms
existe o diretório
management/commands
com o arquivo mms_initial_charge.py. Esse arquivo é o
script de carga inicial das variações de média móvel simples.
Se você for executar o script localmente, primeiramente deve-se iniciar as
dependências do projeto make docker-dependencies-up
.
Para executar o script basta executar o comando abaixo:
python src/manage.py mms_initial_charge --days=365
Ao executar o comando será publicado na fila indicator-mms-calculate mensagens com data para cálculo da quantidade de dias informado no comando. Com isso a task task_calculate_simple_moving_average irá consumir as mensagens e começar a calcular a média móvel do dia e pair recebido.
Para que a task começa a capturar as mensagens para executar o cálculo os workers
do Celery devem estar ligados. Se estiver executando o script localmente basta
rodar o comando make docker-celery-up
.
Esta aplicação faz uso do Docker para facilitar durante o desenvolvimento. Certifique-se de ter o docker instalado em seu dispositivo.
Veja os comandos disponíveis no Makefile:
Comando | Descrição |
---|---|
docker-up-all | Inicia todos os containers docker. |
docker-down-all | Remove todos os containers criados. |
docker-restart-all | Reinicia todos os containers criados. |
docker-dependencies-up | Cria os containers docker de dependências da aplicação. |
docker-dependencies-down | Remove os containers de dependências da aplicação. |
docker-dependencies-downclear | Remove os containers e os volumes de dependências da aplicação. |
docker-app-up | Cria os containers docker da aplicação rest. |
docker-app-down | Remove os containers da aplicação rest. |
docker-app-logs | Visualiza os logs dos containers da aplicação rest. |
docker-app-migrate | Aplicada as migrações no banco de dados. |
docker-app-superuser | Cria o super usuário para acesso ao admin. |
docker-celery-run | Cria os containers docker do Celery (worker e beat). |
docker-celery-down | Remove os containers do Celery (worker e beat). |
docker-celery-logs | Visualiza os logs dos containers do Celery (worker e beat). |
Nesse projeto é possível criar tarefas (tasks) que podem ser executadas de tempo em tempo ou quando solicitadas. Toda essa lógica é feita usando a biblioteca [Celery] (https://docs.celeryproject.org/en/stable).
Seu funcionamento consiste em dois aplicativos, sendo o worker e o beat:
- O beat é apenas uma aplicação que de vez em quando chama uma task para ser
executada, isso de acordo com a configuração definida em
core/settings/base.py
. - O worker é o aplicativo que recebe a mensagem do beat ou de algum comando interno do aplicativo e começa a processar a tarefa de acordo com o que está definido nele.
Dentro de cada aplicativo, configuramos um arquivo chamado tasks.py e nesse arquivo escrevemos o código da tarefa.
Na seção Docker você encontrará alguns comandos para criar os containers docker do Celery.
Para a criação de testes unitários o projeto utiliza o pytest.
Dentro de cada app que fica no diretório src/project
existe uma pasta chamada tests.
Cada arquivo python de teste tem seu nome começado com test_ e cada função
de teste começa com test_.
Existe alguns comandos (make) para você rodar os testes:
Comando | Descrição |
---|---|
test | Irá rodar os testes unitários da aplicação. |
test-coverage | Irá rodar os testes unitários e medir a cobertura do testes. |
test-coverage-html-server | Irá rodar os testes unitários, medir a cobertura, gerar uma página html estática e iniciará um servidor local . |
Os logs do aplicativo são mais poderosos e menos dolorosos com a ajuda de
structlog que é intermediado por structlog
. Todos os logs feitos são
convertidos para JSON, facilitando assim a análise e busca.
Para utiliza-los basta seguir o código abaixo:
import structlog
logger = structlog.get_logger()
logger.info("User logged in", user="test-user")
Todos os logs gerados utilizando o structlog contém o Correlation-ID. Para mais detalhes sobre Correlation-ID acesse a seção.
Correlation ID é um código UUID que amarra todos os logs gerados pela aplicação, facilitando assim a busca de logs gerados em uma requisição, por exemplo. Este aplicativo faz uso do django-cid para fazer a gestão dos Correlation ID.
Ele é injetado nos logs e retornado no cabeçalho de cada solicitação. O usuário pode também enviá-lo no cabeçalho da solicitação (X-Correlation-ID) ou se não for encontrado, o aplicativo irá gerar automaticamente.
Quando precisamos criar novas rotas ou novas tasks para a aplicação devemos criar um novo app Django.
Todos os novos aplicativos são criados no diretório src/project e para criar um novo aplicativo, você deve executar o seguinte comando:
make app name=clients
Observe que o parâmetro name foi passado. Ele é usado para informar o nome do novo aplicativo.
Esse projeto está configurado para gerar um arquivo de changelog toda vez que haver a nescessidade de gerar uma nova versão do projeto.
Uma boa prática é sempre criar um arquivo changelog em cada tarefa (pull/merge request) concluída a fim de manter um histórico da mudança. Para isso temos alguns comandos:
Comando | Descrição |
---|---|
changelog-feature | Significa um novo recurso. |
changelog-improvement | Significa que uma melhoria foi implementada no projeto. |
changelog-bugfix | Significa uma correção de um problema. |
changelog-doc | Significa uma melhoria na documentação. |
changelog-removal | Significa uma suspensão ou remoção de uma rota de API. |
Cada um desses comandos espera o parâmetro message. Você pode usá-lo da seguinte forma:
make changelog-feature message="Adds CRUD for clients management"
Para mais detalhes sobre a criação desses arquivos de changelog você pode ver na documentação da biblioteca towncrier.
Quando uma história termina, é hora de criar uma nova versão do projeto. Todos os arquivos de changelog existentes serão convertidos em uma única mensagem que ficará disponível no arquivo CHANGELOG.md.
Existem três comandos que podemos usar para fechar uma versão. São eles:
Comando | Descrição |
---|---|
release-patch | Cria uma versão de patch. Ex.: 0.0.1 |
release-minor | Cria uma versão de minor. Ex.: 0.1.0 |
release-major | Cria uma versão de major. Ex.: 1.0.0 |
Você pode usá-los da seguinte maneira:
make release-patch
Depois de executar o comando específico, dois novos commits serão criados, um referindo-se à geração do arquivo changelog e o outro referindo-se a geração da nova versão do aplicativo. Além desses novos commits, uma tag específica para a versão do aplicativo também é gerada.
Finalmente, você pode enviar todas as alterações para o seu repositório git
com o comando make push
.
Para mais detalhes sobre o versionamento, você pode conferir a documentação oficial da biblioteca bump2version.
Se você acabou de configurar seu modelo (model), é hora de criar o esquema a ser aplicado no banco de dados no futuro. Observe que em seu aplicativo há uma pasta com o nome de migrations, é aqui que fica o esquema do seu model. Para mais detalhes sobre esses comandos acesse a documentação oficial.
Para criar o esquema, precisamos executar o seguinte comando:
make migration
Você também pode criar um layout em branco, que por sua vez não será relacionado a nenhum modelo em primeiro momento:
make migration-empty app=clients
Observe que o parâmetro app foi informado. Ele é usado para informar em qual aplicativo o model deverá ser criado.
Agora precisamos aplicar este model ao banco de dados e para isso executamos o seguinte comando:
make migrate
Ao criar o commit, é comum apenas falar o que foi feito ou o que está sendo feito, utilizando para isso verbos no passado ou no gerúndio:
Fixed bug with Y
Changing behavior of X
Refatorou classe de autenticação
Atualizando biblioteca xpto
Escrever desse modo pode parecer mais natural na hora de compor a mensagem do commit, mas isso pode dificultar a leitura do histórico de commits de um projeto.
Ao fazer commits em inglês, escreva o título no modo imperativo:
Refactor subsystem X for readability
Update getting started documentation
Remove deprecated methods
Release version 1.0.0
Isso vai de acordo com o padrão que o próprio Git segue quando gera uma mensagens de commit automaticamente em casos
como no git merge
:
Merge branch 'myfeature'
Ou como no git revert:
Revert "Add the thing with the stuff"
This reverts commit cc87791524aedd593cff5a74532befe7ab69ce9d.
Uma dica ao fazer mensagens de commit em inglês, é que um bom título sempre deve completar a frase: if applied, this commit will. Por exemplo:
if applied, this commit will refactor subsystem X for readability
if applied, this commit will update getting started documentation
if applied, this commit will remove deprecated methods
if applied, this commit will release version 1.0.0
Já quando for escrever a mensagem do commit em português conjugue o verbo no presente do indicativo, utilizando a terceira pessoa do singular:
Refatora sistema X para melhorar legibilidade
Atualiza documentação de instalação do projeto
Remove métodos obsoletos
O conteúdo acima foi retirado do site Ruan Brandão.