From cc54fa2d5ac0f7ecc52277ed59f00ffcda85985e Mon Sep 17 00:00:00 2001 From: Date: Thu, 19 Sep 2024 09:02:43 -0300 Subject: [PATCH] Deployed d29d54c with MkDocs version: 1.6.1 --- 03/index.html | 2 +- index.html | 8 ++--- search/search_index.json | 2 +- sitemap.xml | 76 +++++++++++++++++++-------------------- sitemap.xml.gz | Bin 398 -> 398 bytes 5 files changed, 44 insertions(+), 44 deletions(-) diff --git a/03/index.html b/03/index.html index 852a6032..8b584e99 100644 --- a/03/index.html +++ b/03/index.html @@ -2850,7 +2850,7 @@

Conclusão

diff --git a/index.html b/index.html index 2eec29c7..53057ae7 100644 --- a/index.html +++ b/index.html @@ -1811,10 +1811,10 @@

Onde o curso será disponibilizado?Pré-requisitos

Para aproveitar ao máximo este curso, é recomendado que você já tenha algum conhecimento prévio em python, se pudesse listar o que considero importante para não se perder, os tópicos em python importantes são:

As referências servem como base caso você ainda não tenha estudado esses assuntos

@@ -2053,7 +2053,7 @@

F.A.Q.

- September 7, 2024 + September 19, 2024 diff --git a/search/search_index.json b/search/search_index.json index f90d1eca..e706ba73 100644 --- a/search/search_index.json +++ b/search/search_index.json @@ -1 +1 @@ -{"config":{"lang":["pt"],"separator":"[\\s\\-]+","pipeline":["stopWordFilter"]},"docs":[{"location":"","title":"FastAPI do Zero!","text":""},{"location":"#fastapi-do-zero","title":"FastAPI do ZERO","text":"Caso prefira ver a apresenta\u00e7\u00e3o do curso em v\u00eddeo

Esse aula ainda n\u00e3o est\u00e1 dispon\u00edvel em formato de v\u00eddeo, somente em texto ou live!

Aula Slides

Ol\u00e1, boas vindas ao curso de FastAPI!

A nossa inten\u00e7\u00e3o neste curso \u00e9 facilitar o aprendizado no desenvolvimento de APIs usando o FastAPI. Vamos explorar como integrar bancos de dados, criar testes e um sistema b\u00e1sico de autentica\u00e7\u00e3o com JWT. Tudo isso para oferecer uma boa base para quem quer trabalhar com desenvolvimento web com Python. A ideia desse curso \u00e9 apresentar os conceitos de forma pr\u00e1tica, construindo um projeto do zero e indo at\u00e9 a sua fase de produ\u00e7\u00e3o.

"},{"location":"#o-que-e-fastapi","title":"O que \u00e9 FastAPI?","text":"

FastAPI \u00e9 um framework Python moderno, projetado para simplicidade, velocidade e efici\u00eancia. A combina\u00e7\u00e3o de diversas funcionalidades modernas do Python como anota\u00e7\u00f5es de tipo e suporte a concorr\u00eancia, facilitando o desenvolvimento de APIs.

"},{"location":"#sobre-o-curso","title":"Sobre o curso","text":"

Este curso foi desenvolvido para oferecer uma experi\u00eancia pr\u00e1tica no uso do FastAPI, uma das ferramentas mais modernas para constru\u00e7\u00e3o de APIs. Ao longo do curso, o objetivo \u00e9 que voc\u00ea obtenha uma compreens\u00e3o das funcionalidades do FastAPI e de boas pr\u00e1ticas associadas a ele.

O projeto central do curso ser\u00e1 a constru\u00e7\u00e3o de um gerenciador de tarefas (uma lista de tarefas), come\u00e7ando do zero. Esse projeto incluir\u00e1 a implementa\u00e7\u00e3o da autentica\u00e7\u00e3o do usu\u00e1rio e das opera\u00e7\u00f5es CRUD completas.

Para a constru\u00e7\u00e3o do projeto, ser\u00e3o utilizadas as vers\u00f5es mais recentes das ferramentas, dispon\u00edveis em 2024, como a vers\u00e3o 0.114 do FastAPI, a vers\u00e3o 2.0 do Pydantic, a vers\u00e3o 2.0 do SQLAlchemy ORM, al\u00e9m do Python 3.11/3.12 e do Alembic para gerenciamento de migra\u00e7\u00f5es.

Al\u00e9m da constru\u00e7\u00e3o do projeto, o curso tamb\u00e9m incluir\u00e1 a pr\u00e1tica de testes, utilizando o pytest. Essa abordagem planeja garantir que as APIs desenvolvidas sejam n\u00e3o apenas funcionais, mas tamb\u00e9m robustas e confi\u00e1veis.

"},{"location":"#o-que-voce-vai-aprender","title":"O que voc\u00ea vai aprender?","text":"

Aqui est\u00e1 uma vis\u00e3o geral dos t\u00f3picos que abordaremos neste curso:

  1. Configura\u00e7\u00e3o do ambiente de desenvolvimento para FastAPI: come\u00e7aremos do absoluto zero, criando e configurando nosso ambiente de desenvolvimento.

  2. Primeiros Passos com FastAPI e Testes: ap\u00f3s configurar o ambiente, mergulharemos na estrutura b\u00e1sica de um projeto FastAPI e faremos uma introdu\u00e7\u00e3o detalhada ao Test Driven Development (TDD).

  3. Modelagem de Dados com Pydantic e SQLAlchemy: aprenderemos a criar e manipular modelos de dados utilizando Pydantic e SQLAlchemy, dois recursos que levam a efici\u00eancia do FastAPI a outro n\u00edvel.

  4. Autentica\u00e7\u00e3o e Autoriza\u00e7\u00e3o em FastAPI: construiremos um sistema de autentica\u00e7\u00e3o completo, para proteger nossas rotas e garantir que apenas usu\u00e1rios autenticados tenham acesso a certos dados.

  5. Testando sua Aplica\u00e7\u00e3o FastAPI: faremos uma introdu\u00e7\u00e3o detalhada aos testes de aplica\u00e7\u00e3o FastAPI, utilizando as bibliotecas pytest e coverage. Al\u00e9m de execut\u00e1-los em um pipeline de integra\u00e7\u00e3o cont\u00ednua com github actions.

  6. Dockerizando e Fazendo Deploy de sua Aplica\u00e7\u00e3o FastAPI: por fim, aprenderemos como \"dockerizar\" nossa aplica\u00e7\u00e3o FastAPI e fazer seu deploy utilizando Fly.io.

"},{"location":"#esse-curso-e-gratuito","title":"\ud83d\udcb0 Esse curso \u00e9 gratuito?","text":"

SIM! Esse curso foi todo desenvolvido de forma aberta e com a ajuda financeira de pessoas incr\u00edveis. Caso voc\u00ea sinta vontade de contribuir, voc\u00ea pode me pagar um caf\u00e9 por pix (pix.dunossauro@gmail.com) ou apoiar a campanha recorrente de financiamento coletivo da live de python que \u00e9 o que paga as contas aqui de casa.

"},{"location":"#onde-o-curso-sera-disponibilizado","title":"Onde o curso ser\u00e1 disponibilizado?","text":"

Esse material ser\u00e1 disponibilizado de tr\u00eas formas diferentes:

  1. Em livro texto: todo o material est\u00e1 dispon\u00edvel nessa p\u00e1gina;

  2. Em aulas s\u00edncronas ao vivo: para quem prefere o compromisso de acompanhar em grupo. Datas j\u00e1 dispon\u00edveis;

    Playlist das Aulas s\u00edncronas (Ao vivo):

  3. Em formato de v\u00eddeo (ass\u00edncronas): todas as aulas ser\u00e3o disponibilizadas em formato de v\u00eddeo em meu canal do YouTube para quem prefere assistir ao ler. (V\u00eddeos ainda n\u00e3o dispon\u00edveis)

"},{"location":"#pre-requisitos","title":"Pr\u00e9-requisitos","text":"

Para aproveitar ao m\u00e1ximo este curso, \u00e9 recomendado que voc\u00ea j\u00e1 tenha algum conhecimento pr\u00e9vio em python, se pudesse listar o que considero importante para n\u00e3o se perder, os t\u00f3picos em python importantes s\u00e3o:

As refer\u00eancias servem como base caso voc\u00ea ainda n\u00e3o tenha estudado esses assuntos

Alguns outros t\u00f3picos n\u00e3o relativos a python tamb\u00e9m ser\u00e3o abordados. Ent\u00e3o \u00e9 interessante que voc\u00ea tenha algum entendimento b\u00e1sico sobre:

Caso esteja iniciando seus estudos em Python!

Caso voc\u00ea ainda n\u00e3o se sinta uma pessoa preparada, ou caiu aqui sem saber exatamente o que esperar. Temos um pequeno curso introdut\u00f3rio. Destinado aos primeiros passos com python.

Link direto

Tamb\u00e9m temos uma live focada em dicas para iniciar os estudos em python

Link direto

Ou ent\u00e3o a leitura do livro Pense em python

"},{"location":"#aulas","title":"Aulas","text":"
  1. Configurando o Ambiente de Desenvolvimento
  2. Introdu\u00e7\u00e3o ao desenvolvimento WEB
  3. Estruturando seu Projeto e Criando Rotas CRUD
  4. Configurando Banco de Dados e Gerenciando Migra\u00e7\u00f5es com Alembic
  5. Integrando Banco de Dados a API
  6. Autentica\u00e7\u00e3o e Autoriza\u00e7\u00e3o
  7. Refatorando a Estrutura do Projeto
  8. Tornando o sistema de autentica\u00e7\u00e3o robusto
  9. Criando Rotas CRUD para Tarefas
  10. Dockerizando a nossa aplica\u00e7\u00e3o e introduzindo o PostgreSQL
  11. Automatizando os testes com integra\u00e7\u00e3o cont\u00ednua
  12. Fazendo o deploy no Fly.io
  13. Despedida e pr\u00f3ximos passos

Ap\u00f3s todas as aulas, se voc\u00ea sentir que ainda quer evoluir mais e testar seus conhecimentos, temos um projeto final para avaliar o quanto voc\u00ea aprendeu.

"},{"location":"#quem-vai-ministrar-essas-aulas","title":"\ud83e\udd96 Quem vai ministrar essas aulas?","text":"

Prazer! Eu me chamo Eduardo. Mas as pessoas me conhecem na internet como @dunossauro.

Sou um programador Python muito empolgado e curioso. Toco um projeto pessoal chamado Live de Python h\u00e1 quase 7 anos. Onde conversamos sobre tudo e mais um pouco quando o assunto \u00e9 Python.

Esse projeto que estamos desenvolvendo \u00e9 um peda\u00e7o, um projeto, de um grande curso de FastAPI que estou montando. Espero que voc\u00ea se divirta ao m\u00e1ximo com a parte pr\u00e1tica enquanto escrevo em mais detalhes todo o potencial te\u00f3rico que lan\u00e7arei no futuro!

Caso queira saber mais sobre esse projeto completo.

"},{"location":"#revisao-e-contribuicoes","title":"Revis\u00e3o e contribui\u00e7\u00f5es","text":"

Esse material contou com a revis\u00e3o e contribui\u00e7\u00f5es inestim\u00e1veis de pessoas incr\u00edveis:

@adorilson, @aguynaldo, @alphabraga, @andrespp, @azmovi, @bugelseif, @gabrielhardcore, @gbpagano, @henriqueccda, @henriquesebastiao, @ig0r-ferreira, @itsGab, @ivansantiagojr, @jlplautz, @jonathanscheibel, @julioformiga, @lbmendes, @lucasmpavelski, @lucianoratamero, @matheusalmeida28, @me15degrees, @mmaachado, @rennerocha, @ricardo-emanuel01, @rodbv, @rodrigosbarretos, @taconi, @vcwild, @williangl, @vdionysio

Muito obrigado!

"},{"location":"#licenca","title":"\ud83d\udcd6 Licen\u00e7a","text":"

Todo esse curso foi escrito e produzido por Eduardo Mendes (@dunossauro).

Todo esse material \u00e9 gratuito e est\u00e1 sob licen\u00e7a Creative Commons BY-NC-SA. O que significa que:

Pontos de aten\u00e7\u00e3o:

"},{"location":"#ferramentas-necessarias-para-acompanhar-o-curso","title":"\ud83e\uddf0 Ferramentas necess\u00e1rias para acompanhar o curso","text":"
  1. Um editor de texto ou IDE de sua escolha. Estou usando o GNU/Emacs enquanto escrevo as aulas;
  2. Um terminal. Todos os exemplos do curso s\u00e3o executados e explicados no terminal. Voc\u00ea pode usar o que se sentir mais a vontade e for compat\u00edvel com seu sistema operacional;
  3. Ter o interpretador Python instalado em uma vers\u00e3o igual ou superior a 3.11
  4. Uma conta no Github: para podermos testar com Github Actions;
  5. Uma conta no Fly.io: ferramenta que usaremos para fazer deploy.
"},{"location":"#ferramentas-de-apoio","title":"\ud83d\udd27 Ferramentas de apoio","text":"

Toda essa p\u00e1gina foi feita usando as seguintes bibliotecas:

Para os slides:

"},{"location":"#repositorio","title":"\ud83d\udcc1 Reposit\u00f3rio","text":"

O versionamento de tudo est\u00e1 sendo feito no reposit\u00f3rio do curso Github

"},{"location":"#deploy","title":"\ud83d\ude80 Deploy","text":"

Os deploys das p\u00e1ginas est\u00e1ticas geradas pelo MkDocs est\u00e3o sendo feitos no Netlify

"},{"location":"#conclusao","title":"Conclus\u00e3o","text":"

Neste curso, a inten\u00e7\u00e3o \u00e9 fornecer uma compreens\u00e3o completa do framework FastAPI, utilizando-o para construir uma aplica\u00e7\u00e3o de gerenciamento de tarefas. O aprendizado ser\u00e1 focado na pr\u00e1tica, e cada conceito ser\u00e1 acompanhado por exemplos e exerc\u00edcios relevantes.

A jornada come\u00e7ar\u00e1 com a configura\u00e7\u00e3o do ambiente de desenvolvimento e introdu\u00e7\u00e3o ao FastAPI. Ao longo das aulas, abordaremos t\u00f3picos como autentica\u00e7\u00e3o, opera\u00e7\u00f5es CRUD, testes com pytest e deploy. A \u00eanfase ser\u00e1 colocada na aplica\u00e7\u00e3o de boas pr\u00e1ticas e no entendimento das ferramentas e tecnologias atualizadas, incluindo as vers\u00f5es mais recentes do FastAPI, Pydantic, SQLAlchemy ORM, Python e Alembic.

Este conte\u00fado foi pensado para auxiliar na compreens\u00e3o de como criar uma API eficiente e confi\u00e1vel, dando aten\u00e7\u00e3o a aspectos importantes como testes e integra\u00e7\u00e3o com banco de dados.

Nos vemos na primeira aula. \u2764

"},{"location":"#faq","title":"F.A.Q.","text":"

Perguntas frequentes que me fizeram durante os v\u00eddeos:

"},{"location":"01/","title":"Configurando o ambiente de desenvolvimento","text":""},{"location":"01/#configurando-o-ambiente-de-desenvolvimento","title":"Configurando o Ambiente de Desenvolvimento","text":"

Objetivos dessa aula:

Caso prefira ver a aula em v\u00eddeo

Esse aula ainda n\u00e3o est\u00e1 dispon\u00edvel em formato de v\u00eddeo, somente em texto ou live!

Aula Slides C\u00f3digo Quiz Exerc\u00edcios

Nesta aula, iniciaremos nossa jornada na constru\u00e7\u00e3o de uma API com FastAPI. Partiremos do b\u00e1sico, configurando nosso ambiente de desenvolvimento. Discutiremos desde a escolha e instala\u00e7\u00e3o da vers\u00e3o correta do Python at\u00e9 a instala\u00e7\u00e3o e configura\u00e7\u00e3o do Poetry, um gerenciador de pacotes e depend\u00eancias para Python. Al\u00e9m disso, instalaremos e configuraremos uma s\u00e9rie de ferramentas de desenvolvimento \u00fateis, como Ruff, pytest e Taskipy.

Ap\u00f3s configurado o nosso ambiente, criaremos nosso primeiro programa \"Hello, World!\" com FastAPI. Isso nos permitir\u00e1 confirmar que tudo est\u00e1 funcionando corretamente. E, finalmente, exploraremos uma parte crucial do Desenvolvimento Orientado por Testes (TDD), escrevendo nosso primeiro teste com Pytest.

"},{"location":"01/#ambiente-de-desenvolvimento","title":"Ambiente de Desenvolvimento","text":"

Para iniciar esse curso voc\u00ea precisa de algumas ferramentas instaladas:

  1. Um editor de texto a sua escolha (Usarei o GNU/Emacs)
  2. Um terminal a sua escolha (Usarei o Terminator)
  3. Uma vers\u00e3o 3.11+ do Python instalada.
    • Caso n\u00e3o tenha essa vers\u00e3o voc\u00ea pode baixar do site oficial
    • Ou instalar via pyenv
    • Embora a vers\u00e3o 3.7 seja suficiente para executar o FastAPI em si, neste curso usaremos recursos de Python que est\u00e3o presentes apenas a partir da vers\u00e3o 3.11. Portanto, essa \u00e9 a vers\u00e3o m\u00ednima obrigat\u00f3ria para o curso.
  4. O Poetry para gerenciar os pacotes e seu ambiente virtual (caso n\u00e3o conhe\u00e7a o poetry temos uma live de python sobre ele)
  5. Git: Para gerenciar vers\u00f5es do nosso projeto. (Caso precise de um tutorial legal sobre git, o TeoMeWhy fez uma playlist muito legal, totalmente de gra\u00e7a)
  6. Docker: Para criar um container da nossa aplica\u00e7\u00e3o (caso n\u00e3o tenha nenhuma experi\u00eancia com docker a Linuxtips tem uma playlist completa e gr\u00e1tis sobre docker no canal deles no YouTube)1
  7. OPCIONAL: O pipx pode te ajudar bastante nesses momentos de instala\u00e7\u00f5es
  8. OPCIONAL: O ignr para criar nosso gitignore
  9. OPCIONAL (extremamente recomendado): O gh para criar o reposit\u00f3rio e fazer altera\u00e7\u00f5es sem precisar acessar a p\u00e1gina do Github

\ud83d\udea8\ud83d\udea8 Caso voc\u00ea precise de ajuda com a instala\u00e7\u00e3o dessas ferramentas, temos um ap\u00eandice especial para te ajudar com isso!. Basta clicar em achar a ferramenta que deseja instalar! \ud83d\udea8\ud83d\udea8

"},{"location":"01/#instalacao-do-python","title":"Instala\u00e7\u00e3o do Python","text":"

Se voc\u00ea precisar (re)construir o ambiente usado nesse curso, \u00e9 extremamente recomendado que voc\u00ea use o pyenv.

Pyenv \u00e9 uma aplica\u00e7\u00e3o externa ao python que permite que voc\u00ea instale diferentes vers\u00f5es do python no sistema e as isola. Podendo isolar vers\u00f5es espec\u00edficas, para projetos espec\u00edficos. Na computa\u00e7\u00e3o, chamamos esse conceito de shim. Uma camada, onde toda vez que o python for chamado, ele redirecionar\u00e1 ao python na vers\u00e3o especificada no pyenv globalmente ou em uma vers\u00e3o fixada em projeto especifico. Uma esp\u00e9cie de \"proxy\".

graph LR;\n  A[\"Executando o python no terminal\"] --> pyenv\n  pyenv[\"Pyenv shim\"] --> questao{\"existe '.python-version'?\"}\n  questao -->|sim| B[\"Execute esta vers\u00e3o instalada no pyenv\"]\n  questao -->|n\u00e3o| C[\"Use a vers\u00e3o global do pyenv\"]

A instala\u00e7\u00e3o do pyenv varia entre sistemas operacionais. Caso voc\u00ea esteja usando Windows, recomendo que voc\u00ea use o pyenv-windows para fazer a instala\u00e7\u00e3o, os passos est\u00e3o descritos na p\u00e1gina. Para GNU/Linux e MacOS, use o pyenv-installer, os passos para instala\u00e7\u00e3o tamb\u00e9m est\u00e3o descritos.

Navegue at\u00e9 o diret\u00f3rio onde far\u00e1 os c\u00f3digos e exerc\u00edcios do curso e digite os seguintes comandos do pyenv:

Vers\u00e3o 3.11Vers\u00e3o 3.12 $ Execu\u00e7\u00e3o no terminal!
pyenv update\npyenv install 3.11:latest\n
Para quem usa Windows

O pyenv-win tem um bug intermitente em rela\u00e7\u00e3o ao uso de :latest:

PS C:\\Users\\vagrant> pyenv install 3.11:latest\n:: [Info] ::  Mirror: https://www.python.org/ftp/python\npyenv-install: definition not found: 3.11:latest\n\nSee all available versions with `pyenv install --list`.\nDoes the list seem out of date? Update it using `pyenv update`.\n

Caso voc\u00ea se depare com esse erro, pode rodar o comando pyenv install --list e ver a maior vers\u00e3o dispon\u00edvel do python no momento da sua instala\u00e7\u00e3o. Em seguida executar pyenv install 3.11.<a maior vers\u00e3o dispon\u00edvel>. Nesse momento em que escrevo \u00e9 a vers\u00e3o 3.11.9 :

PS C:\\Users\\vagrant> pyenv install 3.11.9\n:: [Info] ::  Mirror: https://www.python.org/ftp/python\n:: [Downloading] ::  3.11.9 ...\n:: [Downloading] ::  From https://www.python.org/ftp/python/3.11.9/3.11.9-amd64.exe\n:: [Downloading] ::  To   C:\\Users\\vagrant\\.pyenv\\pyenv-win\\install_cache\\python-3.11.9-amd64.exe\n:: [Installing] ::  3.11.9 ...\n:: [Info] :: completed! 3.11.9\n

Desta forma os pr\u00f3ximos comandos podem ser executados normalmente.

Certifique que a vers\u00e3o do python 3.11 esteja instalada:

$ Execu\u00e7\u00e3o no terminal!
pyenv versions\n* system (set by /home/dunossauro/.pyenv/version)\n3.10.12\n3.11.1\n3.12.0\n3.11.9\n3.12.0b1\n

A resposta esperada \u00e9 que o Python 3.11.9 (a maior vers\u00e3o do python 3.11 enquanto escrevia esse material) esteja nessa lista.

$ Execu\u00e7\u00e3o no terminal!
pyenv update\npyenv install 3.12:latest\n
Para quem usa Windows

O pyenv-win tem um bug intermitente em rela\u00e7\u00e3o ao uso de :latest:

PS C:\\Users\\vagrant> pyenv install 3.12:latest\n:: [Info] ::  Mirror: https://www.python.org/ftp/python\npyenv-install: definition not found: 3.12:latest\n\nSee all available versions with `pyenv install --list`.\nDoes the list seem out of date? Update it using `pyenv update`.\n

Caso voc\u00ea se depare com esse erro, pode rodar o comando pyenv install --list e ver a maior vers\u00e3o dispon\u00edvel do python no momento da sua instala\u00e7\u00e3o. Em seguida executar pyenv install 3.12.<a maior vers\u00e3o dispon\u00edvel>. Nesse momento em que escrevo \u00e9 a vers\u00e3o 3.12.3 :

PS C:\\Users\\vagrant> pyenv install 3.12.3\n:: [Info] ::  Mirror: https://www.python.org/ftp/python\n:: [Downloading] ::  3.12.3 ...\n:: [Downloading] ::  From https://www.python.org/ftp/python/3.12.3/3.12.3-amd64.exe\n:: [Downloading] ::  To   C:\\Users\\vagrant\\.pyenv\\pyenv-win\\install_cache\\python-3.12.3-amd64.exe\n:: [Installing] ::  3.12.3 ...\n:: [Info] :: completed! 3.12.3\n

Desta forma os pr\u00f3ximos comandos podem ser executados normalmente.

Certifique que a vers\u00e3o do python 3.12 esteja instalada:

$ Execu\u00e7\u00e3o no terminal!
pyenv versions\n* system (set by /home/dunossauro/.pyenv/version)\n3.10.12\n3.11.1\n3.12.0\n3.12.3\n3.12.0b1\n

A resposta esperada \u00e9 que o Python 3.12.3 (a maior vers\u00e3o do python 3.12 enquanto escrevia esse material) esteja nessa lista.

Toda a implementa\u00e7\u00e3o do curso foi feita com o python 3.11 e testada para ser compat\u00edvel com a vers\u00e3o 3.12. Nesse momento de configura\u00e7\u00e3o est\u00e3o dispon\u00edveis as duas vers\u00f5es. Ao decorrer do curso, voc\u00ea pode se deparar com a vers\u00e3o 3.11 fixada no texto. Mas, sinta-se a vontade para alterar para vers\u00e3o 3.12.

"},{"location":"01/#gerenciamento-de-dependencias-com-poetry","title":"Gerenciamento de Depend\u00eancias com Poetry","text":"

Ap\u00f3s instalar o Python, o pr\u00f3ximo passo \u00e9 instalar o Poetry, um gerenciador de pacotes e depend\u00eancias para Python. O Poetry facilita a cria\u00e7\u00e3o, o gerenciamento e a distribui\u00e7\u00e3o de pacotes Python.

Caso esse seja seu primeiro contato com o Poetry

Temos uma live de python explicando somente ele:

Link direto

Para instalar o Poetry, voc\u00ea pode seguir as instru\u00e7\u00f5es presentes na documenta\u00e7\u00e3o oficial do Poetry para o seu sistema operacional. Alternativamente, se voc\u00ea optou por usar o pipx, pode instalar o Poetry com o seguinte comando:

$ Execu\u00e7\u00e3o no terminal!
pipx install poetry\n
Caso queira usar o pipx e n\u00e3o o tenha instalado no seu ambiente

O pipx \u00e9 uma forma de instalar pacotes de forma global no seu sistema sem que eles interfiram no seu ambiente global do python. Ele cria um ambiente virtual isolado para cada ferramenta.

O guia de instala\u00e7\u00e3o do pipx contempla diversos sistemas operacionais: guia

"},{"location":"01/#criacao-do-projeto-fastapi-e-instalacao-das-dependencias","title":"Cria\u00e7\u00e3o do Projeto FastAPI e Instala\u00e7\u00e3o das Depend\u00eancias","text":"

Agora que temos o Python e o Poetry prontos, podemos come\u00e7ar a criar nosso projeto FastAPI.

Inicialmente criaremos um novo projeto python usando o Poetry, com o comando poetry new e em seguida navegaremos at\u00e9 o diret\u00f3rio criado:

$ Execu\u00e7\u00e3o no terminal!
poetry new fast_zero\ncd fast_zero\n

Ele criar\u00e1 uma estrutura de arquivos e pastas como essa:

.\n\u251c\u2500\u2500 fast_zero\n\u2502  \u2514\u2500\u2500 __init__.py\n\u251c\u2500\u2500 pyproject.toml\n\u251c\u2500\u2500 README.md\n\u2514\u2500\u2500 tests\n   \u2514\u2500\u2500 __init__.py\n
Vers\u00e3o 3.11Vers\u00e3o 3.12

Para que a vers\u00e3o do python que instalamos via pyenv seja usada em nosso projeto criado com poetry, devemos dizer ao pyenv qual vers\u00e3o do python ser\u00e1 usada nesse diret\u00f3rio:

$ Execu\u00e7\u00e3o no terminal!
pyenv local 3.11.9  # (1)!\n
  1. Essa era a maior vers\u00e3o do 3.11 quando escrevi

Esse comando criar\u00e1 um arquivo oculto chamado .python-version na raiz do nosso projeto:

.python-version
3.11.9\n

Esse arquivo far\u00e1 com que toda vez que o terminal for aberto nesse diret\u00f3rio, o pyenv use a vers\u00e3o descrita no arquivo quando o python interpretador for chamado.

Em conjunto com essa instru\u00e7\u00e3o, devemos dizer ao poetry que usaremos exatamente a vers\u00e3o 3.11 em nosso projeto. Para isso alteraremos o arquivo de configura\u00e7\u00e3o do projeto o pyproject.toml na raiz do projeto:

pyproject.toml
[tool.poetry.dependencies]\npython = \"3.11.*\"  # (1)!\n
  1. .* quer dizer qualquer vers\u00e3o da 3.11

Para que a vers\u00e3o do python que instalamos via pyenv seja usada em nosso projeto criado com poetry, devemos dizer ao pyenv qual vers\u00e3o do python ser\u00e1 usada nesse diret\u00f3rio:

$ Execu\u00e7\u00e3o no terminal!
pyenv local 3.12.3  # (1)!\n
  1. Essa era a maior vers\u00e3o do 3.12 quando escrevi

Esse comando criar\u00e1 um arquivo oculto chamado .python-version na raiz do nosso projeto:

.python-version
3.12.3\n

Esse arquivo far\u00e1 com que toda vez que o terminal for aberto nesse diret\u00f3rio, o pyenv use a vers\u00e3o descrita no arquivo quando o python interpretador for chamado.

Em conjunto com essa instru\u00e7\u00e3o, devemos dizer ao poetry que usaremos exatamente a vers\u00e3o 3.12 em nosso projeto. Para isso alteraremos o arquivo de configura\u00e7\u00e3o do projeto o pyproject.toml na raiz do projeto:

pyproject.toml
[tool.poetry.dependencies]\npython = \"3.12.*\"  # (1)!\n
  1. .* quer dizer qualquer vers\u00e3o da 3.12

Desta forma, temos uma vers\u00e3o do python selecionada para esse projeto e uma garantia que o poetry usar\u00e1 essa vers\u00e3o para a cria\u00e7\u00e3o do nosso ambiente virtual.

Em seguida, inicializaremos nosso ambiente virtual com Poetry e instalaremos o FastAPI:

$ Execu\u00e7\u00e3o no terminal!
poetry install # (1)!\npoetry add 'fastapi[standard]' # (2)!\n
  1. Cria o ambiente virtual (venv)
  2. Adiciona o FastAPI no nosso ambiente virtual
"},{"location":"01/#primeira-execucao-de-um-hello-world","title":"Primeira Execu\u00e7\u00e3o de um \"Hello, World!\"","text":"

Uma coisa bastante interessante sobre o FastAPI \u00e9 que ele \u00e9 um framework web baseado em fun\u00e7\u00f5es. Da mesma forma em que criamos fun\u00e7\u00f5es tradicionalmente em python, podemos estender essas fun\u00e7\u00f5es para que elas sejam servidas pelo servidor. Por exemplo:

fast_zero/app.py
def read_root():\n    return {'message': 'Ol\u00e1 Mundo!'}\n

Essa fun\u00e7\u00e3o em python basicamente retorna um dicion\u00e1rio com uma chave chamada 'message' e uma mensagem 'Ol\u00e1 Mundo!'. Se adicionarmos essa fun\u00e7\u00e3o em novo arquivo chamado app.py no diret\u00f3rio fast_zero. Podemos fazer a chamada dela pelo terminal interativo (REPL):

>>> Terminal interativo!
>>> read_root()\n{'message': 'Ol\u00e1 Mundo!'}\n

De forma tradicional, como todas as fun\u00e7\u00f5es em python.

Dica: Como abrir o terminal interativo (REPL)

Para abrir o terminal interativo com o seu c\u00f3digo carregado, voc\u00ea deve chamar o Python no terminal usando -i:

$ Execu\u00e7\u00e3o no terminal!
python -i <seu_arquivo.py>\n

O interpretador do Python executa o c\u00f3digo do arquivo e retorna o shell ap\u00f3s executar tudo que est\u00e1 escrito no arquivo.

Para o nosso caso espec\u00edfico, como o nome do arquivo \u00e9 fast_zero/app.py, devemos executar esse comando no terminal:

$ Execu\u00e7\u00e3o no terminal!
python -i fast_zero/app.py\n

Desta forma, usando somente um decorador do FastAPI, podemos fazer com que uma determinada fun\u00e7\u00e3o seja acess\u00edvel pela rede:

fast_zero/app.py
from fastapi import FastAPI # (1)!\n\napp = FastAPI()  # (2)!\n\n@app.get('/')  # (3)!\ndef read_root():  # (4)!\n    return {'message': 'Ol\u00e1 Mundo!'} # (5)!\n
  1. Importando da biblioteca fastapi o objeto FastAPI
  2. Iniciando uma aplica\u00e7\u00e3o FastAPI
  3. Definindo um endpoint com o endere\u00e7o / acess\u00edvel pelo m\u00e9todo HTTP GET
  4. Fun\u00e7\u00e3o que ser\u00e1 executada quando o endere\u00e7o / for acessado por um cliente
  5. Os dados que ser\u00e3o retornados pelo endere\u00e7o quando for chamado

A linha em destaque @app.get('/') exp\u00f5em a nossa fun\u00e7\u00e3o para ser servida pelo FastAPI. Dizendo que quando um cliente acessar o nosso endere\u00e7o de rede no caminho /, usando o m\u00e9todo HTTP GET2, a fun\u00e7\u00e3o ser\u00e1 executada. Desta maneira, temos todo o c\u00f3digo necess\u00e1rio para criar nossa primeira aplica\u00e7\u00e3o web com FastAPI.

Antes de iniciarmos nossa aplica\u00e7\u00e3o, temos que fazer um passo importante, habilitar o ambiente virtual, para que o python consiga enxergar nossas depend\u00eancias instaladas. O poetry tem um comando espec\u00edfico para isso:

$ Execu\u00e7\u00e3o no terminal!
poetry shell\n

Agora com o ambiente virtual ativo, podemos iniciar nosso servidor FastAPI para iniciar nossa aplica\u00e7\u00e3o:

$ Execu\u00e7\u00e3o no terminal!
fastapi dev fast_zero/app.py\n

Esse comando diz ao FastAPI para iniciar o servidor de desenvolvimento (dev) usando o arquivo fast_zero/app.py

A resposta do comando no terminal deve ser parecida com essa:

Resposta do comando `fastapi dev fast_zero/app.py`
INFO     Using path fast_zero/app.py\nINFO     Resolved absolute path /home/dunossauro/git/fastapi-do-zero/codigo_das_aulas/01/fast_zero/app.py\nINFO     Searching for package file structure from directories with __init__.py files\nINFO     Importing from /home/dunossauro/git/fastapi-do-zero/codigo_das_aulas/01\n\n \u256d\u2500 Python package file structure \u2500\u256e\n \u2502                                 \u2502\n \u2502  \ud83d\udcc1 fast_zero                   \u2502\n \u2502  \u251c\u2500\u2500 \ud83d\udc0d __init__.py             \u2502\n \u2502  \u2514\u2500\u2500 \ud83d\udc0d app.py                  \u2502\n \u2502                                 \u2502\n \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\n\nINFO     Importing module fast_zero.app\nINFO     Found importable FastAPI app\n\n \u256d\u2500\u2500\u2500\u2500 Importable FastAPI app \u2500\u2500\u2500\u2500\u2500\u256e\n \u2502                                 \u2502\n \u2502  from fast_zero.app import app  \u2502\n \u2502                                 \u2502\n \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\n\nINFO     Using import string fast_zero.app:app\n\n \u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 FastAPI CLI - Development mode \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\n \u2502                                                     \u2502\n \u2502  Serving at: http://127.0.0.1:8000                  \u2502\n \u2502                                                     \u2502\n \u2502  API docs: http://127.0.0.1:8000/docs               \u2502\n \u2502                                                     \u2502\n \u2502  Running in development mode, for production use:   \u2502\n \u2502                                                     \u2502\n \u2502  fastapi run                                        \u2502\n \u2502                                                     \u2502\n \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\n\nINFO:     Will watch for changes in these directories: ['/home/dunossauro/git/fastapi-do-zero/codigo_das_aulas/01']\nINFO:     Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)\nINFO:     Started reloader process [893203] using WatchFiles\nINFO:     Started server process [893207]\nINFO:     Waiting for application startup.\nINFO:     Application startup complete.\n

A mensagem de resposta do CLI: serving: http://127.0.0.1:8000 tem uma informa\u00e7\u00e3o bastante importante.

  1. Ela nos diz qual o protocolo que ele est\u00e1 servido HTTP, o protocolo padr\u00e3o da web;
  2. O endere\u00e7o de rede (IP) no qual ele est\u00e1 escutando 127.0.0.1, endere\u00e7o especial (loopback) que aponta para a nossa pr\u00f3pria m\u00e1quina;
  3. A porta :8000, a qual \u00e9 a porta da nossa m\u00e1quina que est\u00e1 reservada para nossa aplica\u00e7\u00e3o.

Agora, com o servidor inicializado, podemos usar um cliente para acessar o endere\u00e7o http://127.0.0.1:8000.

O cliente mais tradicional da web \u00e9 o navegador, podemos digitar o endere\u00e7o na barra de navega\u00e7\u00e3o e se tudo ocorreu corretamente, voc\u00ea deve ver a mensagem \"Ol\u00e1 Mundo!\" em formato JSON.

Para parar a execu\u00e7\u00e3o do fastapi no shell, voc\u00ea pode digitar Ctrl+C e a mensagem Shutting down aparecer\u00e1 mostrando que o servidor foi finalizado.

Diferentes clientes para nossa aplica\u00e7\u00e3o

Caso exista uma curiosidade sobre outros clientes HTTP que n\u00e3o o browser, podemos usar aplica\u00e7\u00f5es de linha de comando como tradicional curl:

$ Execu\u00e7\u00e3o no terminal!
curl 127.0.0.1:8000\n{\"message\":\"Ol\u00e1 Mundo!\"}\n

Ou o meu cliente HTTP preferido (escrito em python), o HTTPie:

$ Execu\u00e7\u00e3o no terminal!
http 127.0.0.1:8000\nHTTP/1.1 200 OK\ncontent-length: 25\ncontent-type: application/json\ndate: Thu, 11 Jan 2024 11:46:32 GMT\nserver: uvicorn\n\n{\n    \"message\": \"Ol\u00e1 Mundo!\"\n}\n

Existem at\u00e9 mesmo aplica\u00e7\u00f5es gr\u00e1ficas de c\u00f3digo aberto pensadas para serem clientes HTTP para APIs. Como o hoppscotch:

Ou como o Bruno:

"},{"location":"01/#uvicorn","title":"Uvicorn","text":"

O FastAPI \u00e9 \u00f3timo para criar APIs, mas n\u00e3o pode disponibiliz\u00e1-las na rede sozinho. Embora o FastAPI tenha uma aplica\u00e7\u00e3o de terminal que facilita a execu\u00e7\u00e3o. Para podermos acessar essas APIs por um navegador ou de outras aplica\u00e7\u00f5es clientes, \u00e9 necess\u00e1rio um servidor. \u00c9 a\u00ed que o Uvicorn entra em cena. Ele atua como esse servidor, disponibilizando a API do FastAPI em rede. Isso permite que a API seja acessada de outros dispositivos ou programas.

Como notamos na resposta do comando fastapi dev fast_zero/app.py:

Fim da resposta de `fastapi dev fast_zero/app.py`
INFO:     Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)\nINFO:     Started reloader process [893203] using WatchFiles\nINFO:     Started server process [893207]\nINFO:     Waiting for application startup.\nINFO:     Application startup complete.\n

Sempre que usarmos o fastapi para inicializar a aplica\u00e7\u00e3o no shell, ele faz uma chamada interna para inicializar o uvicorn. Por esse motivo ele aparece nas respostas HTTP e tamb\u00e9m na execu\u00e7\u00e3o do comando.

Voc\u00ea poderia chamar a aplica\u00e7\u00e3o diretamente pelo Uvicorn tamb\u00e9m

$ Execu\u00e7\u00e3o no terminal!
uvicorn fast_zero.app:app\n
Esse comando diz ao uvicorn o seguinte: na pasta fast_zero existe um arquivo chamado app. Dentro desse arquivo, temos uma aplica\u00e7\u00e3o para ser servida com o nome de app. O comando \u00e9 composto por uvicorn pasta.arquivo:vari\u00e1vel.

A resposta do comando no terminal deve ser parecida com essa:

Resultado do comando
INFO:     Started server process [127946]\nINFO:     Waiting for application startup.\nINFO:     Application startup complete.\nINFO:     Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)\n
"},{"location":"01/#instalando-as-ferramentas-de-desenvolvimento","title":"Instalando as ferramentas de desenvolvimento","text":"

As escolhas de ferramentas de desenvolvimento, de forma geral, s\u00e3o escolhas bem particulares. N\u00e3o costumam ser consensuais nem mesmo em times de desenvolvimento. Dito isso, selecionei algumas ferramentas que gosto de usar e alinhadas com a utilidade que elas apresentam no desenvolvimento do projeto.

As ferramentas escolhidas s\u00e3o:

Para instalar essas ferramentas que usaremos em desenvolvimento, podemos usar um grupo (--group dev) do poetry focado nelas, para n\u00e3o serem instaladas quando nossa aplica\u00e7\u00e3o estiver em produ\u00e7\u00e3o:

$ Execu\u00e7\u00e3o no terminal!
poetry add --group dev pytest pytest-cov taskipy ruff httpx\n

O HTTPX foi inclu\u00eddo, pois ele \u00e9 uma depend\u00eancia do cliente de testes do FastAPI.

"},{"location":"01/#configurando-as-ferramentas-de-desenvolvimento","title":"Configurando as ferramentas de desenvolvimento","text":"

Ap\u00f3s a instala\u00e7\u00e3o das ferramentas de desenvolvimento, precisamos definir as configura\u00e7\u00f5es de cada uma individualmente no arquivo pyproject.toml.

"},{"location":"01/#ruff","title":"Ruff","text":"

O Ruff \u00e9 uma ferramenta moderna em python, escrita em rust, compat\u00edvel2 com os projetos de an\u00e1lise est\u00e1tica escritos e mantidos originalmente pela comunidade no projeto PYCQA3 e tem duas fun\u00e7\u00f5es principais:

  1. Analisar o c\u00f3digo de forma est\u00e1tica (Linter): Efetuar a verifica\u00e7\u00e3o se estamos programando de acordo com boas pr\u00e1ticas do python.
  2. Formatar o c\u00f3digo (Formatter): Efetuar a verifica\u00e7\u00e3o do c\u00f3digo para padronizar um estilo de c\u00f3digo pr\u00e9-definido.

Para configurar o ruff montamos a configura\u00e7\u00e3o em 3 tabelas distintas no arquivo pyproject.toml. Uma para as configura\u00e7\u00f5es globais, uma para o linter e uma para o formatador.

Configura\u00e7\u00e3o global

Na configura\u00e7\u00e3o global do Ruff queremos alterar somente duas coisas. O comprimento de linha para 79 caracteres (conforme sugerido na PEP-8) e em seguida, informaremos que o diret\u00f3rio de migra\u00e7\u00f5es de banco de dados ser\u00e1 ignorado na checagem e na formata\u00e7\u00e3o:

pyproject.toml
[tool.ruff]\nline-length = 79\nextend-exclude = ['migrations']\n
Nota sobre \"migrations\"

Nessa fase de configura\u00e7\u00e3o, excluiremos a pasta migrations, isso pode n\u00e3o fazer muito sentido nesse momento. Contudo, quando iniciarmos o trabalho com o banco de dados, a ferramenta Alembic faz gera\u00e7\u00e3o de c\u00f3digo autom\u00e1tico. Por serem c\u00f3digos gerados automaticamente, n\u00e3o queremos alterar a configura\u00e7\u00e3o feita por ela.

Linter

Durante a an\u00e1lise est\u00e1tica do c\u00f3digo, queremos buscar por coisas espec\u00edficas. No Ruff, precisamos dizer exatamente o que ele deve analisar. Isso \u00e9 feito por c\u00f3digos. Usaremos estes:

pyproject.toml
[tool.ruff.lint]\npreview = true\nselect = ['I', 'F', 'E', 'W', 'PL', 'PT']\n

Para mais informa\u00e7\u00f5es sobre a configura\u00e7\u00e3o e sobre os c\u00f3digos do ruff e dos projetos do PyCQA, voc\u00ea pode checar a documenta\u00e7\u00e3o do ruff ou as documenta\u00e7\u00f5es originais dos projetos PyQCA.

Formatter

A formata\u00e7\u00e3o do Ruff praticamente n\u00e3o precisa ser alterada. Pois ele vai seguir as boas pr\u00e1ticas e usar a configura\u00e7\u00e3o global de 79 caracteres por linha. A \u00fanica altera\u00e7\u00e3o que farei \u00e9 o uso de aspas simples ' no lugar de aspas duplas \":

pyproject.toml
[tool.ruff.format]\npreview = true\nquote-style = 'single'\n

Lembrando que a op\u00e7\u00e3o de usar aspas simples \u00e9 totalmente pessoal, voc\u00ea pode usar aspas duplas se quiser.

"},{"location":"01/#pytest","title":"pytest","text":"

O Pytest \u00e9 uma framework de testes, que usaremos para escrever e executar nossos testes. O configuraremos para reconhecer o caminho base para execu\u00e7\u00e3o dos testes na raiz do projeto .:

pyproject.toml
[tool.pytest.ini_options]\npythonpath = \".\"\naddopts = '-p no:warnings'\n

Na segunda linha dizemos para o pytest adicionar a op\u00e7\u00e3o no:warnings. Para ter uma visualiza\u00e7\u00e3o mais limpa dos testes, caso alguma biblioteca exiba uma mensagem de warning, isso ser\u00e1 suprimido pelo pytest.

"},{"location":"01/#taskipy","title":"Taskipy","text":"

A ideia do Taskipy \u00e9 ser um executor de tarefas (task runner) complementar em nossa aplica\u00e7\u00e3o. No lugar de ter que lembrar comandos como o do fastapi, que vimos na execu\u00e7\u00e3o da aplica\u00e7\u00e3o, que tal substituir ele simplesmente por task run?

Isso funcionaria para qualquer comando complicado em nossa aplica\u00e7\u00e3o. Simplificando as chamadas e tamb\u00e9m para n\u00e3o termos que lembrar de como executar todos os comandos de cabe\u00e7a.

Alguns comandos que criaremos agora no in\u00edcio:

pyproject.toml
[tool.taskipy.tasks]\nlint = 'ruff check .; ruff check . --diff'\nformat = 'ruff check . --fix; ruff format .'\nrun = 'fastapi dev fast_zero/app.py'\npre_test = 'task lint'\ntest = 'pytest -s -x --cov=fast_zero -vv'\npost_test = 'coverage html'\n

Os comandos definidos fazem o seguinte:

Para executar um comando, \u00e9 bem mais simples, precisando somente passar a palavra task <comando>.

Caso precise ver o arquivo todo

O meu est\u00e1 exatamente assim:

pyproject.toml
[tool.poetry]\nname = \"fast-zero\"\nversion = \"0.1.0\"\ndescription = \"\"\nauthors = [\"Your Name <you@example.com>\"]\nreadme = \"README.md\"\n\n[tool.poetry.dependencies]\npython = \"3.12.*\" # ou \"3.11.*\"\nfastapi = \"^0.113.0\"\n\n[tool.poetry.group.dev.dependencies]\npytest = \"^8.3.2\"\npytest-cov = \"^5.0.0\"\ntaskipy = \"^1.13.0\"\nruff = \"^0.6.4\"\nhttpx = \"^0.27.2\"\n\n[tool.ruff]\nline-length = 79\nextend-exclude = ['migrations']\n\n[tool.ruff.lint]\npreview = true\nselect = ['I', 'F', 'E', 'W', 'PL', 'PT']\n\n[tool.ruff.format]\npreview = true\nquote-style = 'single'\n\n[tool.pytest.ini_options]\npythonpath = \".\"\naddopts = '-p no:warnings'\n\n[tool.taskipy.tasks]\nlint = 'ruff check .; ruff check . --diff'\nformat = 'ruff check . --fix; ruff format .'\nrun = 'fastapi dev fast_zero/app.py'\npre_test = 'task lint'\ntest = 'pytest -s -x --cov=fast_zero -vv'\npost_test = 'coverage html'\n\n[build-system]\nrequires = [\"poetry-core\"]\nbuild-backend = \"poetry.core.masonry.api\"\n

Um ponto importante \u00e9 que as vers\u00f5es dos pacotes podem variar dependendo da data em que voc\u00ea fizer a instala\u00e7\u00e3o dos pacotes. Esse arquivo \u00e9 somente um exemplo.

"},{"location":"01/#os-efeitos-dessas-configuracoes-de-desenvolvimento","title":"Os efeitos dessas configura\u00e7\u00f5es de desenvolvimento","text":"

Caso voc\u00ea tenha copiado o c\u00f3digo que usamos para definir fast_zero/app.py, pode testar os comandos que criamos para o taskipy:

$ Execu\u00e7\u00e3o no terminal!
task lint\n

Dessa forma, veremos que cometemos algumas infra\u00e7\u00f5es na formata\u00e7\u00e3o da PEP-8. O ruff nos informar\u00e1 que dever\u00edamos ter adicionado duas linhas antes de uma defini\u00e7\u00e3o de fun\u00e7\u00e3o:

fast_zero/app.py:5:1: E302 [*] Expected 2 blank lines, found 1\nFound 1 error.\n[*] 1 fixable with the `--fix` option.\n--- fast_zero/app.py\n+++ fast_zero/app.py\n@@ -2,6 +2,7 @@\n\n app = FastAPI()\n\n+\n @app.get('/')\n def read_root():\n     return {'message': 'Ol\u00e1 Mundo!'}\n\nWould fix 1 error.\n

Para corrigir isso, podemos usar o nosso comando de formata\u00e7\u00e3o de c\u00f3digo:

ComandoResultado $ Execu\u00e7\u00e3o no terminal!
task format\nFound 1 error (1 fixed, 0 remaining).\n3 files left unchanged\n
fast_zero/app.py
from fastapi import FastAPI\n\napp = FastAPI()\n\n\n@app.get('/')\ndef read_root():\n    return {'message': 'Ol\u00e1 Mundo!'}\n
"},{"location":"01/#introducao-ao-pytest-testando-o-hello-world","title":"Introdu\u00e7\u00e3o ao Pytest: Testando o \"Hello, World!\"","text":"

Antes de entendermos a din\u00e2mica dos testes, precisamos entender o efeito que eles t\u00eam no nosso c\u00f3digo. Podemos come\u00e7ar analisando a cobertura (o quanto do nosso c\u00f3digo est\u00e1 sendo efetivamente testado). Vamos executar os testes:

$ Execu\u00e7\u00e3o no terminal!
task test\n

Teremos uma resposta como essa:

$ Execu\u00e7\u00e3o no terminal!
=========================== test session starts ===========================\nplatform linux -- Python 3.11.7, pytest-7.4.4, pluggy-1.3.0\ncachedir: .pytest_cache\nrootdir: /home/dunossauro/git/fast_zero\nconfigfile: pyproject.toml\nplugins: cov-4.1.0, anyio-4.2.0\ncollected 0 items\n\n---------- coverage: platform linux, python 3.11.7-final-0 -----------\nName                    Stmts   Miss  Cover\n-------------------------------------------\nfast_zero/__init__.py       0      0   100%\nfast_zero/app.py            5      5     0%\n-------------------------------------------\nTOTAL                       5      5     0%\n

As linhas no terminal s\u00e3o referentes ao pytest, que disse que coletou 0 itens. Nenhum teste foi executado.

Caso n\u00e3o tenha muita experi\u00eancia com Pytest

Temos uma live de Python explicando os conceitos b\u00e1sicos da biblioteca

Link direto

A parte importante dessa Mensagem est\u00e1 na tabela gerada pelo coverage. Que diz que temos 5 linhas de c\u00f3digo (Stmts) no arquivo fast_zero/app.py e nenhuma delas est\u00e1 coberta pelos nossos testes. Como podemos ver na coluna Miss.

Por n\u00e3o encontrar nenhum teste, o pytest retornou um \"erro\". Isso significa que nossa tarefa post_test n\u00e3o foi executada. Podemos execut\u00e1-la manualmente:

$ Execu\u00e7\u00e3o no terminal!
task post_test\nWrote HTML report to htmlcov/index.html\n

Isso gera um relat\u00f3rio de cobertura de testes em formato HTML. Podemos abrir esse arquivo em nosso navegador e entender exatamente quais linhas do c\u00f3digo n\u00e3o est\u00e3o sendo testadas.

Se clicarmos no arquivo fast_zero/app.py podemos ver em vermelho as linhas que n\u00e3o est\u00e3o sendo testadas:

Isto significa que precisamos testar todo esse arquivo.

"},{"location":"01/#escrevendo-o-teste","title":"Escrevendo o teste","text":"

Agora, escreveremos nosso primeiro teste com Pytest. Mas, antes de escrever o teste, precisamos criar um arquivo espec\u00edfico para eles. Na pasta tests, vamos criar um arquivo chamado test_app.py.

Por conven\u00e7\u00e3o, todos os arquivos de teste do pytest devem iniciar com um prefixo test_.py

Para testar o c\u00f3digo feito com FastAPI, precisamos de um cliente de teste. A grande vantagem \u00e9 que o FastAPI j\u00e1 conta com um cliente de testes no m\u00f3dulo fastapi.testclient com o objeto TestClient, que precisa receber nosso app como par\u00e2metro:

tests/test_app.py
from fastapi.testclient import TestClient  # (1)!\n\nfrom fast_zero.app import app  # (2)!\n\nclient = TestClient(app)  # (3)!\n
  1. Importa do m\u00f3dulo testclient o objeto TestClient
  2. Importa o nosso app definido em fast_zero
  3. Cria um cliente de testes usando a nossa aplica\u00e7\u00e3o como base

S\u00f3 o fato de termos definido um cliente, j\u00e1 nos mostra uma cobertura bastante diferente:

$ Execu\u00e7\u00e3o no terminal!
task test\n# parte da mensagem foi omitida\ncollected 0 items\n\n---------- coverage: platform linux, python 3.11.3-final-0 -----------\nName                    Stmts   Miss  Cover\n-------------------------------------------\nfast_zero/__init__.py       0      0   100%\nfast_zero/app.py            5      1    80%\n-------------------------------------------\nTOTAL                       5      1    80%\n

Por n\u00e3o coletar nenhum teste, o pytest ainda retornou um \"erro\". Para ver a cobertura, precisaremos executar novamente o post_test manualmente:

$ Execu\u00e7\u00e3o no terminal!
task post_test\nWrote HTML report to htmlcov/index.html\n

No navegador, podemos ver que a \u00fanica linha n\u00e3o \"testada\" \u00e9 aquela onde temos a l\u00f3gica (o corpo) da fun\u00e7\u00e3o read_root. As linhas de defini\u00e7\u00e3o est\u00e3o todas verdes:

No verde vemos o que foi executado quando chamamos o teste, no vermelho o que n\u00e3o foi.

Para resolver isso, temos que criar um teste de fato, fazendo uma chamada para nossa API usando o cliente de teste que definimos:

tests/test_app.py
from http import HTTPStatus  # (6)!\n\nfrom fastapi.testclient import TestClient\n\nfrom fast_zero.app import app\n\n\ndef test_root_deve_retornar_ok_e_ola_mundo():  # (1)!\n    client = TestClient(app)  # (2)!\n\n    response = client.get('/')  # (3)!\n\n    assert response.status_code == HTTPStatus.OK  # (4)!\n    assert response.json() == {'message': 'Ol\u00e1 Mundo!'}  # (5)!\n
  1. No nome da fun\u00e7\u00e3o, geralmente escrevemos o que o teste de fato faz. Aqui estamos dizendo que root deve retornar o status OK e a mensagem \"ol\u00e1 mundo\". Root \u00e9 o nome dado a raiz da URL. O caminho /, que colocamos na defini\u00e7\u00e3o do @app.get('/'). OK \u00e9 o status que diz que a requisi\u00e7\u00e3o aconteceu com sucesso no protocolo HTTP.
  2. Aqui criamos o cliente de teste do nosso app
  3. Nesse ponto, o client faz uma requisi\u00e7\u00e3o. Da mesma forma que o browser, um cliente da API. Nisso, chamamos o endere\u00e7o de root, usando o m\u00e9todo GET.
  4. Aqui fazemos a valida\u00e7\u00e3o do c\u00f3digo de resposta, para saber se a resposta \u00e9 referente ao c\u00f3digo 200, que significa OK. Mais informa\u00e7\u00f5es sobre esse t\u00f3pico aqui.
  5. No final, validamos se o dicion\u00e1rio que enviamos na fun\u00e7\u00e3o \u00e9 o mesmo que recebemos quando fizemos a requisi\u00e7\u00e3o.
  6. Importa\u00e7\u00e3o da biblioteca nativa do python que abstrai os c\u00f3digos de resposta e nos apresenta o status. Mais informa\u00e7\u00f5es sobre esse t\u00f3pico aqui.

Esse teste faz uma requisi\u00e7\u00e3o GET no endpoint / e verifica se o c\u00f3digo de status da resposta \u00e9 200 e se o conte\u00fado da resposta \u00e9 {'message': 'Ol\u00e1 Mundo!'}.

$ Execu\u00e7\u00e3o no terminal!
task test\n# parte da mensagem foi omitida\ncollected 1 item\n\ntests/test_app.py::test_root_deve_retornar_ok_e_ola_mundo PASSED\n\n---------- coverage: platform linux, python 3.11.3-final-0 -----------\nName                    Stmts   Miss  Cover\n-------------------------------------------\nfast_zero/__init__.py       0      0   100%\nfast_zero/app.py            5      0   100%\n-------------------------------------------\nTOTAL                       5      0   100%\n\n================ 1 passed in 1.39s ================\nWrote HTML report to htmlcov/index.html\n

Dessa forma, temos um teste que coletou 1 item (1 teste). Esse teste foi aprovado e a cobertura n\u00e3o deixou de abranger nenhuma linha de c\u00f3digo.

Como conseguimos coletar um item, o post_test foi executado e tamb\u00e9m gerou um HTML com a cobertura atualizada.

"},{"location":"01/#estrutura-de-um-teste","title":"Estrutura de um teste","text":"

Agora que escrevemos nosso primeiro teste de forma intuitiva, podemos entender o que cada passo do teste faz. Essa compreens\u00e3o \u00e9 vital, pois nos ajudar\u00e1 a escrever testes com mais confian\u00e7a e efic\u00e1cia. Para desvendar o m\u00e9todo por tr\u00e1s da nossa abordagem, exploraremos uma estrat\u00e9gia conhecida como AAA, que divide o teste em tr\u00eas fases distintas: Arrange, Act, Assert.

Caso fazer testes ainda seja complicado para voc\u00ea

Temos uma live de python focada em ensinar os primeiros passos no mundo dos testes.

Link direto

Para analisar todas as etapas de um teste, usaremos como exemplo este primeiro teste que escrevemos:

tests/test_app.py
from http import HTTPStatus\n\nfrom fastapi.testclient import TestClient\n\nfrom fast_zero.app import app\n\n\ndef test_root_deve_retornar_ok_e_ola_mundo():\n    client = TestClient(app)  # Arrange\n\n    response = client.get('/')  # Act\n\n    assert response.status_code == HTTPStatus.OK  # Assert\n    assert response.json() == {'message': 'Ol\u00e1 Mundo!'}  # Assert\n

Com base nesse c\u00f3digo, podemos observar as tr\u00eas fases:

"},{"location":"01/#fase-1-organizar-arrange","title":"Fase 1 - Organizar (Arrange)","text":"

Nesta primeira etapa, estamos preparando o ambiente para o teste. No exemplo, a linha com o coment\u00e1rio Arrange n\u00e3o \u00e9 o teste em si, ela monta o ambiente para que o teste possa ser executado. Estamos configurando um client de testes para fazer a requisi\u00e7\u00e3o ao app.

"},{"location":"01/#fase-2-agir-act","title":"Fase 2 - Agir (Act)","text":"

Aqui \u00e9 a etapa onde acontece a a\u00e7\u00e3o principal do teste, que consiste em chamar o Sistema Sob Teste (SUT). No nosso caso, o SUT \u00e9 a rota /, e a a\u00e7\u00e3o \u00e9 representada pela linha response = client.get('/'). Estamos exercitando a rota e armazenando sua resposta na vari\u00e1vel response. \u00c9 a fase em que o c\u00f3digo de testes executa o c\u00f3digo de produ\u00e7\u00e3o que est\u00e1 sendo testado. Agir aqui significa interagir diretamente com a parte do sistema que queremos avaliar, para ver como ela se comporta.

"},{"location":"01/#fase-3-afirmar-assert","title":"Fase 3 - Afirmar (Assert)","text":"

Esta \u00e9 a etapa de verificar se tudo correu como esperado. \u00c9 f\u00e1cil notar onde estamos fazendo a verifica\u00e7\u00e3o, pois essa linha sempre tem a palavra reservada assert. A verifica\u00e7\u00e3o \u00e9 booleana, ou est\u00e1 correta, ou n\u00e3o est\u00e1. Por isso, um teste deve sempre incluir um assert para verificar se o comportamento esperado est\u00e1 correto.

Agora que compreendemos o que cada linha de teste faz em espec\u00edfico, podemos nos orientar de forma clara nos testes que escreveremos no futuro. Cada uma das linhas usadas tem uma raz\u00e3o de estar no teste, e conhecer essa estrutura n\u00e3o s\u00f3 nos d\u00e1 uma compreens\u00e3o mais profunda do que estamos fazendo, mas tamb\u00e9m nos d\u00e1 confian\u00e7a para explorar e escrever testes mais complexos.

"},{"location":"01/#criando-nosso-repositorio-no-git","title":"Criando nosso reposit\u00f3rio no git","text":"

Antes de concluirmos a aula, precisamos executar alguns passos:

  1. Criar um arquivo .gitignore para n\u00e3o adicionar o ambiente virtual e outros arquivos desnecess\u00e1rios no versionamento de c\u00f3digo.
  2. Criar um novo reposit\u00f3rio no GitHub para versionar o c\u00f3digo.
  3. Subir o c\u00f3digo que fizemos para o GitHub.

Criando o arquivo .gitignore

Vamos iniciar com a cria\u00e7\u00e3o de um arquivo .gitignore espec\u00edfico para Python. Existem diversos modelos dispon\u00edveis na internet, como os dispon\u00edveis pelo pr\u00f3prio GitHub, ou o gitignore.io. Uma ferramenta \u00fatil \u00e9 a ignr, feita em Python, que faz o download autom\u00e1tico do arquivo para a nossa pasta de trabalho atual:

$ Execu\u00e7\u00e3o no terminal!
ignr -p python > .gitignore\n

O .gitignore \u00e9 importante porque ele nos ajuda a evitar que arquivos desnecess\u00e1rios ou sens\u00edveis sejam enviados para o reposit\u00f3rio. Isso inclui o ambiente virtual, arquivos de configura\u00e7\u00e3o pessoal, entre outros.

Criando um reposit\u00f3rio no github

Agora, com nossos arquivos indesejados ignorados, podemos iniciar o versionamento de c\u00f3digo usando o git. Para criar um reposit\u00f3rio local, usamos o comando git init .. Para criar esse reposit\u00f3rio no GitHub, utilizaremos o gh, um utilit\u00e1rio de linha de comando que nos auxilia nesse processo:

$ Execu\u00e7\u00e3o no terminal!
git init .\ngh repo create\n

Ao executar gh repo create, algumas informa\u00e7\u00f5es ser\u00e3o solicitadas, como o nome do reposit\u00f3rio e se ele ser\u00e1 p\u00fablico ou privado. Isso ir\u00e1 criar um reposit\u00f3rio tanto localmente quanto no GitHub.

Subindo nosso c\u00f3digo para o github

Com o reposit\u00f3rio pronto, vamos versionar nosso c\u00f3digo. Primeiro, adicionamos o c\u00f3digo ao pr\u00f3ximo commit com git add .. Em seguida, criamos um ponto na hist\u00f3ria do projeto com git commit -m \"Configura\u00e7\u00e3o inicial do projeto\". Por fim, sincronizamos o reposit\u00f3rio local com o remoto no GitHub usando git push:

$ Execu\u00e7\u00e3o no terminal!
git add .\ngit commit -m \"Configura\u00e7\u00e3o inicial do projeto\"\ngit push\n

Caso seja a primeira vez que est\u00e1 utilizando o git push, talvez seja necess\u00e1rio configurar suas credenciais do GitHub.

Esses passos garantem que todo o c\u00f3digo criado na aula esteja versionado e dispon\u00edvel para compartilhamento no GitHub.

"},{"location":"01/#exercicio","title":"Exerc\u00edcio","text":"
  1. Crie um reposit\u00f3rio para acompanhar o curso e suba em alguma plataforma, como Github, gitlab, codeberg, etc. E compartilhe o link no reposit\u00f3rio do curso para podermos aprender juntos.

Exerc\u00edcios resolvidos

"},{"location":"01/#conclusao","title":"Conclus\u00e3o","text":"

Pronto! Agora temos um ambiente de desenvolvimento totalmente configurado para come\u00e7ar a trabalhar com FastAPI e j\u00e1 fizemos nossa primeira imers\u00e3o no Desenvolvimento Orientado por Testes. Na pr\u00f3xima aula nos aprofundaremos na estrutura\u00e7\u00e3o da nossa aplica\u00e7\u00e3o FastAPI. At\u00e9 l\u00e1!

Agora que a aula acabou, \u00e9 um bom momento para voc\u00ea relembrar alguns conceitos e fixar melhor o conte\u00fado respondendo ao question\u00e1rio referente a ela.

Quiz

  1. Voc\u00ea n\u00e3o precisa se preocupar com o docker inicialmente, ele ser\u00e1 usado da aula 10 em diante\u00a0\u21a9

  2. Em alguns casos existe uma diverg\u00eancia de opini\u00f5es em os linter mais tradicionais. Mas, em geral funciona bem.\u00a0\u21a9\u21a9

  3. Em vers\u00f5es antigas do texto us\u00e1vamos as ferramentas do PyCQA como o pylint e o isort.\u00a0\u21a9

"},{"location":"02/","title":"Introdu\u00e7\u00e3o ao desenvolvimento WEB","text":""},{"location":"02/#introducao-ao-desenvolvimento-web","title":"Introdu\u00e7\u00e3o ao desenvolvimento WEB","text":"

Objetivos dessa aula:

Caso prefira ver a aula em v\u00eddeo

Esse aula ainda n\u00e3o est\u00e1 dispon\u00edvel em formato de v\u00eddeo, somente em texto ou live!

Aula Slides C\u00f3digo Quiz Exerc\u00edcios

Boas-vindas \u00e0 segunda aula do nosso curso de FastAPI. Agora que j\u00e1 temos o ambiente preparado, com algum c\u00f3digo escrito e testado, \u00e9 o momento ideal para entendermos o que viemos fazer aqui. At\u00e9 este ponto, voc\u00ea j\u00e1 deve saber que o FastAPI \u00e9 um framework para desenvolvimento de aplica\u00e7\u00f5es web, mais especificamente para o desenvolvimento de APIs web. \u00c9 aqui que ter um bom referencial te\u00f3rico se torna importante para compreendermos exatamente o que o framework \u00e9 capaz de fazer.

"},{"location":"02/#a-web","title":"A web","text":"

Sempre que nos referimos a aplica\u00e7\u00f5es web, estamos falando de aplica\u00e7\u00f5es que funcionam em rede. Essa rede pode ser privativa, como a sua rede dom\u00e9stica ou uma rede empresarial, ou podemos estar nos referindo \u00e0 World Wide Web (WWW), comumente conhecida como \"internet\". A internet, que tem uma longa hist\u00f3ria iniciada na d\u00e9cada de 1960, possui diversos padr\u00f5es definidos e vem se aperfei\u00e7oando desde ent\u00e3o. Compreender completamente sua complexidade \u00e9 um desafio, especialmente 6 d\u00e9cadas ap\u00f3s seu in\u00edcio.

Quando falamos em comunica\u00e7\u00e3o em rede, geralmente nos referimos \u00e0 comunica\u00e7\u00e3o entre dois ou mais dispositivos interconectados. A ideia \u00e9 que possamos nos comunicar com outros dispositivos usando a rede.

"},{"location":"02/#o-modelo-cliente-servidor","title":"O modelo cliente-servidor","text":"

No contexto de aplica\u00e7\u00f5es web, geralmente nos referimos a um modelo espec\u00edfico de comunica\u00e7\u00e3o: o cliente-servidor. Neste modelo, temos clientes, como aplicativos m\u00f3veis, terminais de comando, navegadores, etc., acessando recursos fornecidos por outro computador, conhecido como servidor.

Neste modelo, fazemos chamadas de um cliente, via rede, seguindo alguns padr\u00f5es, e recebemos respostas da nossa aplica\u00e7\u00e3o, o servidor. Por exemplo, podemos enviar um comando ao servidor: \"Crie um usu\u00e1rio para mim\". Em resposta, ele nos fornece um retorno, seja uma confirma\u00e7\u00e3o de sucesso ou uma mensagem de erro.

sequenceDiagram\n    participant Cliente\n    participant Servidor\n    Note left of Cliente: Fazendo a requisi\u00e7\u00e3o\n    Cliente->>Servidor: Crie um usu\u00e1rio\n    activate Servidor\n    Note right of Servidor: Processa a requisi\u00e7\u00e3o\n    Servidor-->>Cliente: Sucesso na requisi\u00e7\u00e3o: Usu\u00e1rio criado com sucesso\n    deactivate Servidor\n    Note left of Cliente: Obtivemos a resposta desejada\n\n    Cliente->>Servidor: Crie o mesmo usu\u00e1rio\n    activate Servidor\n    Note left of Cliente: Fazendo uma nova requisi\u00e7\u00e3o\n    Note right of Servidor: Processa a requisi\u00e7\u00e3o\n    Servidor-->>Cliente: Erro na requisi\u00e7\u00e3o: Usu\u00e1rio j\u00e1 existe\n    deactivate Servidor\n    Note left of Cliente: Obtivemos a resposta de erro

A comunica\u00e7\u00e3o \u00e9 bidirecional: um cliente faz uma requisi\u00e7\u00e3o ao servidor, que por sua vez emite uma resposta.

Por exemplo, ao construir um servidor, precisamos de uma biblioteca que consiga \"servir\" nossa aplica\u00e7\u00e3o. \u00c9 a\u00ed que entra o Uvicorn, respons\u00e1vel por servir nossa aplica\u00e7\u00e3o com FastAPI.

Quando executamos:

$ Execu\u00e7\u00e3o no terminal!
fastapi dev fast_zero/app.py\n

Quando executamos esse comando. O FastAPI faz uma chamada ao uvicorn e iniciamos um servidor em loopback, acess\u00edvel apenas internamente no nosso computador. Por isso, ao acessarmos http://127.0.0.1:8000/ no navegador, estamos fazendo uma requisi\u00e7\u00e3o ao servidor em 127.0.0.1:8000.

sequenceDiagram\n    participant Cliente\n    participant Servidor\n    Note left of Cliente: Fazendo a requisi\u00e7\u00e3o\n    Cliente->>Servidor: \n    activate Servidor\n    Note right of Servidor: Processa a requisi\u00e7\u00e3o\n    Servidor-->>Cliente: Sucesso na requisi\u00e7\u00e3o: {\"message\":\"Ol\u00e1 Mundo!\"}\n    deactivate Servidor\n    Note left of Cliente: Obtivemos a resposta desejada
"},{"location":"02/#usando-o-fastapi-na-rede-local","title":"Usando o fastapi na rede local","text":"

Falando em redes, o Uvicorn no seu PC tamb\u00e9m pode servir o FastAPI na sua rede local:

$ Execu\u00e7\u00e3o no terminal!
fastapi dev fast_zero/app.py --host 0.0.0.0\n
Esse comando tamb\u00e9m poderia ser executado com taskipy

Uma caracter\u00edstica interessante do taskipy \u00e9 que qualquer continua\u00e7\u00e3o ap\u00f3s o comando da task \u00e9 passado para o comando original. Poder\u00edamos ent\u00e3o executar dessa forma tamb\u00e9m:

$ Execu\u00e7\u00e3o no terminal!
task run --host 0.0.0.0\n

Assim, voc\u00ea pode acessar a aplica\u00e7\u00e3o de outro computador na sua rede usando o endere\u00e7o IP da sua m\u00e1quina.

Descobrindo o seu endere\u00e7o local usando python

Caso n\u00e3o esteja familiarizado com o terminal ou ferramentas para descobrir seu endere\u00e7o IP:

>>> Terminal interativo!
>>> import socket\n>>> s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)\n>>> s.connect((\"8.8.8.8\", 80))\n>>> s.getsockname()[0]\n'192.168.0.100'# (1)!\n
  1. O endere\u00e7o do meu computador na rede local

"},{"location":"02/#o-modelo-padrao-da-web","title":"O modelo padr\u00e3o da web","text":"

Ignorando muita hist\u00f3ria e diversas camadas de padr\u00f5es, podemos nos concentrar nos tr\u00eas padr\u00f5es principais que ser\u00e3o mais importantes para n\u00f3s agora:

graph\n    A[Web] --> B[URL]\n    A --> C[HTTP]\n    A --> D[HTML]
"},{"location":"02/#url","title":"URL","text":"

Uma URL (Uniform Resource Locator) \u00e9 como um endere\u00e7o que nos ajuda a encontrar um recurso espec\u00edfico em uma rede, como a URL http://127.0.0.1:8000 que usamos para acessar nossa aplica\u00e7\u00e3o.

Uma URL \u00e9 composta por v\u00e1rias partes, como neste exemplo: protocolo://endere\u00e7o:porta/caminho/recurso?query_string#fragmento. Neste primeiro momento, focaremos nos primeiros quatro componentes, essenciais para o andamento da aula:

  1. Protocolo: A primeira parte da URL, terminando com \"://\". Os mais comuns s\u00e3o \"http://\" e \"https://\". Este protocolo define como os dados s\u00e3o trocados entre seu computador e o local onde o recurso est\u00e1 armazenado, seja na internet ou numa rede local.

  2. Endere\u00e7o do Host: Pode ser um endere\u00e7o IP (como \"192.168.1.10\") ou um endere\u00e7o de DNS (como \"youtube.com\"). Ele identifica o dispositivo na rede que cont\u00e9m o recurso desejado.

  3. Porta (opcional): Ap\u00f3s o endere\u00e7o do host, pode haver um n\u00famero ap\u00f3s dois pontos, como em \"192.168.1.10:8080\". Este n\u00famero \u00e9 a porta, usada para direcionar sua solicita\u00e7\u00e3o ao servi\u00e7o espec\u00edfico no dispositivo. Por padr\u00e3o, as portas s\u00e3o 80 para HTTP e 443 para HTTPS, quando n\u00e3o especificadas.

  4. Caminho: Indica a localiza\u00e7\u00e3o exata do recurso no servidor ou dispositivo. Por exemplo, em \"192.168.1.10:8000/busca\", /busca \u00e9 o nome do recurso. Quando n\u00e3o especificado, o servidor responde com o recurso na raiz (/).

Ao acessarmos via navegador a URL http://127.0.0.1:8000, estamos acessando o servidor via protocolo HTTP, no endere\u00e7o do nosso pr\u00f3prio computador, na porta 8000, solicitando o recurso /.

"},{"location":"02/#http","title":"HTTP","text":"

Quando o cliente inicia uma requisi\u00e7\u00e3o para um endere\u00e7o na rede, isso \u00e9 feito via um protocolo e direcionado ao servidor do recurso. Em aplica\u00e7\u00f5es web, a maioria da comunica\u00e7\u00e3o ocorre via protocolo HTTP ou sua vers\u00e3o segura, o HTTPS.

HTTP, ou Hypertext Transfer Protocol (Protocolo de Transfer\u00eancia de Hipertexto), \u00e9 o protocolo fundamental na web para a transfer\u00eancia de dados e comunica\u00e7\u00e3o entre clientes e servidores. Ele baseia-se no modelo de requisi\u00e7\u00e3o-resposta: onde o cliente faz uma requisi\u00e7\u00e3o ao servidor, que responde a essa requisi\u00e7\u00e3o. Essas requisi\u00e7\u00f5es e respostas s\u00e3o formatadas conforme as regras do protocolo HTTP.

"},{"location":"02/#mensagens","title":"Mensagens","text":"

No contexto do HTTP, tanto requisi\u00e7\u00f5es quanto respostas s\u00e3o referidas como mensagens. As mensagens HTTP na vers\u00e3o 1 t\u00eam uma estrutura textual semelhante ao seguinte exemplo.

Um exemplo de mensagem HTTP enviada pelo cliente:

Exemplo da mensagem emitada pelo cliente
GET / HTTP/1.1\nAccept: */*\nAccept-Encoding: gzip, deflate\nConnection: keep-alive\nHost: 127.0.0.1:8000\nUser-Agent: HTTPie/3.2.2\n

Na primeira linha, temos o verbo GET, que solicita um recurso, neste caso, o recurso \u00e9 /. As linhas seguintes comp\u00f5em o cabe\u00e7alho da mensagem. Elas informam que o cliente aceita qualquer tipo de resposta (Accept: */*), indicam a URL destino (Host: 127.0.0.1:8000) e identificam o cliente que gerou a requisi\u00e7\u00e3o (User-Agent: HTTPie/3.2.2), que neste caso foi o cliente HTTPie.

Em resposta a esta mensagem, o servidor enviou o seguinte:

Exemplo da mensagem de resposta do servidor
HTTP/1.1 200 OK\ncontent-length: 24\ncontent-type: application/json\ndate: Fri, 19 Jan 2024 04:05:50 GMT\nserver: uvicorn\n\n{\n    \"message\": \"Ol\u00e1 mundo\"\n}\n

Aqui, na primeira linha da resposta, temos a vers\u00e3o do protocolo HTTP utilizada e o c\u00f3digo de resposta 200 OK, indicando que a requisi\u00e7\u00e3o foi bem-sucedida. O cabe\u00e7alho da resposta inclui informa\u00e7\u00f5es como o content-length e content-type, que especificam o tamanho e o tipo do conte\u00fado da resposta, respectivamente. A data e o servidor que processou a requisi\u00e7\u00e3o tamb\u00e9m s\u00e3o indicados. Finalmente, o corpo da resposta, formatado em JSON, cont\u00e9m a mensagem \"Ol\u00e1 mundo\".

Como as mensagens do HTTP foram geradas?

A visualiza\u00e7\u00e3o das mensagens foram geradas com o cliente CLI do HTTPie: http GET http://127.0.0.1:8000 -v

GET / HTTP/1.1\nAccept: */*\nAccept-Encoding: gzip, deflate\nConnection: keep-alive\nHost: 127.0.0.1:8000\nUser-Agent: HTTPie/3.2.2\n\nHTTP/1.1 200 OK\ncontent-length: 24\ncontent-type: application/json\ndate: Fri, 19 Jan 2024 04:05:50 GMT\nserver: uvicorn\n\n{\n    \"message\": \"Ol\u00e1 mundo\"\n}\n
"},{"location":"02/#cabecalho","title":"Cabe\u00e7alho","text":"

O cabe\u00e7alho de uma mensagem HTTP cont\u00e9m metadados essenciais sobre a requisi\u00e7\u00e3o ou resposta. Alguns elementos comuns que podem ser inclu\u00eddos no cabe\u00e7alho s\u00e3o:

"},{"location":"02/#corpo","title":"Corpo","text":"

O corpo da mensagem cont\u00e9m os dados propriamente ditos, variando conforme o tipo de m\u00eddia. Exemplos podem incluir um objeto JSON ou uma estrutura HTML.

"},{"location":"02/#verbos","title":"Verbos","text":"

Quando um cliente faz uma requisi\u00e7\u00e3o HTTP, ele indica sua inten\u00e7\u00e3o ao servidor utilizando verbos. Estes verbos sinalizam diferentes a\u00e7\u00f5es no protocolo HTTP. Vejamos alguns exemplos:

Na nossa aplica\u00e7\u00e3o FastAPI, definimos que a fun\u00e7\u00e3o read_root que ser\u00e1 executada quando uma requisi\u00e7\u00e3o GET for feita por um cliente no caminho /:

fast_zero/app.py
@app.get('/')\ndef read_root():\n    return {'message': 'Ol\u00e1 Mundo!'}\n

Quando realizamos a requisi\u00e7\u00e3o via navegador, o verbo padr\u00e3o \u00e9 o GET. Por isso, obtemos na tela a mensagem {'message': 'Ol\u00e1 Mundo!'}.

Essa \u00e9 exatamente a resposta fornecida pela execu\u00e7\u00e3o da fun\u00e7\u00e3o read_root. No futuro, criaremos fun\u00e7\u00f5es para lidar com os outros verbos HTTP.

"},{"location":"02/#codigos-de-resposta","title":"C\u00f3digos de resposta","text":"

No mundo das requisi\u00e7\u00f5es usando o protocolo HTTP, al\u00e9m da resposta obtida quando nos comunicamos com o servidor, tamb\u00e9m recebemos um c\u00f3digo de resposta (status code). Os c\u00f3digos s\u00e3o formas de mostrar ao cliente como o servidor lidou com a sua requisi\u00e7\u00e3o. Os c\u00f3digos s\u00e3o divididos em classes e as classes s\u00e3o distribu\u00eddas por centenas:

Para mais informa\u00e7\u00f5es a cerca do status code acesse a documenta\u00e7\u00e3o do iana

Sempre que fazemos uma requisi\u00e7\u00e3o, obtemos um c\u00f3digo de resposta. Por exemplo, em nosso arquivo de teste, quando efetuamos a requisi\u00e7\u00e3o, fazemos a checagem para ver se recebemos um c\u00f3digo de sucesso, o c\u00f3digo 200 OK:

tests/test_app.py
def test_root_deve_retornar_ok_e_ola_mundo():\n    client = TestClient(app)\n\n    response = client.get('/')\n\n    assert response.status_code == 200\n
"},{"location":"02/#boas-praticas-para-constantes","title":"Boas pr\u00e1ticas para constantes","text":"

Quando comparamos valores, a boa pr\u00e1tica \u00e9 que eles nunca sejam expl\u00edcitos no c\u00f3digo como status_code == 200. Neste caso em espec\u00edfico \u00e9 f\u00e1cil saber o que 200 significa pois estamos estudando exatamente essa parte. Mas, em alguns momentos podemos nos deparar com c\u00f3digos que n\u00e3o sabemos o significado. Por exemplo um c\u00f3digo 209. O que ele significa?1

Quando trabalhamos com \"valores m\u00e1gicos\" no c\u00f3digo, a PEP-8 recomenda que criemos constantes que representem esses valores. Como por exemplo OK = 200.

A boa pr\u00e1tica para lidar com c\u00f3digos de status \u00e9 usar a classe http.HTTPStatus, que j\u00e1 mapeia todos os status code em um \u00fanico objeto. Fazendo que a compara\u00e7\u00e3o seja feita de forma simples, como:

tests/test_app.py
def test_root_deve_retornar_ok_e_ola_mundo():\n    client = TestClient(app)\n\n    response = client.get('/')\n\n    assert response.status_code == HTTPStatus.OK\n
"},{"location":"02/#o-lado-do-servidor","title":"O lado do servidor","text":"

Para garantir que a resposta obtida pelo cliente seja considerada um sucesso, o FastAPI, por padr\u00e3o, envia o c\u00f3digo de sucesso 200 para o m\u00e9todo GET. No entanto, tamb\u00e9m podemos deixar isso expl\u00edcito na defini\u00e7\u00e3o da rota:

Apenas o d\u00edgitoUsando HTTPStatus fast_zero/app.py
@app.get(\"/\", status_code=200)\ndef read_root():\n    return {'message': 'Ol\u00e1 Mundo!'}\n
fast_zero/app.py
@app.get(\"/\", status_code=HTTPStatus.OK)\ndef read_root():\n    return {'message': 'Ol\u00e1 Mundo!'}\n

Temos diversos c\u00f3digos a explorar durante nossa jornada, mas gostaria de listar os mais comuns dentro do nosso escopo:

Assim, podemos ir ao terceiro pilar do desenvolvimento web que s\u00e3o os conte\u00fados relacionados as respostas.

"},{"location":"02/#html","title":"HTML","text":"

Sobre o c\u00f3digo apresentado nesse t\u00f3pico!

Todo o c\u00f3digo apresentado neste t\u00f3pico \u00e9 apenas um exemplo b\u00e1sico do uso de HTML com FastAPI e n\u00e3o ser\u00e1 utilizado no curso. No entanto, \u00e9 extremamente importante mencionar este t\u00f3pico.

Embora este t\u00f3pico abranja apenas HTML puro, o FastAPI pode utilizar Jinja como sistema de templates para uma aplica\u00e7\u00e3o mais eficiente.

Interessado em aprender sobre a aplica\u00e7\u00e3o de templates?

Na live sobre websockets com FastAPI, discutimos bastante sobre templates. Voc\u00ea pode assistir ao v\u00eddeo aqui:

A documenta\u00e7\u00e3o do FastAPI tamb\u00e9m oferece um t\u00f3pico focado em Templates.

O terceiro pilar fundamental da web \u00e9 o HTML, sigla para Hypertext Markup Language. Trata-se da linguagem de marca\u00e7\u00e3o padr\u00e3o usada para criar e estruturar p\u00e1ginas na internet. Quando acessamos um site, o que vemos em nossos navegadores \u00e9 o resultado da interpreta\u00e7\u00e3o do HTML. Esta linguagem utiliza uma s\u00e9rie de 'tags' \u2013 como <html>, <head>, <body>, <h1>, <p> e outras \u2013 para definir a estrutura e o conte\u00fado de uma p\u00e1gina web.

A beleza do HTML reside em sua simplicidade e efic\u00e1cia. Mais do que uma linguagem, \u00e9 uma forma de organizar e apresentar informa\u00e7\u00f5es na web. Cada tag tem um prop\u00f3sito espec\u00edfico: <h1> a <h6> s\u00e3o usadas para t\u00edtulos e subt\u00edtulos; <p> para par\u00e1grafos; <a> para links; enquanto <div> e <span> auxiliam na organiza\u00e7\u00e3o e estilo do conte\u00fado. Juntas, essas tags formam a espinha dorsal de quase todas as p\u00e1ginas da internet.

Se nosso objetivo fosse apresentar um HTML simples, poder\u00edamos usar a classe de resposta HTMLResponse:

from fastapi import FastAPI\nfrom fastapi.responses import HTMLResponse\n\napp = FastAPI()\n\n\n@app.get('/', response_class=HTMLResponse)\ndef read_root():\n    return \"\"\"\n    <html>\n      <head>\n        <title> Nosso ol\u00e1 mundo!</title>\n      </head>\n      <body>\n        <h1> Ol\u00e1 Mundo </h1>\n      </body>\n    </html>\"\"\"\n

Ao acessarmos nossa URL no navegador, podemos ver o HTML sendo renderizado:

Embora o HTML seja crucial para a estrutura\u00e7\u00e3o de p\u00e1ginas web, nosso curso foca em uma perspectiva diferente: a transfer\u00eancia de dados. Enquanto o HTML \u00e9 usado para apresentar dados visualmente nos navegadores, existe outra camada focada na transfer\u00eancia de informa\u00e7\u00f5es entre sistemas e servidores. Aqui entra o conceito de APIs (Application Programming Interfaces), que frequentemente utilizam JSON (JavaScript Object Notation) para a troca de dados. JSON \u00e9 um formato leve de troca de dados, f\u00e1cil de ler e escrever para humanos, e simples de interpretar e gerar para m\u00e1quinas.

Portanto, embora n\u00e3o aprofundemos no HTML como linguagem, \u00e9 importante entender seu papel como a camada de apresenta\u00e7\u00e3o padr\u00e3o da web. Agora, direcionaremos nossa aten\u00e7\u00e3o para as APIs e a troca de dados em JSON, explorando como essas tecnologias permitem a comunica\u00e7\u00e3o eficiente entre diferentes sistemas e aplicativos.

"},{"location":"02/#apis","title":"APIs","text":"

Quando falamos sobre aplica\u00e7\u00f5es web que n\u00e3o envolvem uma camada de visualiza\u00e7\u00e3o, como HTML, geralmente estamos nos referindo a APIs. A sigla API vem de Application Programming Interface (Interface de Programa\u00e7\u00e3o de Aplica\u00e7\u00f5es). Uma API \u00e9 projetada para ser uma interface claramente definida e documentada, que facilita a intera\u00e7\u00e3o por meio do protocolo HTTP.

A ess\u00eancia das APIs reside no modelo cliente-servidor, onde o cliente troca dados com o servidor atrav\u00e9s de endpoints, respeitando as regras estabelecidas pelo protocolo HTTP. Por exemplo, para solicitar dados ao servidor, usamos o verbo GET, direcionando a requisi\u00e7\u00e3o a um endpoint espec\u00edfico do servidor, que em resposta nos fornece o dado ou recurso solicitado.

"},{"location":"02/#endpoint","title":"Endpoint","text":"

O termo \"endpoint\" refere-se a um ponto espec\u00edfico em uma API para onde as requisi\u00e7\u00f5es s\u00e3o enviadas. Basicamente, \u00e9 um endere\u00e7o na web (URL) onde o servidor ou a API est\u00e1 ativo e pronto para responder a requisi\u00e7\u00f5es dos clientes. Cada endpoint est\u00e1 associado a uma fun\u00e7\u00e3o espec\u00edfica da API, como recuperar dados, criar novos registros, atualizar ou deletar dados existentes.

A localiza\u00e7\u00e3o e estrutura de um endpoint, que incluem o caminho na URL e os m\u00e9todos HTTP permitidos, definem como os clientes devem formatar suas requisi\u00e7\u00f5es para serem compreendidas e processadas pelo servidor. Por exemplo, um endpoint para recuperar informa\u00e7\u00f5es de um usu\u00e1rio pode ter um endere\u00e7o como https://api.exemplo.com/usuarios/{id}, onde {id} \u00e9 o identificador \u00fanico do usu\u00e1rio desejado.

Atualmente, em nossa aplica\u00e7\u00e3o, temos apenas um endpoint dispon\u00edvel: o /. Vejamos o exemplo:

aula_00.py
@app.get('/')\ndef read_root():\n    return {'message': 'Ol\u00e1 Mundo!'}\n

Quando utilizamos o decorador @app.get('/'), estamos instruindo nossa API que, para chamadas de m\u00e9todo GET no endpoint /, a fun\u00e7\u00e3o read_root ser\u00e1 executada. O resultado dessa fun\u00e7\u00e3o, neste caso {'message': 'Ol\u00e1 Mundo!'}, \u00e9 o que ser\u00e1 retornado ao cliente.

"},{"location":"02/#documentacao","title":"Documenta\u00e7\u00e3o","text":"

Uma pergunta comum nesse est\u00e1gio \u00e9: \"Ok, mas como descobrir ou conhecer os endpoints dispon\u00edveis em uma API?\". A resposta reside na documenta\u00e7\u00e3o. Uma documenta\u00e7\u00e3o eficaz \u00e9 essencial em APIs, especialmente quando muitos clientes diferentes precisam se comunicar com o servidor. A melhor pr\u00e1tica \u00e9 fornecer uma documenta\u00e7\u00e3o detalhada, clara e acess\u00edvel sobre os endpoints dispon\u00edveis, incluindo informa\u00e7\u00f5es sobre o formato e a estrutura dos dados que podem ser enviados e recebidos.

A documenta\u00e7\u00e3o de uma API serve como um guia ou um manual, facilitando o entendimento e a utiliza\u00e7\u00e3o por desenvolvedores e usu\u00e1rios finais. Ela desempenha um papel crucial ao:

"},{"location":"02/#openapi-e-documentacao-automatica","title":"OpenAPI e documenta\u00e7\u00e3o autom\u00e1tica","text":"

Uma das solu\u00e7\u00f5es mais eficazes para a documenta\u00e7\u00e3o de APIs \u00e9 a utiliza\u00e7\u00e3o da especifica\u00e7\u00e3o OpenAPI, dispon\u00edvel em OpenAPI Specification. Essa especifica\u00e7\u00e3o fornece um padr\u00e3o robusto para descrever APIs, permitindo aos desenvolvedores criar documenta\u00e7\u00f5es precisas e test\u00e1veis de forma autom\u00e1tica. Esta abordagem n\u00e3o apenas simplifica o processo de documenta\u00e7\u00e3o, mas tamb\u00e9m garante que a documenta\u00e7\u00e3o seja consistentemente atualizada e precisa.

Para visualizar e interagir com essa documenta\u00e7\u00e3o, ferramentas como Swagger UI e Redoc s\u00e3o amplamente utilizadas. Elas transformam a especifica\u00e7\u00e3o OpenAPI em visualiza\u00e7\u00f5es interativas, fornecendo uma interface f\u00e1cil de navegar onde os usu\u00e1rios podem n\u00e3o apenas ler a documenta\u00e7\u00e3o, mas tamb\u00e9m experimentar a API diretamente na interface. Esta funcionalidade interativa \u00e9 fundamental para uma compreens\u00e3o pr\u00e1tica de como a API funciona, al\u00e9m de oferecer uma maneira eficiente de testar suas funcionalidades em tempo real.

No contexto do FastAPI, h\u00e1 suporte autom\u00e1tico tanto para Swagger UI quanto para Redoc. Para explorar a documenta\u00e7\u00e3o atual da nossa aplica\u00e7\u00e3o, basta iniciar o servidor com o seguinte comando:

$ Execu\u00e7\u00e3o no terminal!
task run\n
"},{"location":"02/#swagger-ui","title":"Swagger UI","text":"

Ao acessarmos o endere\u00e7o http://127.0.0.1:8000/docs, nos deparamos com a interface do Swagger UI:

Esta imagem nos d\u00e1 uma vis\u00e3o geral dos endpoints dispon\u00edveis na nossa aplica\u00e7\u00e3o, neste caso, o endpoint / que aceita o verbo HTTP GET. Ao explorar mais a fundo e clicar nesse m\u00e9todo:

Na documenta\u00e7\u00e3o, \u00e9 poss\u00edvel observar diversas informa\u00e7\u00f5es cruciais, como o c\u00f3digo de resposta 200, que indica sucesso, o tipo de dado retornado pelo cabe\u00e7alho (application/json) e um exemplo do valor de retorno. Contudo, a documenta\u00e7\u00e3o atual sugere, incorretamente, que o retorno \u00e9 uma string, quando, na verdade, nossa aplica\u00e7\u00e3o retorna um objeto JSON. Essa diferen\u00e7a ser\u00e1 abordada em breve.

Um aspecto interessante do Swagger UI \u00e9 a possibilidade de interagir diretamente com a API atrav\u00e9s da interface. Ao clicar em Try it out, um bot\u00e3o Execute se torna dispon\u00edvel:

Clicar em Execute faz do Swagger um cliente tempor\u00e1rio da nossa API, enviando uma requisi\u00e7\u00e3o ao servidor e exibindo a resposta:

A resposta ilustra como fazer a chamada usando o Curl, a URL utilizada, o c\u00f3digo de resposta 200, e detalhes da resposta do servidor, incluindo o corpo da mensagem (body) e os cabe\u00e7alhos (headers).

Caso queira saber mais sobre OpenAPI e Swagger

Temos uma live focada em OpenAPI, que s\u00e3o as especifica\u00e7\u00f5es do Swagger:

"},{"location":"02/#redoc","title":"Redoc","text":"

Assim como o Swagger UI, o Redoc \u00e9 outra ferramenta popular para visualizar a documenta\u00e7\u00e3o de APIs OpenAPI, mas com um foco em uma apresenta\u00e7\u00e3o mais limpa e leg\u00edvel. Para acessar a documenta\u00e7\u00e3o Redoc da nossa aplica\u00e7\u00e3o, podemos visitar o endere\u00e7o http://127.0.0.1:8000/redoc. O Redoc organiza a documenta\u00e7\u00e3o de uma maneira mais linear e de f\u00e1cil leitura, destacando as descri\u00e7\u00f5es dos endpoints, os m\u00e9todos HTTP dispon\u00edveis, os schemas dos dados de entrada e sa\u00edda, al\u00e9m de exemplos de requisi\u00e7\u00f5es e respostas.

"},{"location":"02/#trafegando-json","title":"Trafegando JSON","text":"

Quando discutimos APIs \"modernas\"2, nos referimos a APIs que priorizam o tr\u00e1fego de dados, deixando de lado a camada de apresenta\u00e7\u00e3o, como o HTML. O objetivo \u00e9 transmitir dados de forma agn\u00f3stica para diferentes tipos de clientes. Nesse contexto, o JSON (JavaScript Object Notation) se tornou a m\u00eddia padr\u00e3o, gra\u00e7as \u00e0 sua leveza e facilidade de leitura tanto por humanos quanto por m\u00e1quinas.

O JSON \u00e9 apreciado por sua simplicidade, apresentando dados em estruturas de documento chave-valor, onde os valores podem ser strings, n\u00fameros, booleanos, arrays, entre outros.

Abaixo, um exemplo ilustra o formato JSON:

Exemplo de um JSON
{\n    \"livros\": [\n        {\n            \"titulo\": \"O apanhador no campo de centeio\",\n            \"autor\": \"J.D. Salinger\",\n            \"ano\": 1945,\n            \"disponivel\": false\n        },\n        {\n            \"titulo\": \"O mestre e a margarida\",\n            \"autor\": \"Mikhail Bulg\u00e1kov\",\n            \"ano\": 1966,\n            \"disponivel\": true\n        }\n    ]\n}\n

Este exemplo demonstra como o JSON organiza dados de forma intuitiva e acess\u00edvel, tornando-o ideal para a comunica\u00e7\u00e3o de dados em uma ampla variedade de aplica\u00e7\u00f5es.

"},{"location":"02/#contratos-em-apis-json","title":"Contratos em APIs JSON","text":"

Quando falamos sobre o compartilhamento de JSON entre cliente e servidor, \u00e9 crucial estabelecer um entendimento m\u00fatuo sobre a estrutura dos dados que ser\u00e3o trocados. A este entendimento, denominamos schema, que atua como um contrato definindo a forma e o conte\u00fado dos dados trafegados.

O schema de uma API desempenha um papel fundamental ao assegurar que ambos, cliente e servidor, estejam alinhados quanto \u00e0 estrutura dos dados. Este \"contrato\" especifica:

Por exemplo, para nossa mensagem simples retornada por read_root ({'message': 'Ol\u00e1 mundo!'}), ter\u00edamos um schema assim:

{\n  \"type\": \"object\",\n  \"properties\": {\n    \"message\": {\n      \"type\": \"string\"\n    }\n  },\n  \"required\": [\"message\"]\n}\n

Onde estamos dizendo ao cliente que ao chamar a API, ser\u00e1 retornado um objeto, esse objeto cont\u00e9m a propriedade \"message\", a mensagem ser\u00e1 do tipo string. Ao final, vemos que o campo message \u00e9 requerido. Isso quer dizer que ele sempre ser\u00e1 enviado na resposta.

E em casos mais complicados, como ficaria o schema?

Para o exemplo que fizemos antes, sobre os livros, o schema seria assim:

{\n  \"type\": \"object\",\n  \"properties\": {\n    \"livros\": {\n      \"type\": \"array\",\n      \"items\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"titulo\": {\n            \"type\": \"string\"\n          },\n          \"autor\": {\n            \"type\": \"string\"\n          },\n          \"ano\": {\n            \"type\": \"integer\"\n          },\n          \"disponivel\": {\n            \"type\": \"boolean\"\n          }\n        },\n        \"required\": [\"titulo\", \"autor\", \"ano\", \"disponivel\"]\n      }\n    }\n  },\n  \"required\": [\"livros\"]\n}\n
"},{"location":"02/#pydantic","title":"Pydantic","text":"

No universo de APIs e contratos de dados, especialmente ao trabalhar com Python, o Pydantic se destaca como uma ferramenta poderosa e vers\u00e1til. Essa biblioteca, altamente integrada ao ecossistema Python, especializa-se na cria\u00e7\u00e3o de schemas de dados e na valida\u00e7\u00e3o de tipos. Com o Pydantic, \u00e9 poss\u00edvel expressar schemas JSON de maneira elegante e eficiente atrav\u00e9s de classes Python, proporcionando uma ponte robusta entre a flexibilidade do JSON e a seguran\u00e7a de tipos de Python.

Por exemplo, o schema JSON {'message': 'Ol\u00e1 mundo!'}. Com o Pydantic, podemos representar esse schema na forma de uma classe Python chamada Message. Isso \u00e9 feito de maneira intuitiva e direta:

fast_zero/schemas.py
from pydantic import BaseModel\n\n\nclass Message(BaseModel):\n    message: str\n

Para iniciar o desenvolvimento com schemas no contexto do FastAPI, podemos criar um arquivo chamado fast_zero/schemas.py e definir a classe Message. Vale ressaltar que o Pydantic \u00e9 uma depend\u00eancia integrada do FastAPI (n\u00e3o precisa ser instalado), refletindo a import\u00e2ncia dessa biblioteca no processo de valida\u00e7\u00e3o de dados e na gera\u00e7\u00e3o de documenta\u00e7\u00e3o autom\u00e1tica para APIs, como a documenta\u00e7\u00e3o OpenAPI.

"},{"location":"02/#integrando-o-pydantic-com-o-fastapi","title":"Integrando o Pydantic com o FastAPI","text":"

A integra\u00e7\u00e3o do modelo Pydantic (ou schema JSON) com o FastAPI \u00e9 feita ao especificar o modelo no campo response_model do decorador do endpoint. Isso garante que a resposta da API esteja alinhada com o schema definido, al\u00e9m de auxiliar na valida\u00e7\u00e3o dos dados retornados:

fast_zero/app.py
from http import HTTPStatus\n\nfrom fastapi import FastAPI\n\nfrom fast_zero.schemas import Message\n\napp = FastAPI()\n\n\n@app.get('/', status_code=HTTPStatus.OK, response_model=Message)\ndef read_root():\n    return {'message': 'Ol\u00e1 Mundo!'}\n

Com essa abordagem, ao iniciar o servidor (task run) e acessar a Swagger UI em http://127.0.0.1:8000/docs, observamos uma evolu\u00e7\u00e3o significativa na documenta\u00e7\u00e3o. Um novo campo Schemas \u00e9 exibido, destacando a estrutura do modelo Message que definimos:

Al\u00e9m disso, na se\u00e7\u00e3o de Responses, temos um exemplo claro da sa\u00edda esperada do endpoint: {\"message\": \"string\"}. Isso ilustra como a API ir\u00e1 responder, especificando que o campo obrigat\u00f3rio \"message\" ser\u00e1 retornado com um valor do tipo \"string\".

"},{"location":"02/#exercicio","title":"Exerc\u00edcio","text":"
  1. Crie um endpoint que retorne \"ol\u00e1 mundo\" usando HTML e escreva seu teste. Dica: para capturar a resposta do HTML do cliente de testes, voc\u00ea pode usar response.text

Exerc\u00edcios resolvidos

"},{"location":"02/#conclusao","title":"Conclus\u00e3o","text":"

Nesta aula, navegamos brevemente pelo vasto mundo do desenvolvimento web com foco em APIs, abra\u00e7ando desde os fundamentos da comunica\u00e7\u00e3o na web at\u00e9 as pr\u00e1ticas de troca de dados. Exploramos o modelo cliente-servidor, entendemos algumas das nuances das mensagens HTTP e tivemos uma introdu\u00e7\u00e3o sobre URLs e HTML. Embora o HTML desempenhe um papel central na camada de apresenta\u00e7\u00e3o, o nosso foco recaiu sobre as APIs, particularmente aquelas que trafegam JSON, um formato de dados.

Aprofundamos no uso de ferramentas e conceitos vitais para a cria\u00e7\u00e3o de APIs, como o FastAPI e o Pydantic, que juntos oferecem uma poderosa combina\u00e7\u00e3o para a valida\u00e7\u00e3o de dados e a gera\u00e7\u00e3o autom\u00e1tica de documenta\u00e7\u00e3o. A explora\u00e7\u00e3o do Swagger UI e do Redoc enriqueceu nosso entendimento sobre a import\u00e2ncia da documenta\u00e7\u00e3o acess\u00edvel e clara para APIs, facilitando tanto o desenvolvimento quanto a usabilidade.

Essa aula nos deu uma fundamenta\u00e7\u00e3o b\u00e1sica para avan\u00e7armos na constru\u00e7\u00e3o de APIs. Embora tenhamos exemplificado os conceitos com FastAPI, esses conceitos te\u00f3ricos podem ajudar voc\u00ea a desenvolver ferramentas web em qualquer tecnologia ou linguagem.

Nos vemos nas pr\u00f3ximas aulas para aplicar todos esses conceitos de forma mais aprofundada!

Agora que a aula acabou, \u00e9 um bom momento para voc\u00ea relembrar alguns conceitos e fixar melhor o conte\u00fado respondendo ao question\u00e1rio referente a ela.

Quiz

  1. O c\u00f3digo 209 n\u00e3o \u00e9 um status code v\u00e1lido no http, por isso o usei como exemplo.\u00a0\u21a9

  2. Apesar da no\u00e7\u00e3o comum de que APIs modernas s\u00e3o projetadas para trafegar JSON, existem debates intensos sobre as melhores pr\u00e1ticas para a transfer\u00eancia de dados em APIs. Uma leitura recomendada \u00e9 o livro hypermedia systems, que \u00e9 gratuito e oferece percep\u00e7\u00f5es valiosas.\u00a0\u21a9

"},{"location":"03/","title":"Estruturando o Projeto e Criando Rotas CRUD","text":""},{"location":"03/#estruturando-o-projeto-e-criando-rotas-crud","title":"Estruturando o Projeto e Criando Rotas CRUD","text":"

Objetivos dessa aula:

Caso prefira ver a aula em v\u00eddeo

Esse aula ainda n\u00e3o est\u00e1 dispon\u00edvel em formato de v\u00eddeo, somente em texto ou live!

Aula Slides C\u00f3digo Quiz Exerc\u00edcios

Boas-vindas de volta \u00e0 nossa s\u00e9rie de aulas sobre a constru\u00e7\u00e3o de uma aplica\u00e7\u00e3o utilizando FastAPI. Na \u00faltima aula, abordamos conceitos b\u00e1sicos do desenvolvimento web e finalizamos a configura\u00e7\u00e3o do nosso ambiente. Hoje, avan\u00e7aremos na estrutura\u00e7\u00e3o dos primeiros endpoints da nossa API, concentrando-nos nas quatro opera\u00e7\u00f5es fundamentais de CRUD - Criar, Ler, Atualizar e Deletar. Exploraremos como estas opera\u00e7\u00f5es se aplicam tanto \u00e0 comunica\u00e7\u00e3o web quanto \u00e0 intera\u00e7\u00e3o com o banco de dados.

O objetivo desta aula \u00e9 implementar um sistema de cadastro de usu\u00e1rios na nossa aplica\u00e7\u00e3o. Ao final, voc\u00ea conseguir\u00e1 cadastrar, listar, alterar e deletar usu\u00e1rios, al\u00e9m de realizar testes para validar estas funcionalidades.

Nota para pessoas mais experiente sobre essa aula

O princ\u00edpio por tr\u00e1s dessa aula \u00e9 demonstrar como construir os endpoints e os testes mais b\u00e1sicos poss\u00edveis.

Talvez lhe cause estranhamento o uso de um banco de dados em uma lista e os testes sendo constru\u00eddos a partir de efeitos colaterais. Mas o objetivo principal \u00e9 que as pessoas consigam se concentrar na cria\u00e7\u00e3o dos primeiros testes sem muito atrito.

Estas quest\u00f5es ser\u00e3o resolvidas nas aulas seguintes.

"},{"location":"03/#crud-e-http","title":"CRUD e HTTP","text":"

No desenvolvimento de APIs, existem quatro a\u00e7\u00f5es principais que fazemos com os dados: criar, ler, atualizar e excluir. Essas a\u00e7\u00f5es ajudam a gerenciar os dados no banco de dados e na aplica\u00e7\u00e3o web. Vamos nos focar nesse primeiro momento nas rela\u00e7\u00f5es entre os dados.

CRUD \u00e9 um acr\u00f4nimo que representa as quatro opera\u00e7\u00f5es b\u00e1sicas que voc\u00ea pode realizar em qualquer banco de dados persistente:

Com essas opera\u00e7\u00f5es podemos realizar qualquer tipo de comportamento em uma base dados. Podemos criar um registro, em seguida alter\u00e1-lo, quem sabe depois disso tudo delet\u00e1-lo.

Quando falamos de APIs servindo dados, todas essas opera\u00e7\u00f5es t\u00eam alguma forma similar no protocolo HTTP. O protocolo tem verbos para indicar essas mesmas a\u00e7\u00f5es que queremos representar no banco de dados.

Dessa forma podemos criar associa\u00e7\u00f5es entre os endpoints e a base de dados. Por exemplo: quando quisermos inserir um dado no banco de dados, n\u00f3s como clientes devemos comunicar essa inten\u00e7\u00e3o ao servidor usando o m\u00e9todo POST enviando os dados (em nosso caso no formato JSON) que devem ser persistidos na base de dados. Com isso iniciamos o processo de create na base de dados.

"},{"location":"03/#respostas-da-api","title":"Respostas da API","text":"

Usamos c\u00f3digos de status para informar ao cliente o resultado das opera\u00e7\u00f5es no servidor, como se um dado foi criado, encontrado, atualizado ou exclu\u00eddo com sucesso. Por isso investiremos mais algum momento aqui.

Os c\u00f3digos que devemos prestar aten\u00e7\u00e3o para responder corretamente as requisi\u00e7\u00f5es. Os casos de sucesso incluem:

Os c\u00f3digos de erro mais comuns que temos que conhecer para lidar com poss\u00edveis erros na aplica\u00e7\u00e3o, s\u00e3o:

Compreendendo esses c\u00f3digos, estamos prontos para iniciar a implementa\u00e7\u00e3o de alguns endpoints e colocar esses conceitos em pr\u00e1tica.

"},{"location":"03/#implementando-endpoints","title":"Implementando endpoints","text":"

Para facilitar o aprendizado, sugiro dividir a cria\u00e7\u00e3o de novos endpoints em tr\u00eas etapas:

  1. Rela\u00e7\u00e3o com o HTTP: Determinar o verbo HTTP esperado e os c\u00f3digos de resposta para situa\u00e7\u00f5es de sucesso e erro.
  2. Modelos de Dados: Definir o formato do JSON esperado, campos e seus tipos, e pensar nos modelos de resposta para situa\u00e7\u00f5es de sucesso e erro.
  3. Implementa\u00e7\u00e3o do Corpo: Decidir o tratamento dos dados recebidos e o tipo de processamento aplicado.

As duas primeiras etapas nos ajudam a definir a interface de comunica\u00e7\u00e3o e como ela ser\u00e1 documentada. A terceira etapa \u00e9 mais espec\u00edfica e envolve decis\u00f5es sobre a intera\u00e7\u00e3o com o banco de dados, valida\u00e7\u00f5es adicionais e a defini\u00e7\u00e3o do que constitui sucesso ou erro na requisi\u00e7\u00e3o.

Essas etapas nos orientam na implementa\u00e7\u00e3o completa do endpoint, garantindo que nada seja esquecido.

"},{"location":"03/#iniciando-a-implementacao-da-rota-post","title":"Iniciando a implementa\u00e7\u00e3o da rota POST","text":"

Nesta aula, nosso foco principal ser\u00e1 desenvolver um sistema de cadastro de usu\u00e1rios. Para isso, a implementa\u00e7\u00e3o de uma forma eficiente para criar novos usu\u00e1rios na base de dados \u00e9 essencial. Exploraremos como utilizar o verbo HTTP POST, fundamental para comunicar ao servi\u00e7o a nossa inten\u00e7\u00e3o de enviar novos dados, como no cadastro de usu\u00e1rios.

"},{"location":"03/#implementacao-do-endpoint","title":"Implementa\u00e7\u00e3o do endpoint","text":"

Para iniciar, criaremos um endpoint que aceita o verbo POST com dados em formato JSON. Esse endpoint responder\u00e1 com o status 201 em caso de sucesso na cria\u00e7\u00e3o do recurso. Com isso, estabelecemos a base para a nossa funcionalidade de cadastro.

Usaremos o decorador @app.post() do FastAPI para definir nosso endpoint, que come\u00e7ar\u00e1 com a URL /users/, indicando onde receberemos os dados para criar novos usu\u00e1rios:

fast_zero/app.py
@app.post('/users/')\ndef create_user():\n    ...\n
"},{"location":"03/#status-code-de-resposta","title":"Status code de resposta","text":"

\u00c9 crucial definir que, ao cadastrar um usu\u00e1rio com sucesso, o sistema deve retornar o c\u00f3digo de resposta 201 CREATED, indicando a cria\u00e7\u00e3o bem-sucedida do recurso. Para isso, adicionamos o par\u00e2metro status_code ao decorador:

fast_zero/app.py
@app.post('/users/', status_code=HTTPStatus.CREATED)\ndef create_user():\n    ...\n

Conversaremos em breve sobre os c\u00f3digos de resposta no t\u00f3pico do pydantic

"},{"location":"03/#modelo-de-dados","title":"Modelo de dados","text":"

O modelo de dados \u00e9 uma parte fundamental, onde consideramos tanto os dados recebidos do cliente quanto os dados que ser\u00e3o retornados a ele. Esta abordagem assegura uma comunica\u00e7\u00e3o eficaz e clara.

"},{"location":"03/#modelo-de-entrada-de-dados","title":"Modelo de entrada de dados","text":"

Para os dados de entrada, como estamos pensando em um cadastro de usu\u00e1rio na aplica\u00e7\u00e3o, \u00e9 importante que tenhamos insumos para identific\u00e1-lo como o email, uma senha (password) para que ele consiga fazer o login no futuro e seu nome de usu\u00e1rio (username). Dessa forma, podemos imaginar um modelo de entrada desta forma:

{\n    \"username\": \"joao123\",\n    \"email\": \"joao123@email.com\",\n    \"password\": \"segredo123\"\n}\n

Para a aplica\u00e7\u00e3o conseguir expor esse modelo na documenta\u00e7\u00e3o, devemos criar uma classe do pydantic em nosso arquivo de schemas (fast_zero/schemas.py) que represente esse schema:

fast_zero/schemas.py
class UserSchema(BaseModel):\n    username: str\n    email: str\n    password: str\n

Como j\u00e1 temos o endpoint definido, precisamos fazer a associa\u00e7\u00e3o do modelo com ele. Para fazer isso basta que o endpoint receba um par\u00e2metro e esse par\u00e2metro esteja associado a um modelo via anota\u00e7\u00e3o de par\u00e2metros:

fast_zero/app.py
from fast_zero.schemas import Message, UserSchema\n\n# ...\n\n@app.post('/users/', status_code=HTTPStatus.CREATED)\ndef create_user(user: UserSchema):\n    ...\n

Dessa forma, o modelo de entrada, o que o endpoint espera receber j\u00e1 est\u00e1 documentado e aparecer\u00e1 no swagger UI.

Para visualizar, temos que iniciar o servidor:

$ Execu\u00e7\u00e3o no terminal!
task run\n

E acessar a p\u00e1gina http://127.0.01:8000/docs. Isso nos mostrar\u00e1 as defini\u00e7\u00f5es do nosso endpoint usando o modelo no swagger:

"},{"location":"03/#modelo-de-saida-de-dados","title":"Modelo de sa\u00edda de dados","text":"

O modelo de sa\u00edda explica ao cliente quais dados ser\u00e3o retornados quando a chamada a esse endpoint for feita. Para a API ter um uso flu\u00eddo, temos que especificar o retorno corretamente na documenta\u00e7\u00e3o.

Se dermos uma olhada no estado atual de resposta da nossa API, podemos ver que a resposta no swagger \u00e9 \"string\" para o c\u00f3digo de resposta 201:

Quando fazemos uma chamada com o m\u00e9todo POST o esperado \u00e9 que os dados criados sejam retornados ao cliente. Poder\u00edamos usar o mesmo modelo de antes o UserSchema, por\u00e9m, por uma quest\u00e3o de seguran\u00e7a, seria ideal n\u00e3o retornar a senha do usu\u00e1rio. Quanto menos ela trafegar na rede, melhor.

Desta forma, podemos pensar no mesmo schema, por\u00e9m, sem a senha. Algo como:

{\n    \"username\": \"joao123\",\n    \"email\": \"joao123@email.com\"\n}\n

Transcrevendo isso em um modelo do pydantic em nosso arquivo de schemas (fast_zero/schemas.py) temos isso:

fast_zero/schemas.py
class UserPublic(BaseModel):\n    username: str\n    email: str\n

Para aplicar um modelo a resposta do endpoint, temos que passar o modelo ao par\u00e2metro response_model, como fizemos na aula passada:

fast_zero/app.py
from fast_zero.schemas import Message, UserPublic, UserSchema\n\n# C\u00f3digo omitido\n\n@app.post('/users/', status_code=HTTPStatus.CREATED, response_model=UserPublic)\ndef create_user(user: UserSchema):\n    ...\n

Tendo um modelo descritivo de resposta para o cliente na documenta\u00e7\u00e3o:

"},{"location":"03/#validacao-e-pydantic","title":"Valida\u00e7\u00e3o e pydantic","text":"

Um ponto crucial do Pydantic \u00e9 sua habilidade de checar se os dados est\u00e3o corretos enquanto o programa est\u00e1 rodando, garantindo que tudo esteja conforme esperado. Fazendo com que, caso o cliente envie um dado que n\u00e3o corresponde com o schema definido, seja levantado um erro 422. E caso a nossa resposta como servidor tamb\u00e9m n\u00e3o siga o schema, ser\u00e1 levantado um erro 500. Fazendo com que ele seja uma garantia de duas vias, nossa API segue a especifica\u00e7\u00e3o da documenta\u00e7\u00e3o.

Quando relacionamos um modelo \u00e0 resposta do enpoint, o Pydantic de forma autom\u00e1tica, cria um schema chamado HTTPValidationError:

Esse modelo \u00e9 usado quando o JSON enviado na requisi\u00e7\u00e3o n\u00e3o cumpre os requisitos do schema.

Por exemplo, se fizermos uma requisi\u00e7\u00e3o que foge dos padr\u00f5es definidos no schema:

Essa requisi\u00e7\u00e3o foge dos padr\u00f5es, pois n\u00e3o envia o campo password e envia o tipo de dado errado para email.

Com isso, receberemos um erro 422 UNPROCESSABLE ENTITY, dizendo que nosso schema foi violado e a resposta cont\u00e9m os detalhes dos campos faltantes ou mal formatados:

A mensagem completa de retorno do servidor mostra de forma detalhada os erros de valida\u00e7\u00e3o encontrados em cada campo, individualmente no campo details:

{\n  \"detail\": [\n    {\n      \"type\": \"string_type\", # (1)!\n      \"loc\": [\n        \"body\",\n        \"email\" # (2)!\n      ],\n      \"msg\": \"Input should be a valid string\", #(3)!\n      \"input\": 1,\n      \"url\": \"https://errors.pydantic.dev/2.5/v/string_type\"\n    },\n    {\n      \"type\": \"missing\", #(4)!\n      \"loc\": [\n        \"body\",\n        \"password\" #(5)!\n      ],\n      \"msg\": \"Field required\", #(6)!\n      \"input\": {\n        \"username\": \"string\",\n        \"email\": 1\n      },\n      \"url\": \"https://errors.pydantic.dev/2.5/v/missing\"\n    }\n  ]\n}\n
  1. Tipo de erro no campo: Era esperado que o valor fosse string (string_type)
  2. Campo notificado
  3. Mensagem que acompanha o erro
  4. Tipo de erro no campo: Era esperado que o valor fosse enviado (missing)
  5. Campo notificado
  6. Mensagem que acompanha o erro

Vemos que o pydantic desempenha um papel bastante importante no funcionamento da API. Pois ele consegue \"barrar\" o request antes dele ser exposto \u00e0 nossa fun\u00e7\u00e3o de endpoint. Evitando que diversos casos estranhos sejam cobertos de forma transparente. Tanto em rela\u00e7\u00e3o aos tipos dos campos, quanto em rela\u00e7\u00e3o aos campos que deveriam ser enviados, mas n\u00e3o foram.

"},{"location":"03/#estendendo-a-validacao-com-e-mail","title":"Estendendo a valida\u00e7\u00e3o com e-mail","text":"

Outro ponto que deve ser considerado \u00e9 a capacidade de estender os campos usados pelo pydantic nas anota\u00e7\u00f5es de tipo.

Para garantir que o campo email realmente contenha um e-mail v\u00e1lido, podemos usar uma ferramenta especial do Pydantic que verifica se o e-mail tem o formato correto, como a presen\u00e7a de @ e um dom\u00ednio v\u00e1lido.

Para isso, o pydantic tem um tipo de dado espec\u00edfico, o EmailStr. Que garante que o valor que ser\u00e1 recebido pelo schema, seja de fato um e-mail em formato v\u00e1lido. Podemos adicion\u00e1-lo ao campo email nos modelos UserSchema e UserPublic:

from pydantic import BaseModel, EmailStr\n\n# C\u00f3digo omitido\n\nclass UserSchema(BaseModel):\n    username: str\n    email: EmailStr\n    password: str\n\nclass UserPublic(BaseModel):\n    username: str\n    email: EmailStr\n

Embora essa seja um defini\u00e7\u00e3o valida do schema, o pydantic depende que a extens\u00e3o de emails seja instalada em nosso ambiente. Ao executar o servidor, podemos ver que o erro \u00e9 bastante descritivo:

File \"<...>/.venv/<...>/pydantic/networks.py\", line 354, in import_email_validator\n  raise ImportError('email-validator is not installed, run `pip install pydantic[email]`') from e\nImportError: email-validator is not installed, run `pip install pydantic[email]`\n

Traduzindo de forma literal, ter\u00edamos algo como: ImportError: email-validator n\u00e3o est\u00e1 instalado, rode 'pip install pydantic[email]'.

Como estamos usando o poetry como gerenciador de pacotes, podemos executar dessa forma:

$ Execu\u00e7\u00e3o no terminal!
poetry add \"pydantic[email]\"\n

Com isso, o pydantic ir\u00e1 oferecer um exemplo de email no swagger \"user@example.com\" e acerta os schemas para fazer essas valida\u00e7\u00f5es:

Dessa forma, o campo esperar\u00e1 n\u00e3o somente uma string como antes, mas um endere\u00e7o de email v\u00e1lido.

"},{"location":"03/#validacao-da-resposta","title":"Valida\u00e7\u00e3o da resposta","text":"

Ap\u00f3s aperfei\u00e7oarmos nossos modelos do Pydantic para garantir que os dados de entrada e sa\u00edda estejam corretamente validados, chegamos a um ponto crucial: a implementa\u00e7\u00e3o do corpo do nosso endpoint. At\u00e9 agora, nosso endpoint est\u00e1 definido, mas sem uma l\u00f3gica de processamento real, conforme mostrado abaixo:

fast_zero/app.py
@app.post('/users/', status_code=HTTPStatus.CREATED, response_model=UserPublic)\ndef create_user(user: UserSchema):\n    ...\n

Este \u00e9 o momento perfeito para realizar um request e observar diretamente a atua\u00e7\u00e3o do Pydantic na valida\u00e7\u00e3o da resposta. Ao tentarmos executar um request v\u00e1lido, sem a implementa\u00e7\u00e3o adequada do endpoint, nos deparamos com uma situa\u00e7\u00e3o interessante: o Pydantic tenta validar a resposta que o nosso endpoint deveria fornecer, mas, como ainda n\u00e3o implementamos essa l\u00f3gica, o resultado n\u00e3o atende ao schema definido.

A tentativa resulta em uma mensagem de erro exibida diretamente no Swagger, indicando um erro de servidor interno (HTTP 500). Esse tipo de erro sugere que algo deu errado no lado do servidor, mas n\u00e3o oferece detalhes espec\u00edficos sobre a natureza do problema para o cliente. O erro 500 \u00e9 uma resposta gen\u00e9rica para indicar falhas no servidor.

Para investigar a causa exata do erro 500, \u00e9 necess\u00e1rio consultar o console ou os logs de nossa aplica\u00e7\u00e3o, onde os detalhes do erro s\u00e3o registrados. Neste caso, o erro apresentado nos logs \u00e9 o seguinte:

raise ResponseValidationError( # (1)!\nfastapi.exceptions.ResponseValidationError: 1 validation errors:\n{\n  \"type\":\"model_attributes_type\",\n  \"loc\": (\"response\"),\n  \"msg\":\"Input should be a valid dictionary or object to extract fields from\",#(2)!\n  \"input\":\"None\", #(3)!\n  \"url\":\"https://errors.pydantic.dev/2.6/v/model_attributes_type\"\n}\n
  1. O json dessa mensagem de erro aparece em uma \u00fanica linha. Eu fiz a quebra para que ficasse mais f\u00e1cil de lermos a resposta.
  2. A mensagem de erro diz que o input passado ao modelo do pydantic deveria ser um dicion\u00e1rio v\u00e1lido ou um objeto onde os campos poderiam ser extra\u00eddos.
  3. O objeto enviado ao modelo

Essencialmente, o erro nos informa que o modelo esperava receber um objeto v\u00e1lido para processamento, mas, em vez disso, recebeu None. Isso ocorre porque ainda n\u00e3o implementamos a l\u00f3gica para processar o input recebido e retornar uma resposta adequada que corresponda ao modelo UserPublic.

Agora, tendo identificado a capacidade do Pydantic em validar as respostas e encontrarmos um erro devido \u00e0 falta de implementa\u00e7\u00e3o, podemos proceder com uma solu\u00e7\u00e3o simples. Para come\u00e7ar, podemos utilizar diretamente os dados recebidos em user e retorn\u00e1-los. Esta a\u00e7\u00e3o simples j\u00e1 \u00e9 suficiente para satisfazer o schema, pois o objeto user cont\u00e9m os atributos email e username, esperados pelo modelo UserPublic:

fast_zero/app.py
@app.post('/users/', status_code=HTTPStatus.CREATED, response_model=UserPublic)\ndef create_user(user: UserSchema):\n    return user\n

Este retorno simples do objeto user garante que o schema seja cumprido. Agora, ao realizarmos novamente a chamada no Swagger, o objeto que enviamos \u00e9 retornado conforme esperado, mas sem expor a senha, alinhado ao modelo UserPublic e emitindo uma resposta com o c\u00f3digo 201:

Essa abordagem nos permite fechar o ciclo de valida\u00e7\u00e3o, demonstrando a efic\u00e1cia do Pydantic na garantia de que os dados de resposta estejam corretos. Com essa implementa\u00e7\u00e3o simples, estabelecemos a base para o desenvolvimento real do c\u00f3digo do endpoint POST, preparando o terreno para uma l\u00f3gica mais complexa que envolver\u00e1 a cria\u00e7\u00e3o e o manejo de usu\u00e1rios dentro de nossa aplica\u00e7\u00e3o.

"},{"location":"03/#de-volta-ao-post","title":"De volta ao POST","text":"

Agora que j\u00e1 dominamos a defini\u00e7\u00e3o dos modelos, podemos prosseguir com a aula e a implementa\u00e7\u00e3o dos endpoints. Vamos retomar a implementa\u00e7\u00e3o do POST, adicionando um banco de dados falso/simulado em mem\u00f3ria. Isso nos permitir\u00e1 explorar as opera\u00e7\u00f5es do CRUD sem a complexidade da implementa\u00e7\u00e3o de um banco de dados real, facilitando a assimila\u00e7\u00e3o dos muitos conceitos discutidos nesta aula.

"},{"location":"03/#criando-um-banco-de-dados-falso","title":"Criando um banco de dados falso","text":"

Para interagir com essas rotas de maneira pr\u00e1tica, vamos criar uma lista provis\u00f3ria que simular\u00e1 um banco de dados. Isso nos permitir\u00e1 adicionar dados e entender o funcionamento do FastAPI. Portanto, introduzimos uma lista provis\u00f3ria para atuar como nosso \"banco\" e modificamos nosso endpoint para inserir nossos modelos do Pydantic nessa lista:

fast_zero/app.py
from fast_zero.schemas import Message, UserDB, UserPublic, UserSchema\n\n# c\u00f3digo omitido\n\ndatabase = []  #(1)!\n\n# c\u00f3digo omitido\n\n@app.post('/users/', status_code=HTTPStatus.CREATED, response_model=UserPublic)\ndef create_user(user: UserSchema):\n    user_with_id = UserDB(**user.model_dump(), id=len(database) + 1)  #(2)!\n\n    database.append(user_with_id)\n\n    return user_with_id\n
  1. A lista database \u00e9 provis\u00f3ria. Ela est\u00e1 aqui s\u00f3 para entendermos os conceitos de crud, nas pr\u00f3ximas aulas vamos trabalhar com a cria\u00e7\u00e3o do banco de dados definitivo.

  2. .model_dump() \u00e9 um m\u00e9todo de modelos do pydantic que converte o objeto em dicion\u00e1rio. Por exemplo, user.model_dump() faria a convers\u00e3o em {'username': 'nome do usu\u00e1rio', 'password': 'senha do usu\u00e1rio', 'email': 'email do usu\u00e1rio'}. Os ** querem dizer que o dicion\u00e1rio ser\u00e1 desempacotado em par\u00e2metros. Fazendo com que a chamada seja equivalente a UserDB(username='nome do usu\u00e1rio', password='senha do usu\u00e1rio', email='email do usu\u00e1rio', id=len(database) + 1)

Para simular um banco de dados de forma mais realista, \u00e9 essencial que cada usu\u00e1rio tenha um ID \u00fanico. Portanto, ajustamos nosso modelo de resposta p\u00fablica (UserPublic) para incluir o ID do usu\u00e1rio. Tamb\u00e9m introduzimos um novo modelo, UserDB, que inclui tanto a senha do usu\u00e1rio quanto seu identificador \u00fanico:

fast_zero/schemas.py
class UserPublic(BaseModel):\n    id: int\n    username: str\n    email: EmailStr\n\n\nclass UserDB(UserSchema):\n    id: int\n

Essa abordagem simples nos permite avan\u00e7ar na constru\u00e7\u00e3o dos outros endpoints. \u00c9 crucial testar esse endpoint para assegurar seu correto funcionamento.

"},{"location":"03/#implementando-o-teste-da-rota-post","title":"Implementando o teste da rota POST","text":"

Antes de prosseguir, vamos verificar a cobertura de nossos testes:

$ Execu\u00e7\u00e3o no terminal!
task test\n\n# parte da resposta foi omitida\n\n---------- coverage: platform linux, python 3.11.3-final-0 -----------\nName                    Stmts   Miss  Cover\n-------------------------------------------\nfast_zero/__init__.py       0      0   100%\nfast_zero/app.py           12      3    75%\nfast_zero/schemas.py       11      0   100%\n-------------------------------------------\nTOTAL                      23      3    87%\n\n# parte da resposta foi omitida\n

Vemos que temos 3 Miss. Possivelmente das linhas que acabamos de escrever. Podemos olhar o HTML do coverage para ter certeza:

Ent\u00e3o, vamos escrever nosso teste. Esse teste para a rota POST precisa verificar se a cria\u00e7\u00e3o de um novo usu\u00e1rio funciona corretamente. Enviamos uma solicita\u00e7\u00e3o POST com um novo usu\u00e1rio para a rota /users/. Em seguida, verificamos se a resposta tem o status HTTP 201 (Criado) e se a resposta cont\u00e9m o novo usu\u00e1rio criado.

tests/test_app.py
def test_create_user():\n    client = TestClient(app)\n\n    response = client.post(\n        '/users/',\n        json={\n            'username': 'alice',\n            'email': 'alice@example.com',\n            'password': 'secret',\n        },\n    )\n    assert response.status_code == HTTPStatus.CREATED\n    assert response.json() == {\n        'username': 'alice',\n        'email': 'alice@example.com',\n        'id': 1,\n    }\n

Ao executar o teste:

$ Execu\u00e7\u00e3o no terminal!
task test\n\n# parte da resposta foi omitida\n\ntests/test_app.py::test_root_deve_retornar_ok_e_ola_mundo PASSED\ntests/test_app.py::test_create_user PASSED\n\n---------- coverage: platform linux, python 3.11.3-final-0 -----------\nName                    Stmts   Miss  Cover\n-------------------------------------------\nfast_zero/__init__.py       0      0   100%\nfast_zero/app.py           12      0   100%\nfast_zero/schemas.py       11      0   100%\n-------------------------------------------\nTOTAL                      23      0   100%\n\n# parte da resposta foi omitida\n
"},{"location":"03/#nao-se-repita-dry","title":"N\u00e3o se repita (DRY)","text":"

Voc\u00ea deve ter notado que a linha client = TestClient(app) est\u00e1 repetida na primeira linha dos dois testes que fizemos. Repetir c\u00f3digo pode tornar o gerenciamento de testes mais complexo \u00e0 medida que cresce, e \u00e9 aqui que o princ\u00edpio de \"N\u00e3o se repita\" (DRY) entra em jogo. DRY incentiva a redu\u00e7\u00e3o da repeti\u00e7\u00e3o, criando um c\u00f3digo mais limpo e manuten\u00edvel.

Para solucionar essa repeti\u00e7\u00e3o, podemos usar uma funcionalidade do pytest chamada Fixture. Uma fixture \u00e9 como uma fun\u00e7\u00e3o que prepara dados ou estado necess\u00e1rios para o teste. Pode ser pensada como uma forma de n\u00e3o repetir a fase de Arrange de um teste, simplificando a chamada e n\u00e3o repetindo c\u00f3digo.

Se fixtures s\u00e3o uma novidade para voc\u00ea

Existe uma live de Python onde discutimos especificamente sobre fixtures

Link direto

Neste caso, criaremos uma fixture que retorna nosso client. Para fazer isso, precisamos criar o arquivo tests/conftest.py. O arquivo conftest.py \u00e9 um arquivo especial reconhecido pelo pytest que permite definir fixtures que podem ser reutilizadas em diferentes m\u00f3dulos de teste em um projeto. \u00c9 uma forma de centralizar recursos comuns de teste.

tests/conftest.py
import pytest\nfrom fastapi.testclient import TestClient\n\nfrom fast_zero.app import app\n\n\n@pytest.fixture\ndef client():\n    return TestClient(app)\n

Agora, em vez de repetir a cria\u00e7\u00e3o do client em cada teste, podemos simplesmente passar a fixture como um argumento nos nossos testes:

tests/test_app.py
from http import HTTPStatus\n\n\ndef test_root_deve_retornar_ok_e_ola_mundo(client):\n    response = client.get('/')\n\n    assert response.status_code == HTTPStatus.OK\n    assert response.json() == {'message': 'Ol\u00e1 Mundo!'}\n\n\ndef test_create_user(client):\n    response = client.post(\n        '/users/',\n        json={\n            'username': 'alice',\n            'email': 'alice@example.com',\n            'password': 'secret',\n        },\n    )\n    assert response.status_code == HTTPStatus.CREATED\n    assert response.json() == {\n        'username': 'alice',\n        'email': 'alice@example.com',\n        'id': 1,\n    }\n

Com essa simples mudan\u00e7a, conseguimos tornar nosso c\u00f3digo mais limpo e f\u00e1cil de manter, seguindo o princ\u00edpio DRY.

Vemos que estamos no caminho certo. Agora que a rota POST est\u00e1 implementada, seguiremos para a pr\u00f3xima opera\u00e7\u00e3o CRUD: Read.

"},{"location":"03/#implementando-a-rota-get","title":"Implementando a Rota GET","text":"

A rota GET \u00e9 usada para recuperar informa\u00e7\u00f5es de um ou mais usu\u00e1rios do nosso sistema. No contexto do CRUD, o verbo HTTP GET est\u00e1 associado \u00e0 opera\u00e7\u00e3o \"Read\". Se a solicita\u00e7\u00e3o for bem-sucedida, a rota deve retornar o status HTTP 200 (OK).

Para estruturar a resposta dessa rota, podemos criar um novo modelo chamado UserList. Este modelo representar\u00e1 uma lista de usu\u00e1rios e cont\u00e9m apenas um campo chamado users, que \u00e9 uma lista de UserPublic. Isso nos permite retornar m\u00faltiplos usu\u00e1rios de uma vez.

fast_zero/schemas.py
class UserList(BaseModel):\n    users: list[UserPublic]\n

Com esse modelo definido, podemos criar nosso endpoint GET. Este endpoint retornar\u00e1 uma inst\u00e2ncia de UserList, que por sua vez cont\u00e9m uma lista de UserPublic. Cada UserPublic \u00e9 criado a partir dos dados de um usu\u00e1rio em nosso banco de dados fict\u00edcio.

fast_zero/app.py
from fast_zero.schemas import Message, UserDB, UserList, UserPublic, UserSchema\n\n# c\u00f3digo omitido\n\n@app.get('/users/', response_model=UserList)\ndef read_users():\n    return {'users': database}\n

Com essa implementa\u00e7\u00e3o, nossa API agora pode retornar uma lista de usu\u00e1rios. No entanto, nosso trabalho ainda n\u00e3o acabou. A pr\u00f3xima etapa \u00e9 escrever testes para garantir que nossa rota GET est\u00e1 funcionando corretamente. Isso nos ajudar\u00e1 a identificar e corrigir quaisquer problemas antes de prosseguirmos com a implementa\u00e7\u00e3o de outras rotas.

"},{"location":"03/#implementando-o-teste-da-rota-de-get","title":"Implementando o teste da rota de GET","text":"

Nosso teste da rota GET tem que verificar se a recupera\u00e7\u00e3o dos usu\u00e1rios est\u00e1 funcionando corretamente. Enviamos uma solicita\u00e7\u00e3o GET para a rota /users/. Em seguida, verificamos se a resposta tem o status HTTP 200 (OK) e se a resposta cont\u00e9m a lista de usu\u00e1rios.

tests/test_app.py
def test_read_users(client):\n    response = client.get('/users/')\n    assert response.status_code == HTTPStatus.OK\n    assert response.json() == {\n        'users': [\n            {\n                'username': 'alice',\n                'email': 'alice@example.com',\n                'id': 1,\n            }\n        ]\n    }\n

Com as rotas POST e GET implementadas, agora podemos criar e recuperar usu\u00e1rios. Implementaremos a pr\u00f3xima opera\u00e7\u00e3o CRUD: Update.

Coisas que devemos considerar sobre este e os pr\u00f3ximos testes

Note que para que esse teste seja executado com sucesso o teste do endpoint de POST tem que ser executado antes. Isso \u00e9 problem\u00e1tico no mundo dos testes. Pois cada teste deve estar isolado e n\u00e3o depender da execu\u00e7\u00e3o de nada externo a ele.

Para que isso aconte\u00e7a, precisaremos de um mecanismo que reinicie o banco de dados a cada teste, mas ainda n\u00e3o temos um banco de dados real. O banco de dados ser\u00e1 introduzido na aplica\u00e7\u00e3o na aula 04.

O mecanismo que far\u00e1 com que os testes n\u00e3o interfiram em outros e sejam independentes ser\u00e1 introduzido na aula 05.

"},{"location":"03/#implementando-a-rota-put","title":"Implementando a Rota PUT","text":"

A rota PUT \u00e9 usada para atualizar as informa\u00e7\u00f5es de um usu\u00e1rio existente. No contexto do CRUD, o verbo HTTP PUT est\u00e1 associado \u00e0 opera\u00e7\u00e3o \"Update\". Se a solicita\u00e7\u00e3o for bem-sucedida, a rota deve retornar o status HTTP 200 (OK). No entanto, se o usu\u00e1rio solicitado n\u00e3o for encontrado, dever\u00edamos retornar o status HTTP 404 (N\u00e3o Encontrado).

Uma caracter\u00edstica importante do verbo PUT \u00e9 que ele \u00e9 direcionado a um recurso em espec\u00edfico. Nesse caso, estamos direcionando a altera\u00e7\u00e3o a um user em espec\u00edfico na base de dados. O identificador de user \u00e9 o campo id que estamos usando nos modelos do Pydantic. Nesse caso, nosso endpoint deve receber o identificador de quem ser\u00e1 alterado.

Para fazer essa identifica\u00e7\u00e3o do recurso na URL usamos a seguinte combina\u00e7\u00e3o /caminho/recurso. Mas, como o recurso \u00e9 din\u00e2mico, ele deve ser enviado pelo cliente. Fazendo com que o valor tenha que ser uma vari\u00e1vel. Dentro do FastAPI, as vari\u00e1veis de recursos s\u00e3o descritas dentro de {}, como {user_id}. Fazendo com que o caminho completo do nosso endpoint seja '/users/{user_id}'. Da seguinte forma:

fast_zero/app.py
from fastapi import FastAPI, HTTPException\n\n# ...\n\n@app.put('/users/{user_id}', response_model=UserPublic)\ndef update_user(user_id: int, user: UserSchema):\n    if user_id > len(database) or user_id < 1: #(1)!\n        raise HTTPException(\n            status_code=HTTPStatus.NOT_FOUND, detail='User not found'\n        ) #(2)!\n\n    user_with_id = UserDB(**user.model_dump(), id=user_id)\n    database[user_id - 1] = user_with_id #(3)!\n\n    return user_with_id\n
  1. Aqui fazemos uma valida\u00e7\u00e3o simples, verificamos se o id n\u00e3o \u00e9 maior que o tamanho da lista user_id > len(database) e o n\u00famero enviado para o user_id \u00e9 um valor positivo user_id < 1.
  2. Levantamos um erro do tipo HTTPException para dizer que o usu\u00e1rio n\u00e3o existe na base de dados. Esse modelo j\u00e1 est\u00e1 presente no swagger com HTTPValidationError.
  3. Nessa linha, estamos substituindo a posi\u00e7\u00e3o (user_id - 1) na lista pelo novo objeto.

Para que essa vari\u00e1vel definida na URL seja transferida para nosso endpoint, devemos adicionar um par\u00e2metro na fun\u00e7\u00e3o com o mesmo nome da vari\u00e1vel definida. Como def update_user(user_id: int) na linha em destaque.

"},{"location":"03/#implementando-o-teste-da-rota-de-put","title":"Implementando o teste da rota de PUT","text":"

Nosso teste da rota PUT precisa verificar se a atualiza\u00e7\u00e3o de um usu\u00e1rio existente funciona corretamente. Enviamos uma solicita\u00e7\u00e3o PUT com as novas informa\u00e7\u00f5es do usu\u00e1rio para a rota /users/{user_id}. Em seguida, verificamos se a resposta tem o status HTTP 200 (OK) e se a resposta cont\u00e9m o usu\u00e1rio atualizado.

tests/test_app.py
def test_update_user(client):\n    response = client.put(\n        '/users/1',\n        json={\n            'username': 'bob',\n            'email': 'bob@example.com',\n            'password': 'mynewpassword',\n        },\n    )\n    assert response.status_code == HTTPStatus.OK\n    assert response.json() == {\n        'username': 'bob',\n        'email': 'bob@example.com',\n        'id': 1,\n    }\n

Com as rotas POST, GET e PUT implementadas, agora podemos criar, recuperar e atualizar usu\u00e1rios. A \u00faltima opera\u00e7\u00e3o CRUD que precisamos implementar \u00e9 Delete.

"},{"location":"03/#implementando-a-rota-delete","title":"Implementando a Rota DELETE","text":"

A rota DELETE \u00e9 usada para excluir um usu\u00e1rio do nosso sistema. No contexto do CRUD, o verbo HTTP DELETE est\u00e1 associado \u00e0 opera\u00e7\u00e3o \"Delete\". Se a solicita\u00e7\u00e3o for bem-sucedida, a rota deve retornar o status HTTP 200 (OK). No entanto, se o usu\u00e1rio solicitado n\u00e3o for encontrado, dever\u00edamos retornar o status HTTP 404 (N\u00e3o Encontrado).

Este endpoint receber\u00e1 o ID do usu\u00e1rio que queremos excluir. Note que, estamos lan\u00e7ando uma exce\u00e7\u00e3o HTTP quando o ID do usu\u00e1rio est\u00e1 fora do range da nossa lista (simula\u00e7\u00e3o do nosso banco de dados). Quando conseguimos excluir o usu\u00e1rio com sucesso, retornamos a mensagem de sucesso em um modelo do tipo Message.

fast_zero/app.py
@app.delete('/users/{user_id}', response_model=Message)\ndef delete_user(user_id: int):\n    if user_id > len(database) or user_id < 1:\n        raise HTTPException(\n            status_code=HTTPStatus.NOT_FOUND, detail='User not found'\n        )\n\n    del database[user_id - 1]\n\n    return {'message': 'User deleted'}\n

Com a implementa\u00e7\u00e3o da rota DELETE conclu\u00edda, \u00e9 fundamental garantirmos que essa rota est\u00e1 funcionando conforme o esperado. Para isso, precisamos escrever testes para essa rota.

"},{"location":"03/#implementando-o-teste-da-rota-de-delete","title":"Implementando o teste da rota de DELETE","text":"

Nosso teste da rota DELETE precisa verificar se a exclus\u00e3o de um usu\u00e1rio existente funciona corretamente. Enviamos uma solicita\u00e7\u00e3o DELETE para a rota /users/{user_id}. Em seguida, verificamos se a resposta tem o status HTTP 200 (OK) e se a resposta cont\u00e9m uma mensagem informando que o usu\u00e1rio foi exclu\u00eddo.

tests/test_app.py
def test_delete_user(client):\n    response = client.delete('/users/1')\n\n    assert response.status_code == HTTPStatus.OK\n    assert response.json() == {'message': 'User deleted'}\n
"},{"location":"03/#checando-tudo-antes-do-commit","title":"Checando tudo antes do commit","text":"

Antes de fazermos o commit, \u00e9 uma boa pr\u00e1tica checarmos todo o c\u00f3digo, e podemos fazer isso com as a\u00e7\u00f5es que criamos com o taskipy.

$ Execu\u00e7\u00e3o no terminal!
$ task lint\n
$ Execu\u00e7\u00e3o no terminal!
$ task format\n6 files left unchanged\n
$ Execu\u00e7\u00e3o no terminal!
$ task test\n...\ntests/test_app.py::test_root_deve_retornar_ok_e_ola_mundo PASSED\ntests/test_app.py::test_create_user PASSED\ntests/test_app.py::test_read_users PASSED\ntests/test_app.py::test_update_user PASSED\ntests/test_app.py::test_delete_user PASSED\n\n---------- coverage: platform linux, python 3.11.4-final-0 -----------\nName                   Stmts   Miss  Cover\n------------------------------------------\nfastzero/__init__.py       0      0   100%\nfastzero/app.py           28      2    93%\nfastzero/schemas.py       15      0   100%\n------------------------------------------\nTOTAL                     43      2    95%\n\n\n============================ 5 passed in 1.48s =============================\nWrote HTML report to htmlcov/index.html\n
"},{"location":"03/#commit","title":"Commit","text":"

Ap\u00f3s toda essa jornada de aprendizado, constru\u00e7\u00e3o e teste de rotas, chegou a hora de registrar nosso progresso utilizando o git. Fazer commits regulares \u00e9 uma boa pr\u00e1tica, pois mant\u00e9m um hist\u00f3rico detalhado das altera\u00e7\u00f5es e facilita a volta a uma vers\u00e3o anterior do c\u00f3digo, se necess\u00e1rio.

Primeiramente, verificaremos as altera\u00e7\u00f5es feitas no projeto com o comando git status. Este comando nos mostrar\u00e1 todos os arquivos modificados que ainda n\u00e3o foram inclu\u00eddos em um commit.

$ Execu\u00e7\u00e3o no terminal!
git status\n

Em seguida, adicionaremos todas as altera\u00e7\u00f5es para o pr\u00f3ximo commit. O comando git add . adiciona todas as altera\u00e7\u00f5es feitas em todos os arquivos do projeto.

$ Execu\u00e7\u00e3o no terminal!
git add .\n

Agora, estamos prontos para fazer o commit. Com o comando git commit, criamos uma nova entrada no hist\u00f3rico do nosso projeto. \u00c9 importante adicionar uma mensagem descritiva ao commit, para que, no futuro, outras pessoas ou n\u00f3s mesmos entendamos o que foi alterado. Nesse caso, a mensagem do commit poderia ser \"Implementando rotas CRUD\".

$ Execu\u00e7\u00e3o no terminal!
git commit -m \"Implementando rotas CRUD\"\n

Por fim, enviamos nossas altera\u00e7\u00f5es para o reposit\u00f3rio remoto com git push. Se voc\u00ea tiver v\u00e1rias branches, certifique-se de estar na branch correta antes de executar este comando.

$ Execu\u00e7\u00e3o no terminal!
git push\n

E pronto! As altera\u00e7\u00f5es est\u00e3o seguras no hist\u00f3rico do git, e podemos continuar com o pr\u00f3ximo passo do projeto.

"},{"location":"03/#exercicios","title":"Exerc\u00edcios","text":"
  1. Escrever um teste para o erro de 404 (NOT FOUND) para o endpoint de PUT;
  2. Escrever um teste para o erro de 404 (NOT FOUND) para o endpoint de DELETE;
  3. Criar um endpoint de GET para pegar um \u00fanico recurso como users/{id} e fazer seus testes para 200 e 404.

Exerc\u00edcios resolvidos

"},{"location":"03/#conclusao","title":"Conclus\u00e3o","text":"

Com a implementa\u00e7\u00e3o bem-sucedida das rotas CRUD, demos um passo significativo na constru\u00e7\u00e3o de uma funcional com FastAPI. Agora podemos manipular usu\u00e1rios - criar, ler, atualizar e excluir - o que \u00e9 fundamental para muitos sistemas de informa\u00e7\u00e3o.

O papel dos testes em cada etapa n\u00e3o pode ser subestimado. Testes n\u00e3o apenas nos ajudam a assegurar que nosso c\u00f3digo est\u00e1 funcionando como esperado, mas tamb\u00e9m nos permitem refinar nossas solu\u00e7\u00f5es e detectar problemas potenciais antes que eles afetem a funcionalidade geral do nosso sistema. Nunca subestime a import\u00e2ncia de executar seus testes sempre que fizer uma altera\u00e7\u00e3o em seu c\u00f3digo!

At\u00e9 aqui, no entanto, trabalhamos com um \"banco de dados\" provis\u00f3rio, na forma de uma lista Python, que \u00e9 vol\u00e1til e n\u00e3o persiste os dados de uma execu\u00e7\u00e3o do aplicativo para outra. Para nosso aplicativo ser \u00fatil em um cen\u00e1rio do mundo real, precisamos armazenar nossos dados de forma mais duradoura. \u00c9 a\u00ed que os bancos de dados entram.

Outro ponto que deve ser destacado \u00e9 que nossas implementa\u00e7\u00f5es de testes sofrem interfer\u00eancia dos testes anteriores. Testes devem funcionar de forma isolada, sem a depend\u00eancia de um teste anterior. Vamos ajustar isso no futuro.

No pr\u00f3ximo t\u00f3pico, exploraremos uma das partes mais cr\u00edticas de qualquer aplicativo - a conex\u00e3o e intera\u00e7\u00e3o com um banco de dados. Aprenderemos a integrar nosso aplicativo FastAPI com um banco de dados real, permitindo a persist\u00eancia de nossos dados de usu\u00e1rio entre as sess\u00f5es do aplicativo.

Agora que a aula acabou, \u00e9 um bom momento para voc\u00ea relembrar alguns conceitos e fixar melhor o conte\u00fado respondendo ao question\u00e1rio referente a ela.

Quiz

"},{"location":"04/","title":"Configurando o banco de dados e gerenciando migra\u00e7\u00f5es com Alembic","text":""},{"location":"04/#configurando-o-banco-de-dados-e-gerenciando-migracoes-com-alembic","title":"Configurando o Banco de Dados e Gerenciando Migra\u00e7\u00f5es com Alembic","text":"

Objetivos dessa aula:

Caso prefira ver a aula em v\u00eddeo

Esse aula ainda n\u00e3o est\u00e1 dispon\u00edvel em formato de v\u00eddeo, somente em texto ou live!

Aula Slides C\u00f3digo Quiz Exerc\u00edcios

Com os endpoints da nossa API j\u00e1 estabelecidos, estamos, por ora, utilizando um banco de dados simulado, armazenando uma lista em mem\u00f3ria. Nesta aula, iniciaremos o processo de configura\u00e7\u00e3o do nosso banco de dados real. Nossa agenda inclui a instala\u00e7\u00e3o do SQLAlchemy, a defini\u00e7\u00e3o do modelo de usu\u00e1rios, e a execu\u00e7\u00e3o da primeira migra\u00e7\u00e3o com o Alembic para um banco de dados evolutivo. Al\u00e9m disso, exploraremos como desacoplar as configura\u00e7\u00f5es do banco de dados da aplica\u00e7\u00e3o, seguindo os princ\u00edpios dos 12 fatores.

Antes de prosseguirmos com a instala\u00e7\u00e3o e a configura\u00e7\u00e3o, \u00e9 crucial entender alguns conceitos fundamentais sobre ORMs (Object-Relational Mapping).

"},{"location":"04/#o-que-e-um-orm-e-por-que-usamos-um","title":"O que \u00e9 um ORM e por que usamos um?","text":"

ORM significa Mapeamento Objeto-Relacional. \u00c9 uma t\u00e9cnica de programa\u00e7\u00e3o que vincula (ou mapeia) objetos a registros de banco de dados. Em outras palavras, um ORM permite que voc\u00ea interaja com seu banco de dados, como se voc\u00ea estivesse trabalhando com objetos Python.

O SQLAlchemy \u00e9 um exemplo de ORM. Ele permite que voc\u00ea trabalhe com bancos de dados SQL de maneira mais natural aos programadores Python. Em vez de escrever consultas SQL cruas, voc\u00ea pode usar m\u00e9todos e atributos Python para manipular seus registros de banco de dados.

Mas por que usar\u00edamos um ORM? Aqui est\u00e3o algumas raz\u00f5es:

"},{"location":"04/#configuracoes-de-ambiente-e-os-12-fatores","title":"Configura\u00e7\u00f5es de ambiente e os 12 fatores","text":"

Uma boa pr\u00e1tica no desenvolvimento de aplica\u00e7\u00f5es \u00e9 separar as configura\u00e7\u00f5es do c\u00f3digo. Configura\u00e7\u00f5es, como credenciais de banco de dados, s\u00e3o propensas a mudan\u00e7as entre ambientes diferentes (como desenvolvimento, teste e produ\u00e7\u00e3o). Mistur\u00e1-las com o c\u00f3digo pode tornar o processo de mudan\u00e7a entre esses ambientes complicado e propenso a erros.

Caso queira saber mais sobre 12 fatores

Temos uma live focada nesse assunto com a participa\u00e7\u00e3o especial do Bruno Rocha

Link direto

Al\u00e9m disso, expor credenciais de banco de dados e outras informa\u00e7\u00f5es sens\u00edveis no c\u00f3digo-fonte \u00e9 uma pr\u00e1tica de seguran\u00e7a ruim. Se esse c\u00f3digo fosse comprometido, essas informa\u00e7\u00f5es poderiam ser usadas para acessar e manipular seus recursos.

Por isso, usaremos o pydantic-settings para gerenciar nossas configura\u00e7\u00f5es de ambiente. A biblioteca permite que voc\u00ea defina configura\u00e7\u00f5es em arquivos separados ou vari\u00e1veis de ambiente e acesse-as de uma maneira estruturada e segura em seu c\u00f3digo.

Isso est\u00e1 alinhado com a metodologia dos 12 fatores, um conjunto de melhores pr\u00e1ticas para desenvolvimento de aplica\u00e7\u00f5es modernas. O terceiro fator, \"Config\", afirma que as configura\u00e7\u00f5es que variam entre os ambientes devem ser armazenadas no ambiente e n\u00e3o no c\u00f3digo.

Agora que entendemos melhor esses conceitos, come\u00e7aremos instalando as bibliotecas que iremos usar. O primeiro passo \u00e9 instalar o SQLAlchemy, um ORM que nos permite trabalhar com bancos de dados SQL de maneira Pythonica. Al\u00e9m disso, o Alembic, que \u00e9 uma ferramenta de migra\u00e7\u00e3o de banco de dados, funciona muito bem com o SQLAlchemy e nos ajudar\u00e1 a gerenciar as altera\u00e7\u00f5es do esquema do nosso banco de dados.

$ Execu\u00e7\u00e3o no terminal!
poetry add sqlalchemy\n

Al\u00e9m disso, para evitar a escrita de configura\u00e7\u00f5es do banco de dados diretamente no c\u00f3digo-fonte, usaremos o pydantic-settings. Este pacote nos permite gerenciar as configura\u00e7\u00f5es do nosso aplicativo de uma maneira mais segura e estruturada.

$ Execu\u00e7\u00e3o no terminal!
poetry add pydantic-settings\n

Agora estamos prontos para mergulhar na configura\u00e7\u00e3o do nosso banco de dados! Vamos em frente.

"},{"location":"04/#o-basico-sobre-sqlalchemy","title":"O b\u00e1sico sobre SQLAlchemy","text":"

SQLAlchemy \u00e9 uma biblioteca Python vers\u00e1til, concebida para intermediar a intera\u00e7\u00e3o entre Python e bancos de dados relacionais, como MySQL, PostgreSQL e SQLite. A biblioteca \u00e9 constitu\u00edda por duas partes principais: o Core e o ORM (Object Relational Mapper).

Caso nunca tenha trabalhado com SQLAlchemy

Temos uma live de Python cobrindo as mudan\u00e7as e o b\u00e1sico sobre o SQLAlchemy na vers\u00e3o 2+:

Al\u00e9m do Core e do ORM, o SQLAlchemy conta com outros componentes cruciais que ser\u00e3o foco desta aula, a Engine e a Session:

"},{"location":"04/#engine","title":"Engine","text":"

A 'Engine' do SQLAlchemy \u00e9 o ponto de contato com o banco de dados, estabelecendo e gerenciando as conex\u00f5es. Ela \u00e9 instanciada atrav\u00e9s da fun\u00e7\u00e3o create_engine(), que recebe as credenciais do banco de dados, o endere\u00e7o de conex\u00e3o (URI) e configura o pool de conex\u00f5es.

"},{"location":"04/#session","title":"Session","text":"

Quanto \u00e0 persist\u00eancia de dados e consultas ao banco de dados utilizando o ORM, a Session \u00e9 a principal interface. Ela atua como um intermedi\u00e1rio entre o aplicativo Python e o banco de dados, mediada pela Engine. A Session \u00e9 encarregada de todas as transa\u00e7\u00f5es, fornecendo uma API para conduzi-las.

Agora que conhecemos a Engine e a Session, vamos explorar a defini\u00e7\u00e3o de modelos de dados.

"},{"location":"04/#definindo-os-modelos-de-dados-com-sqlalchemy","title":"Definindo os Modelos de Dados com SQLAlchemy","text":"

Os modelos de dados definem a estrutura de como os dados ser\u00e3o armazenados no banco de dados. No ORM do SQLAlchemy, esses modelos s\u00e3o definidos como classes Python que podem ser herdados ou registradas (isso depende de como voc\u00ea usa o ORM). Vamos usar o registrador de tabelas, que j\u00e1 faz a convers\u00e3o autom\u00e1tica das classes em dataclasses

Cada classe que \u00e9 registrada pelo objeto registry \u00e9 automaticamente mapeada para uma tabela no banco de dados. Adicionalmente, a classe base inclui um objeto de metadados que \u00e9 uma cole\u00e7\u00e3o de todas as tabelas declaradas. Este objeto \u00e9 utilizado para gerenciar opera\u00e7\u00f5es como cria\u00e7\u00e3o, modifica\u00e7\u00e3o e exclus\u00e3o de tabelas.

Agora definiremos nosso modelo User. No diret\u00f3rio fast_zero, crie um novo arquivo chamado models.py e incluiremos o seguinte c\u00f3digo no arquivo:

fast_zero/models.py
from datetime import datetime\nfrom sqlalchemy.orm import Mapped, registry\n\ntable_registry = registry()\n\n\n@table_registry.mapped_as_dataclass\nclass User:\n    __tablename__ = 'users'\n\n    id: Mapped[int]\n    username: Mapped[str]\n    password: Mapped[str]\n    email: Mapped[str]\n    created_at: Mapped[datetime]\n

Aqui, Mapped refere-se a um atributo Python que \u00e9 associado (ou mapeado) a uma coluna espec\u00edfica em uma tabela de banco de dados. Por exemplo, Mapped[int] indica que este atributo \u00e9 um inteiro que ser\u00e1 mapeado para uma coluna correspondente em uma tabela de banco de dados. Da mesma forma, Mapped[str] se referiria a um atributo de string que seria mapeado para uma coluna de string correspondente. Esta abordagem permite ao SQLAlchemy realizar a convers\u00e3o entre os tipos de dados Python e os tipos de dados do banco de dados, al\u00e9m de oferecer uma interface Pythonica para a intera\u00e7\u00e3o entre eles.

Em especial, devemos nos atentar com o campo __tablename__. Ele \u00e9 referente ao nome que a tabela ter\u00e1 no banco de dados. Como geralmente um objeto no python representa somente uma entidade, usarei 'users' no plural para representar a tabela.

"},{"location":"04/#o-uso-do-modelo","title":"O uso do modelo","text":"

Se quisermos usar esse objeto, ela se comporta como uma dataclass tradicional. Podendo ser instanciada da forma tradicional:

C\u00f3digo de exemplo
eduardo = User(\n    id=1,\n    username='dunossauro',\n    password='senha123',\n    email='duno@ssauro.com',\n    created_at=datetime.now()\n)\n

Por padr\u00e3o, todos os atributos precisam ser especificados. O que pode n\u00e3o ser muito interessante, pois alguns dados devem ser preenchidos pelo banco de dados. Como o identificador da linha no banco ou a hora em que o registro foi criado.

Para isso, precisamos adicionar mais informa\u00e7\u00f5es ao modelo.

"},{"location":"04/#configuracoes-de-colunas","title":"Configura\u00e7\u00f5es de colunas","text":"

Quando definimos tabelas no banco de dados, as colunas podem apresentar propriedades espec\u00edficas. Por exemplo:

Para esses casos, o SQLAlchemy conta com a fun\u00e7\u00e3o mapped_column. Dentro dela, voc\u00ea pode definir diversas propriedades.

Para o nosso caso, gostaria que email e username n\u00e3o se repetissem na base de dados e que as colunas id e created_at tivessem o valor definido pelo pr\u00f3prio banco de dados, quando o registro fosse criado.

Para isso, vamos aplicar alguns par\u00e2metros nas colunas usando mapped_column:

fast_zero/models.py
from datetime import datetime\n\nfrom sqlalchemy import func\nfrom sqlalchemy.orm import Mapped, mapped_column, registry\n\ntable_registry = registry()\n\n\n@table_registry.mapped_as_dataclass\nclass User:\n    __tablename__ = 'users'\n\n    id: Mapped[int] = mapped_column(init=False, primary_key=True)#(1)!\n    username: Mapped[str] = mapped_column(unique=True)#(2)!\n    password: Mapped[str]\n    email: Mapped[str] = mapped_column(unique=True)\n    created_at: Mapped[datetime] = mapped_column(#(3)!\n        init=False, server_default=func.now()\n    )\n
  1. init=False diz que, quando o objeto for instanciado, esse par\u00e2metro n\u00e3o deve ser passado. primary_key=True diz que o campo id \u00e9 a chave prim\u00e1ria dessa tabela.
  2. unique=True diz que esse campo n\u00e3o deve se repetir na tabela. Por exemplo, se tivermos um username \"dunossauro\", n\u00e3o podemos ter outro com o mesmo valor.
  3. server_default=func.now() diz que, quando a classe for instanciada, o resultado de func.now() ser\u00e1 o valor atribu\u00eddo a esse atributo. No caso, a data e hora em que ele foi instanciado.

Desta forma, unimos tanto o uso que queremos ter no python, quanto a configura\u00e7\u00e3o esperada da tabela no banco de dados. Os par\u00e2metros de mapeamento dizem:

O campo init n\u00e3o tem uma rela\u00e7\u00e3o direta com o banco de dados, mas sim com a forma em que vamos usar o objeto do modelo no c\u00f3digo. Ele diz que os atributos marcados com init=false n\u00e3o devem ser passados no momento em que User for instanciado. Por exemplo:

C\u00f3digo de exemplo
eduardo = User(\n    username='dunossauro', password='senha123', email='duno@ssauro.com',\n)\n

Por n\u00e3o passarmos estes par\u00e2metros para User, o SQLAlchemy se encarregar\u00e1 de atribuir os valores a eles de forma autom\u00e1tica.

O campo created_at ser\u00e1 preenchido pelo resultado da fun\u00e7\u00e3o passada em server_default. O campo id, por contar com primary_key=True, ser\u00e1 autopreenchido com o id correspondente quando for armazenado no banco de dados.

Existem diversas op\u00e7\u00f5es nessa fun\u00e7\u00e3o. Caso queira ver mais possibilidades de mapeamento, aqui est\u00e1 a referencia para mais campos

"},{"location":"04/#testando-as-tabelas","title":"Testando as Tabelas","text":"

Antes de prosseguirmos, uma boa pr\u00e1tica seria criar um teste para validar se toda a estrutura do banco de dados funciona. Criaremos um arquivo para validar isso: test_db.py.

A partir daqui, voc\u00ea pode prosseguir com a estrutura\u00e7\u00e3o do conte\u00fado desse arquivo para definir os testes necess\u00e1rios para validar o seu modelo de usu\u00e1rio e sua intera\u00e7\u00e3o com o banco de dados.

"},{"location":"04/#antes-de-escrever-os-testes","title":"Antes de Escrever os Testes","text":"

A essa altura, se estiv\u00e9ssemos buscando apenas cobertura, poder\u00edamos simplesmente testar utilizando o modelo, e isso seria suficiente. No entanto, queremos verificar se toda a nossa intera\u00e7\u00e3o com o banco de dados ocorrer\u00e1 com sucesso. Isso inclui saber se os tipos de dados na tabela foram mapeados corretamente, se \u00e9 poss\u00edvel interagir com o banco de dados, se o ORM est\u00e1 estruturado adequadamente com a classe base. Precisamos garantir que todo esse esquema funcione.

graph\n  A[Aplicativo Python] -- utiliza --> B[SQLAlchemy ORM]\n  B -- fornece --> D[Session]\n  D -- interage com --> C[Modelos]\n  C -- mapeados para --> G[Tabelas no Banco de Dados]\n  D -- depende de --> E[Engine]\n  E -- conecta-se com --> F[Banco de Dados]\n  C -- associa-se a --> H[Metadata]\n  H -- mant\u00e9m informa\u00e7\u00f5es de --> G[Tabelas no Banco de Dados]

Neste diagrama, vemos a rela\u00e7\u00e3o completa entre o aplicativo Python e o banco de dados. A conex\u00e3o \u00e9 estabelecida atrav\u00e9s do SQLAlchemy ORM, que fornece uma Session para interagir com os Modelos. Esses modelos s\u00e3o mapeados para as tabelas no banco de dados, enquanto a Engine se conecta com o banco de dados e depende de Metadata para manter as informa\u00e7\u00f5es das tabelas.

Portanto, criaremos uma fixture para podermos usar todo esse esquema sempre que necess\u00e1rio.

"},{"location":"04/#criando-uma-fixture-para-interacoes-com-o-banco-de-dados","title":"Criando uma Fixture para intera\u00e7\u00f5es com o Banco de Dados","text":"

Para testar o banco, temos que fazer diversos passos, e isso pode tornar nosso teste bastante grande. Uma fixture pode ajudar a isolar toda essa configura\u00e7\u00e3o do banco de dados fora do teste. Assim, evitamos repetir o mesmo c\u00f3digo em todos os testes e ainda garantimos que cada teste tenha sua pr\u00f3pria vers\u00e3o limpa do banco de dados.

Criaremos uma fixture para a conex\u00e3o com o banco de dados chamada session:

tests/conftest.py
import pytest\nfrom fastapi.testclient import TestClient\nfrom sqlalchemy import create_engine\nfrom sqlalchemy.orm import Session\n\nfrom fast_zero.app import app\nfrom fast_zero.models import table_registry\n\n\n@pytest.fixture\ndef client():\n    return TestClient(app)\n\n\n@pytest.fixture\ndef session():\n    engine = create_engine('sqlite:///:memory:')#(1)!\n    table_registry.metadata.create_all(engine)#(2)!\n\n    with Session(engine) as session:#(3)!\n        yield session#(4)!\n\n    table_registry.metadata.drop_all(engine)#(5)!\n

Aqui, estamos utilizando o SQLite como o banco de dados em mem\u00f3ria para os testes. Essa \u00e9 uma pr\u00e1tica comum em testes unit\u00e1rios, pois a utiliza\u00e7\u00e3o de um banco de dados em mem\u00f3ria \u00e9 mais r\u00e1pida do que um banco de dados persistido em disco. Com o SQLite em mem\u00f3ria, podemos criar e destruir bancos de dados facilmente, o que \u00e9 \u00fatil para isolar os testes e garantir que os dados de um teste n\u00e3o afetem outros testes. Al\u00e9m disso, n\u00e3o precisamos nos preocupar com a limpeza dos dados ap\u00f3s a execu\u00e7\u00e3o dos testes, j\u00e1 que o banco de dados em mem\u00f3ria \u00e9 descartado quando o programa \u00e9 encerrado.

O que cada linha da fixture faz?

  1. create_engine('sqlite:///:memory:'): cria um mecanismo de banco de dados SQLite em mem\u00f3ria usando SQLAlchemy. Este mecanismo ser\u00e1 usado para criar uma sess\u00e3o de banco de dados para nossos testes.

  2. table_registry.metadata.create_all(engine): cria todas as tabelas no banco de dados de teste antes de cada teste que usa a fixture session.

  3. Session(engine): cria uma sess\u00e3o Session para que os testes possam se comunicar com o banco de dados. Por conta do yield a sess\u00e3o \u00e9 sempre renovada ap\u00f3s cada teste.

  4. yield Session(): fornece uma inst\u00e2ncia de Session que ser\u00e1 injetada em cada teste que solicita a fixture session. Essa sess\u00e3o ser\u00e1 usada para interagir com o banco de dados de teste.

  5. table_registry.metadata.drop_all(engine): ap\u00f3s cada teste que usa a fixture session, todas as tabelas do banco de dados de teste s\u00e3o eliminadas, garantindo que cada teste seja executado contra um banco de dados limpo.

Resumindo, essa fixture est\u00e1 configurando e limpando um banco de dados de teste para cada teste que o solicita, assegurando que cada teste seja isolado e tenha seu pr\u00f3prio ambiente limpo para trabalhar. Isso \u00e9 uma boa pr\u00e1tica em testes de unidade, j\u00e1 que queremos que cada teste seja independente e n\u00e3o afete os demais.

"},{"location":"04/#criando-um-teste-para-a-nossa-tabela","title":"Criando um Teste para a Nossa Tabela","text":"

Agora, no arquivo test_db.py, escreveremos um teste para a cria\u00e7\u00e3o de um usu\u00e1rio. Este teste adiciona um novo usu\u00e1rio ao banco de dados, faz commit das mudan\u00e7as, e depois verifica se o usu\u00e1rio foi devidamente criado consultando-o pelo nome de usu\u00e1rio. Se o usu\u00e1rio foi criado corretamente, o teste passa. Caso contr\u00e1rio, o teste falha, indicando que h\u00e1 algo errado com nossa fun\u00e7\u00e3o de cria\u00e7\u00e3o de usu\u00e1rio.

tests/test_db.py
from sqlalchemy import select\n\nfrom fast_zero.models import User\n\n\ndef test_create_user(session):\n    new_user = User(username='alice', password='secret', email='teste@test')\n    session.add(new_user)#(1)!\n    session.commit()#(2)!\n\n    user = session.scalar(select(User).where(User.username == 'alice'))#(3)!\n\n    assert user.username == 'alice'\n
  1. O m\u00e9todo .add da sess\u00e3o, adiciona o registro a sess\u00e3o. O dado fica em um estado transiente. Ele n\u00e3o foi adicionado ao banco de dados ainda. Mas j\u00e1 est\u00e1 reservado na sess\u00e3o. Ele \u00e9 uma aplica\u00e7\u00e3o do padr\u00e3o de projeto Unidade de trabalho.
  2. No momento em que existem dados transientes na sess\u00e3o e queremos \"performar\" efetivamente as a\u00e7\u00f5es no banco de dados. Usamos o m\u00e9todo .commit.
  3. O m\u00e9todo .scalar \u00e9 usado para performar buscas no banco (queries). Ele pega o primeiro resultado da busca e faz uma opera\u00e7\u00e3o de converter o resultado do banco de dados em um Objeto criado pelo SQLAlchemy, nesse caso, caso encontre um resultado, ele ir\u00e1 converter na classe User. A fun\u00e7\u00e3o de select \u00e9 uma fun\u00e7\u00e3o de busca de dados no banco. Nesse caso estamos procurando em todos os Users onde (where) o nome \u00e9 igual a \"alice\".
"},{"location":"04/#executando-o-teste","title":"Executando o teste","text":"

A execu\u00e7\u00e3o de testes \u00e9 uma parte vital do desenvolvimento de qualquer aplica\u00e7\u00e3o. Os testes nos ajudam a identificar e corrigir problemas antes que eles se tornem mais s\u00e9rios. Eles tamb\u00e9m fornecem a confian\u00e7a de que nossas mudan\u00e7as n\u00e3o quebraram nenhuma funcionalidade existente. No nosso caso, executaremos os testes para validar nossos modelos de usu\u00e1rio e garantir que eles estejam funcionando como esperado.

Para executar os testes, digite o seguinte comando:

$ Execu\u00e7\u00e3o no terminal!
task test\n
# ...\n\ntests/test_app.py::test_root_deve_retornar_ok_e_ola_mundo PASSED\ntests/test_app.py::test_create_user PASSED\ntests/test_app.py::test_read_users PASSED\ntests/test_app.py::test_update_user PASSED\ntests/test_app.py::test_delete_user PASSED\ntests/test_db.py::test_create_user PASSED\n\n---------- coverage: platform linux, python 3.11.3-final-0 -----------\nName                    Stmts   Miss  Cover\n-------------------------------------------\nfast_zero/__init__.py       0      0   100%\nfast_zero/app.py           28      2    93%\nfast_zero/models.py        11      0   100%\nfast_zero/schemas.py       15      0   100%\n-------------------------------------------\nTOTAL                      54      2    96%\n

Neste caso, podemos ver que todos os nossos testes passaram com sucesso. Isso significa que nossa funcionalidade de cria\u00e7\u00e3o de usu\u00e1rio est\u00e1 funcionando corretamente e que nosso modelo de usu\u00e1rio est\u00e1 sendo corretamente persistido no banco de dados.

Com nossos modelos e testes de banco de dados agora em ordem, estamos prontos para avan\u00e7ar para a pr\u00f3xima fase de configura\u00e7\u00e3o de nosso banco de dados e gerenciamento de migra\u00e7\u00f5es.

"},{"location":"04/#configuracao-do-ambiente-do-banco-de-dados","title":"Configura\u00e7\u00e3o do ambiente do banco de dados","text":"

Por fim, configuraremos nosso banco de dados. Primeiro, criaremos um novo arquivo chamado settings.py dentro do diret\u00f3rio fast_zero. Aqui, usaremos o Pydantic para criar uma classe Settings que ir\u00e1 pegar as configura\u00e7\u00f5es do nosso arquivo .env. Neste arquivo, a classe Settings \u00e9 definida como:

fast_zero/settings.py
from pydantic_settings import BaseSettings, SettingsConfigDict\n\n\nclass Settings(BaseSettings):\n    model_config = SettingsConfigDict( #(1)!\n        env_file='.env', env_file_encoding='utf-8'#(2)!\n    )\n\n    DATABASE_URL: str#(3)!\n
  1. SettingsConfigDict: \u00e9 um objeto do pydantic-settings que carrega as vari\u00e1veis em um arquivo de configura\u00e7\u00e3o. Por exemplo, um .env.
  2. Aqui definimos o caminho para o arquivo de configura\u00e7\u00e3o e o encoding dele.
  3. DATABASE_URL: Essa vari\u00e1vel sera preenchida com o valor encontrado com o mesmo nome no arquivo .env.

Agora, definiremos o DATABASE_URL no nosso arquivo de ambiente .env. Crie o arquivo na raiz do projeto e adicione a seguinte linha:

.env
DATABASE_URL=\"sqlite:///database.db\"\n

Com isso, quando a classe Settings for instanciada, ela ir\u00e1 automaticamente carregar as configura\u00e7\u00f5es do arquivo .env.

Finalmente, adicione o arquivo de banco de dados, database.db, ao .gitignore para garantir que n\u00e3o seja inclu\u00eddo no controle de vers\u00e3o. Adicionar informa\u00e7\u00f5es sens\u00edveis ou arquivos bin\u00e1rios ao controle de vers\u00e3o \u00e9 geralmente considerado uma pr\u00e1tica ruim.

$ Execu\u00e7\u00e3o no terminal!
echo 'database.db' >> .gitignore\n
"},{"location":"04/#instalando-o-alembic-e-criando-a-primeira-migracao","title":"Instalando o Alembic e Criando a Primeira Migra\u00e7\u00e3o","text":"

Antes de avan\u00e7armos, \u00e9 importante entender o que s\u00e3o migra\u00e7\u00f5es de banco de dados e por que s\u00e3o \u00fateis. As migra\u00e7\u00f5es s\u00e3o uma maneira de fazer altera\u00e7\u00f5es ou atualiza\u00e7\u00f5es no banco de dados, como adicionar uma tabela ou uma coluna a uma tabela, ou alterar o tipo de dados de uma coluna. Elas s\u00e3o extremamente \u00fateis, pois nos permitem manter o controle de todas as altera\u00e7\u00f5es feitas no esquema do banco de dados ao longo do tempo. Elas tamb\u00e9m nos permitem reverter para uma vers\u00e3o anterior do esquema do banco de dados, se necess\u00e1rio.

Caso nunca tenha trabalhado com Migra\u00e7\u00f5es

Temos uma live de Python focada nesse assunto em espec\u00edfico

Link direto

Agora, come\u00e7aremos instalando o Alembic, que \u00e9 uma ferramenta de migra\u00e7\u00e3o de banco de dados para SQLAlchemy. Usaremos o Poetry para adicionar o Alembic ao nosso projeto:

$ Execu\u00e7\u00e3o no terminal!
poetry add alembic\n

Ap\u00f3s a instala\u00e7\u00e3o do Alembic, precisamos inici\u00e1-lo em nosso projeto. O comando de inicializa\u00e7\u00e3o criar\u00e1 um diret\u00f3rio migrations e um arquivo de configura\u00e7\u00e3o alembic.ini:

$ Execu\u00e7\u00e3o no terminal!
alembic init migrations\n

Com isso, a estrutura do nosso projeto sofre algumas altera\u00e7\u00f5es e novos arquivos s\u00e3o criados:

.\n\u251c\u2500\u2500 .env\n\u251c\u2500\u2500 alembic.ini\n\u251c\u2500\u2500 fast_zero\n\u2502  \u251c\u2500\u2500 __init__.py\n\u2502  \u251c\u2500\u2500 app.py\n\u2502  \u251c\u2500\u2500 models.py\n\u2502  \u251c\u2500\u2500 schemas.py\n\u2502  \u2514\u2500\u2500 settings.py\n\u251c\u2500\u2500 migrations\n\u2502  \u251c\u2500\u2500 env.py\n\u2502  \u251c\u2500\u2500 README\n\u2502  \u251c\u2500\u2500 script.py.mako\n\u2502  \u2514\u2500\u2500 versions\n\u251c\u2500\u2500 poetry.lock\n\u251c\u2500\u2500 pyproject.toml\n\u251c\u2500\u2500 README.md\n\u2514\u2500\u2500 tests\n   \u251c\u2500\u2500 __init__.py\n   \u251c\u2500\u2500 conftest.py\n   \u251c\u2500\u2500 test_app.py\n   \u2514\u2500\u2500 test_db.py\n

No arquivo alembic.ini: ficam as configura\u00e7\u00f5es gerais das nossas migra\u00e7\u00f5es. Na pasta migrations foram criados um arquivo chamado env.py, esse arquivo \u00e9 respons\u00e1vel por como as migra\u00e7\u00f5es ser\u00e3o feitas, e o arquivo script.py.mako \u00e9 um template para as novas migra\u00e7\u00f5es.

"},{"location":"04/#criando-uma-migracao-automatica","title":"Criando uma migra\u00e7\u00e3o autom\u00e1tica","text":"

Com o Alembic devidamente instalado e iniciado, agora \u00e9 o momento de gerar nossa primeira migra\u00e7\u00e3o. Mas, antes disso, precisamos garantir que o Alembic consiga acessar nossas configura\u00e7\u00f5es e modelos corretamente. Para isso, faremos algumas altera\u00e7\u00f5es no arquivo migrations/env.py.

Neste arquivo, precisamos:

  1. Importar as Settings do nosso arquivo settings.py e a table_registry dos nossos modelos.
  2. Configurar a URL do SQLAlchemy para ser a mesma que definimos em Settings.
  3. Verificar a exist\u00eancia do arquivo de configura\u00e7\u00e3o do Alembic e, se presente, l\u00ea-lo.
  4. Definir os metadados de destino como table_registry.metadata, que \u00e9 o que o Alembic utilizar\u00e1 para gerar automaticamente as migra\u00e7\u00f5es.

O arquivo migrations/env.py modificado ficar\u00e1 assim:

migrations/env.py
from logging.config import fileConfig\n\nfrom sqlalchemy import engine_from_config\nfrom sqlalchemy import pool\n\nfrom alembic import context\n\nfrom fast_zero.models import table_registry\nfrom fast_zero.settings import Settings\n\nconfig = context.config\nconfig.set_main_option('sqlalchemy.url', Settings().DATABASE_URL)\n\nif config.config_file_name is not None:\n    fileConfig(config.config_file_name)\n\ntarget_metadata = table_registry.metadata\n\n# other values from the config, defined by the needs of env.py,\n# ...\n

Feitas essas altera\u00e7\u00f5es, estamos prontos para gerar nossa primeira migra\u00e7\u00e3o autom\u00e1tica. O Alembic \u00e9 capaz de gerar migra\u00e7\u00f5es a partir das mudan\u00e7as detectadas nos nossos modelos do SQLAlchemy.

Para criar a migra\u00e7\u00e3o, utilizamos o seguinte comando:

$ Execu\u00e7\u00e3o no terminal!
alembic revision --autogenerate -m \"create users table\"\n

Este comando instrui o Alembic a criar uma nova revis\u00e3o de migra\u00e7\u00e3o no diret\u00f3rio migrations/versions. A revis\u00e3o gerada conter\u00e1 os comandos SQL necess\u00e1rios para aplicar a migra\u00e7\u00e3o (criar a tabela de usu\u00e1rios) e para reverter essa migra\u00e7\u00e3o, caso seja necess\u00e1rio.

"},{"location":"04/#analisando-a-migracao-automatica","title":"Analisando a migra\u00e7\u00e3o autom\u00e1tica","text":"

Ao criar uma migra\u00e7\u00e3o autom\u00e1tica com o Alembic, um arquivo \u00e9 gerado dentro da pasta migrations/versions. O nome deste arquivo come\u00e7a com um ID de revis\u00e3o (um hash \u00fanico gerado pelo Alembic), seguido por uma breve descri\u00e7\u00e3o que fornecemos no momento da cria\u00e7\u00e3o da migra\u00e7\u00e3o, neste caso, create_users_table.

Vamos analisar o arquivo de migra\u00e7\u00e3o:

migrations/versions/e018397cecf4_create_users_table.py
\"\"\"create users table\n\nRevision ID: e018397cecf4\nRevises:\nCreate Date: 2023-07-13 03:43:03.730534\n\n\"\"\"\nfrom typing import Sequence, Union\n\nfrom alembic import op\nimport sqlalchemy as sa\n\n\n# revision identifiers, used by Alembic.\nrevision: str = 'e018397cecf4'\ndown_revision: Union[str, None] = None\nbranch_labels: Union[str, Sequence[str], None] = None\ndepends_on: Union[str, Sequence[str], None] = None\n\n\ndef upgrade() -> None:\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.create_table('users',\n    sa.Column('id', sa.Integer(), nullable=False),\n    sa.Column('username', sa.String(), nullable=False),\n    sa.Column('password', sa.String(), nullable=False),\n    sa.Column('email', sa.String(), nullable=False),\n    sa.Column('created_at', sa.DateTime(), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False),\n    sa.PrimaryKeyConstraint('id'),\n    sa.UniqueConstraint('email'),\n    sa.UniqueConstraint('username')\n    )\n    # ### end Alembic commands ###\n\n\ndef downgrade() -> None:\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.drop_table('users')\n    # ### end Alembic commands ###\n

Esse arquivo descreve as mudan\u00e7as a serem feitas no banco de dados. Ele usa a linguagem core do SQLAlchemy, que \u00e9 mais baixo n\u00edvel que o ORM. As fun\u00e7\u00f5es upgrade e downgrade definem, respectivamente, o que fazer para aplicar e para desfazer a migra\u00e7\u00e3o. No nosso caso, a fun\u00e7\u00e3o upgrade cria a tabela 'users' com os campos que definimos em fast_zero/models.pye a fun\u00e7\u00e3o downgrade a remove.

"},{"location":"04/#analisando-o-banco-de-dados","title":"Analisando o banco de dados","text":"

Ao criar a migra\u00e7\u00e3o, o Alembic teve que observar se j\u00e1 existiam migra\u00e7\u00f5es anteriores no banco de dados. Como o banco de dados n\u00e3o existia, ele criou um novo banco sqlite com o nome que definimos na vari\u00e1vel de ambiente DATABASE_URL. No caso database.db.

Se olharmos a estrutura de pastas, esse arquivo agora existe:

.\n\u251c\u2500\u2500 .env\n\u251c\u2500\u2500 alembic.ini\n\u251c\u2500\u2500 database.db\n\u251c\u2500\u2500 fast_zero\n\u2502  \u2514\u2500\u2500 ...\n\u251c\u2500\u2500 migrations\n\u2502  \u2514\u2500\u2500 ...\n\u251c\u2500\u2500 poetry.lock\n\u251c\u2500\u2500 pyproject.toml\n\u251c\u2500\u2500 README.md\n\u2514\u2500\u2500 tests\n   \u2514\u2500\u2500 ...\n

Pelo fato do sqlite3 ser um banco baseado em um \u00fanico arquivo, no momento das migra\u00e7\u00f5es, o sqlalchemy faz a cria\u00e7\u00e3o do arquivo de banco de dados caso ele n\u00e3o exista.

No momento da verifica\u00e7\u00e3o, caso n\u00e3o exista a tabela de migra\u00e7\u00f5es, ela ser\u00e1 criada. A tabela de migra\u00e7\u00f5es \u00e9 nomeada como alembic_version.

Vamos acessar o console do sqlite e verificar se isso foi feito. Precisamos chamar sqlite3 nome_do_arquivo.db:

$ Execu\u00e7\u00e3o no terminal!
sqlite3 database.db\n
Caso n\u00e3o tenha o SQLite instalado na sua m\u00e1quina: Arch
pacman -S sqlite\n
Debian/Ubuntu
sudo apt install sqlite3\n
Mac
brew install sqlite\n
Windows
winget install --id SQLite.SQLite\n

Quando executamos esse comando, o console do sqlite ser\u00e1 inicializado. E dentro dele podemos executar alguns comandos. Como fazer consultas, ver as tabelas criadas, adicionar dados, etc.

A cara do console \u00e9 essa:

SQLite version 3.45.1 2024-01-30 16:01:20\nEnter \".help\" for usage hints.\nsqlite>\n

Aqui voc\u00ea pode digitar comandos, da mesma forma em que fazemos no terminal interativo do python. O comando .schema nos mostra todas as tabelas criadas no banco de dados:

sqlite> .schema\nCREATE TABLE alembic_version (\n    version_num VARCHAR(32) NOT NULL,\n    CONSTRAINT alembic_version_pkc PRIMARY KEY (version_num)\n);\n

Nisso vemos que o Alembic criou uma tabela chamada alembic_version no banco de dados. Nessa tabela temos um \u00fanico campo chamado version_num que \u00e9 o campo que marca a vers\u00e3o atual da migra\u00e7\u00e3o no banco.

Para ver a vers\u00e3o atual do banco, podemos executar uma busca no campo e ver o resultado:

sqlite> select version_num from alembic_version;\n

O resultado deve ser vazio, pois n\u00e3o aplicamos nenhuma migra\u00e7\u00e3o, ele somente criou a tabela de migra\u00e7\u00f5es.

Para sair do console do sqlite temos que digitar o comando .quit:

sqlite> .quit\n

Agora que temos o terminal de volta, podemos aplicar as migra\u00e7\u00f5es.

"},{"location":"04/#aplicando-a-migracao","title":"Aplicando a migra\u00e7\u00e3o","text":"

Para aplicar as migra\u00e7\u00f5es, usamos o comando upgrade do CLI Alembic. O argumento head indica que queremos aplicar todas as migra\u00e7\u00f5es que ainda n\u00e3o foram aplicadas:

$ Execu\u00e7\u00e3o no terminal!
alembic upgrade head\n

Teremos a seguinte resposta:

INFO  [alembic.runtime.migration] Context impl SQLiteImpl.\nINFO  [alembic.runtime.migration] Will assume non-transactional DDL.\nINFO  [alembic.runtime.migration] Running upgrade  -> e018397cecf4, create users table\n

Vemos na \u00faltima linha executada a migra\u00e7\u00e3o de c\u00f3digo e018397cecf4, com o nome create users table.

Agora, se examinarmos nosso banco de dados novamente:

$ Execu\u00e7\u00e3o no terminal!
sqlite3 database.db\n

Podemos verificar se a tabela users foi criada no schema do banco:

sqlite> .schema\nCREATE TABLE alembic_version (\n    version_num VARCHAR(32) NOT NULL,\n    CONSTRAINT alembic_version_pkc PRIMARY KEY (version_num)\n);\nCREATE TABLE users (\n        id INTEGER NOT NULL,\n        username VARCHAR NOT NULL,\n        password VARCHAR NOT NULL,\n        email VARCHAR NOT NULL,\n        created_at DATETIME DEFAULT (CURRENT_TIMESTAMP) NOT NULL,\n        PRIMARY KEY (id),\n        UNIQUE (email),\n        UNIQUE (username)\n);\n

Se examinarmos os dados da tabela alembic_version podemos ver que o n\u00famero da migra\u00e7\u00e3o \u00e9 referente ao valor criado no arquivo de migra\u00e7\u00e3o e018397cecf4_create_users_table.py

sqlite> select version_num from alembic_version;\ne018397cecf4\nsqlite> .quit\n

Com isso, finalizamos a cria\u00e7\u00e3o do banco de dados. Lembre-se de que todas essas mudan\u00e7as que fizemos s\u00f3 existem localmente no seu ambiente de trabalho at\u00e9 agora. Para serem compartilhadas com outras pessoas, precisamos fazer commit dessas mudan\u00e7as no nosso sistema de controle de vers\u00e3o.

"},{"location":"04/#commit","title":"Commit","text":"

Primeiro, verificaremos o status do nosso reposit\u00f3rio para ver as mudan\u00e7as que fizemos:

$ Execu\u00e7\u00e3o no terminal!
git status\n

Voc\u00ea ver\u00e1 uma lista de arquivos modificados ou adicionados. As altera\u00e7\u00f5es devem incluir os arquivos de migra\u00e7\u00e3o que criamos, bem como quaisquer altera\u00e7\u00f5es que fizemos em nossos arquivos de modelo e configura\u00e7\u00e3o.

Em seguida, adicionaremos todas as mudan\u00e7as ao pr\u00f3ximo commit:

$ Execu\u00e7\u00e3o no terminal!
git add .\n

Agora, estamos prontos para fazer o commit das nossas altera\u00e7\u00f5es. Escreveremos uma mensagem de commit que descreve as mudan\u00e7as que fizemos:

$ Execu\u00e7\u00e3o no terminal!
git commit -m \"Adicionada a primeira migra\u00e7\u00e3o com Alembic. Criada tabela de usu\u00e1rios.\"\n

Finalmente, enviaremos as mudan\u00e7as para o reposit\u00f3rio remoto:

$ Execu\u00e7\u00e3o no terminal!
git push\n

E pronto! As mudan\u00e7as que fizemos foram salvas no hist\u00f3rico do Git e agora est\u00e3o dispon\u00edveis no GitHub.

"},{"location":"04/#exercicios","title":"Exerc\u00edcios","text":"
  1. Fazer uma altera\u00e7\u00e3o no modelo (tabela User) e adicionar um campo chamado updated_at:
    • Esse campo deve ser mapeado para o tipo datetime
    • Esse campo n\u00e3o deve ser inicializado por padr\u00e3o init=False
    • O valor padr\u00e3o deve ser now
    • Toda vez que a tabela for atualizada esse campo deve ser atualizado:
      mapped_column(onupdate=func.now())\n
  2. Criar uma nova migra\u00e7\u00e3o autogerada com alembic
  3. Aplicar essa migra\u00e7\u00e3o ao banco de dados

Exerc\u00edcios resolvidos

"},{"location":"04/#conclusao","title":"Conclus\u00e3o","text":"

Nesta aula, demos passos significativos para preparar nosso projeto FastAPI para interagir com um banco de dados. Come\u00e7amos definindo nosso primeiro modelo de dados, o User, utilizando o SQLAlchemy. Al\u00e9m disso, conforme as pr\u00e1ticas de Desenvolvimento Orientado por Testes (TDD), implementamos um teste para assegurar que a funcionalidade de cria\u00e7\u00e3o de um novo usu\u00e1rio no banco de dados esteja operando corretamente.

Avan\u00e7amos para configurar o ambiente de desenvolvimento, onde estabelecemos um arquivo .env para armazenar nossa DATABASE_URL e ajustamos o SQLAlchemy para utilizar essa URL. Complementarmente, inclu\u00edmos o arquivo do banco de dados ao .gitignore para evitar que seja rastreado pelo controle de vers\u00e3o.

Na \u00faltima parte desta aula, focamos na instala\u00e7\u00e3o e configura\u00e7\u00e3o do Alembic, uma ferramenta de migra\u00e7\u00e3o de banco de dados para SQLAlchemy. Usando o Alembic, criamos nossa primeira migra\u00e7\u00e3o que, automaticamente, gera o esquema do banco de dados a partir dos nossos modelos SQLAlchemy.

Com esses passos, nosso projeto est\u00e1 bem encaminhado para come\u00e7ar a persistir dados. Na pr\u00f3xima aula, avan\u00e7aremos para a fase crucial de conectar o SQLAlchemy aos endpoints do nosso projeto. Isso permitir\u00e1 a realiza\u00e7\u00e3o de opera\u00e7\u00f5es de CRUD nos nossos usu\u00e1rios diretamente atrav\u00e9s da API.

Agora que a aula acabou, \u00e9 um bom momento para voc\u00ea relembrar alguns conceitos e fixar melhor o conte\u00fado respondendo ao question\u00e1rio referente a ela.

Quiz

"},{"location":"05/","title":"Integrando Banco de Dados a API","text":""},{"location":"05/#integrando-banco-de-dados-a-api","title":"Integrando Banco de Dados a API","text":"

Objetivos dessa aula:

Caso prefira ver a aula em v\u00eddeo

Esse aula ainda n\u00e3o est\u00e1 dispon\u00edvel em formato de v\u00eddeo, somente em texto ou live!

Aula Slides C\u00f3digo Quiz Exerc\u00edcios

Ap\u00f3s termos estabelecido nossos modelos e migra\u00e7\u00f5es na aula anterior, \u00e9 hora de darmos um passo significativo: a integra\u00e7\u00e3o do banco de dados real com a nossa aplica\u00e7\u00e3o FastAPI. Deixaremos para tr\u00e1s o banco de dados simulado que utilizamos at\u00e9 ent\u00e3o e nos dedicaremos \u00e0 implementa\u00e7\u00e3o de um banco de dados real e plenamente operacional. Al\u00e9m disso, adaptaremos a estrutura dos nossos testes para que eles sejam compat\u00edveis com o banco de dados, incluindo a cria\u00e7\u00e3o de novas fixtures.

"},{"location":"05/#integrando-sqlalchemy-a-nossa-aplicacao-fastapi","title":"Integrando SQLAlchemy \u00e0 Nossa Aplica\u00e7\u00e3o FastAPI","text":"

Para aqueles que n\u00e3o est\u00e3o familiarizados, o SQLAlchemy \u00e9 uma biblioteca Python que facilita a intera\u00e7\u00e3o com um banco de dados SQL. Ele faz isso oferecendo uma forma de trabalhar com bancos de dados que aproveita a facilidade e o poder do Python, ao mesmo tempo, em que mant\u00e9m a efici\u00eancia e a flexibilidade dos bancos de dados SQL.

Caso nunca tenha trabalhado com SQLAlchemy

Recomendo fortemente que voc\u00ea assista a essa live de python onde os conceitos principais do sqlalchemy s\u00e3o expostos em uma discuss\u00e3o divertida.

Link direto

Uma pe\u00e7a chave do SQLAlchemy \u00e9 o conceito de uma \"sess\u00e3o\". Se voc\u00ea \u00e9 novo no mundo dos bancos de dados, pode pensar na sess\u00e3o como um carrinho de compras virtual: conforme voc\u00ea navega pelo site (ou, neste caso, conforme seu c\u00f3digo executa), voc\u00ea pode adicionar ou remover itens desse carrinho. No entanto, nenhuma altera\u00e7\u00e3o \u00e9 realmente feita at\u00e9 que voc\u00ea decida finalizar a compra. No contexto do SQLAlchemy, \"finalizar a compra\" \u00e9 equivalente a fazer o commit das suas altera\u00e7\u00f5es.

A sess\u00e3o no SQLAlchemy \u00e9 t\u00e3o poderosa que, na verdade, incorpora tr\u00eas padr\u00f5es de arquitetura importantes.

  1. Mapa de Identidade: Imagine que voc\u00ea esteja comprando frutas em uma loja online. Cada fruta que voc\u00ea adiciona ao seu carrinho recebe um c\u00f3digo de barras \u00fanico, para a loja saber exatamente qual fruta voc\u00ea quer. O Mapa de Identidade no SQLAlchemy \u00e9 esse sistema de c\u00f3digo de barras: ele garante que cada objeto na sess\u00e3o seja \u00fanico e facilmente identific\u00e1vel.

  2. Reposit\u00f3rio: A sess\u00e3o tamb\u00e9m atua como um reposit\u00f3rio. Isso significa que ela \u00e9 como um porteiro: ela controla todas as comunica\u00e7\u00f5es entre o seu c\u00f3digo Python e o banco de dados. Todos os comandos que voc\u00ea deseja enviar para o banco de dados devem passar pela sess\u00e3o.

  3. Unidade de Trabalho: Finalmente, a sess\u00e3o age como uma unidade de trabalho. Isso significa que ela mant\u00e9m o controle de todas as altera\u00e7\u00f5es que voc\u00ea quer fazer no banco de dados. Se voc\u00ea adicionar uma fruta ao seu carrinho e depois mudar de ideia e remover, a sess\u00e3o lembrar\u00e1 de ambas as a\u00e7\u00f5es. Ent\u00e3o, quando voc\u00ea finalmente decidir finalizar a compra, ela enviar\u00e1 todas as suas altera\u00e7\u00f5es para o banco de dados de uma s\u00f3 vez.

Entender esses conceitos \u00e9 importante, pois nos ajuda a entender melhor como o SQLAlchemy funciona e como podemos us\u00e1-lo de forma mais eficaz. Agora que temos uma ideia do que \u00e9 uma sess\u00e3o, configuraremos uma para nosso projeto.

Para isso, criaremos a fun\u00e7\u00e3o get_session e tamb\u00e9m definiremos Session no arquivo database.py:

fast_zero/database.py
from sqlalchemy import create_engine\nfrom sqlalchemy.orm import Session\n\nfrom fast_zero.settings import Settings\n\nengine = create_engine(Settings().DATABASE_URL)\n\n\ndef get_session():#(1)!\n    with Session(engine) as session:\n        yield session\n
  1. Quando os testes forem executados, essa linha nunca ser\u00e1 coberta. Pois os testes substitui\u00e7\u00e3o esse bloco por uma fixture em tempo de execu\u00e7\u00e3o aqui. Uma forma de fugir disso e explicar ao coverage que esse bloco n\u00e3o dever\u00e1 ser considerado na cobertura \u00e9 adicionar o coment\u00e1rio # pragma: no cover. Isso far\u00e1 ele ignorar esse bloco na contagem.
"},{"location":"05/#gerenciando-dependencias-com-fastapi","title":"Gerenciando Depend\u00eancias com FastAPI","text":"

Assim como a sess\u00e3o SQLAlchemy, que implementa v\u00e1rios padr\u00f5es arquiteturais importantes, FastAPI tamb\u00e9m usa um conceito de padr\u00e3o arquitetural chamado \"Inje\u00e7\u00e3o de Depend\u00eancia\".

No mundo do desenvolvimento de software, uma \"depend\u00eancia\" \u00e9 um componente que um m\u00f3dulo de software precisa para realizar sua fun\u00e7\u00e3o. Imagine um m\u00f3dulo como uma f\u00e1brica e as depend\u00eancias como as partes ou mat\u00e9rias-primas que a f\u00e1brica precisa para produzir seus produtos. Em vez de a f\u00e1brica ter que buscar essas pe\u00e7as por conta pr\u00f3pria (o que seria ineficiente), elas s\u00e3o entregues \u00e0 f\u00e1brica, prontas para serem usadas. Este \u00e9 o conceito de Inje\u00e7\u00e3o de Depend\u00eancia.

A Inje\u00e7\u00e3o de Depend\u00eancia permite que mantenhamos um baixo n\u00edvel de acoplamento entre diferentes m\u00f3dulos de um sistema. As depend\u00eancias entre os m\u00f3dulos n\u00e3o s\u00e3o definidas no c\u00f3digo, mas sim pela configura\u00e7\u00e3o de uma infraestrutura de software (container) respons\u00e1vel por \"injetar\" em cada componente suas depend\u00eancias declaradas.

Em termos pr\u00e1ticos, o que isso significa \u00e9 que, em vez de cada parte do nosso c\u00f3digo ter que criar suas pr\u00f3prias inst\u00e2ncias de classes ou servi\u00e7os de que depende (o que pode levar a duplica\u00e7\u00e3o de c\u00f3digo e tornar os testes mais dif\u00edceis), essas inst\u00e2ncias s\u00e3o criadas uma vez e depois injetadas onde s\u00e3o necess\u00e1rias.

FastAPI fornece a fun\u00e7\u00e3o Depends para ajudar a declarar e gerenciar essas depend\u00eancias. \u00c9 uma maneira declarativa de dizer ao FastAPI: \"Antes de executar esta fun\u00e7\u00e3o, execute primeiro essa outra fun\u00e7\u00e3o e passe-me o resultado\". Isso \u00e9 especialmente \u00fatil quando temos opera\u00e7\u00f5es que precisam ser realizadas antes de cada request, como abrir uma sess\u00e3o de banco de dados.

"},{"location":"05/#modificando-o-endpoint-post-users","title":"Modificando o Endpoint POST /users","text":"

Agora que temos a nossa sess\u00e3o de banco de dados gerenciada por meio do FastAPI e da inje\u00e7\u00e3o de depend\u00eancias, atualizaremos nossos endpoints para poderem tirar proveito disso. Come\u00e7aremos com a rota de POST para a cria\u00e7\u00e3o de usu\u00e1rios. Ao inv\u00e9s de usarmos o banco de dados falso que criamos inicialmente, agora faremos a inser\u00e7\u00e3o real dos usu\u00e1rios no nosso banco de dados.

Para isso, vamos modificar o nosso endpoint da seguinte maneira:

fast_zero/app.py
from http import HTTPStatus\n\nfrom fastapi import Depends, FastAPI, HTTPException\nfrom sqlalchemy import select\nfrom sqlalchemy.orm import Session\n\nfrom fast_zero.database import get_session\nfrom fast_zero.models import User\nfrom fast_zero.schemas import Message, UserDB, UserList, UserPublic, UserSchema\n\n# ...\n\n\n@app.post('/users/', status_code=HTTPStatus.CREATED, response_model=UserPublic)\ndef create_user(user: UserSchema, session: Session = Depends(get_session)):#(1)!\n    db_user = session.scalar(\n        select(User).where(\n            (User.username == user.username) | (User.email == user.email)#(2)!\n        )\n    )\n\n    if db_user:\n        if db_user.username == user.username:#(3)!\n            raise HTTPException(\n                status_code=HTTPStatus.BAD_REQUEST,\n                detail='Username already exists',\n            )\n        elif db_user.email == user.email:\n            raise HTTPException(\n                status_code=HTTPStatus.BAD_REQUEST,\n                detail='Email already exists',\n            )\n\n    db_user = User(\n        username=user.username, password=user.password, email=user.email\n    )\n    session.add(db_user)\n    session.commit()\n    session.refresh(db_user)\n\n    return db_user\n
  1. session: Session = Depends(get_session) diz que a fun\u00e7\u00e3o get_session ser\u00e1 executada antes da execu\u00e7\u00e3o da fun\u00e7\u00e3o e o valor retornado por get_session ser\u00e1 atribu\u00eddo ao par\u00e2metro session.
  2. Faz uma busca por User onde (where) o username \u00e9 igual ao que veio no request, ou (|) o email \u00e9 igual ao que veio no request. A busca tem o objetivo de achar um registro que tenha ou o email ou o username cadastrado. Pois, eles s\u00e3o \u00fanicos na base de dados. Precisamos validar para ver se j\u00e1 n\u00e3o constam na base de dados.
  3. Por conta do ou (|), precisamos validar o que \u00e9 que j\u00e1 existe na base. Se \u00e9 o username ou se \u00e9 o email

Vamos analisar esse c\u00f3digo com um pouco de calma, diversas coisas est\u00e3o acontecendo aqui, ent\u00e3o, vamos ter um pouco mais de cuidado em um \"bloco a bloco\":

Ao final disso, temos uma integra\u00e7\u00e3o entre o m\u00e9todo POST da API e uma inser\u00e7\u00e3o no banco de dados.

"},{"location":"05/#testando-o-endpoint-post-users-com-pytest-e-fixtures","title":"Testando o Endpoint POST /users com Pytest e Fixtures","text":"

Agora que nossa rota de POST est\u00e1 funcionando com o banco de dados real, precisamos atualizar nossos testes para refletir essa mudan\u00e7a. Como estamos usando a inje\u00e7\u00e3o de depend\u00eancias, precisamos tamb\u00e9m usar essa funcionalidade nos nossos testes para podermos injetar a sess\u00e3o de banco de dados de teste.

Alteraremos a nossa fixture client para substituir a fun\u00e7\u00e3o get_session que estamos injetando no endpoint pela sess\u00e3o do banco em mem\u00f3ria que j\u00e1 t\u00ednhamos definido para banco de dados.

tests/conftest.py
import pytest\nfrom fastapi.testclient import TestClient\nfrom sqlalchemy import create_engine\nfrom sqlalchemy.orm import Session\n\nfrom fast_zero.app import app\nfrom fast_zero.database import get_session\nfrom fast_zero.models import table_registry\n\n\n@pytest.fixture\ndef client(session):\n    def get_session_override():#(1)!\n        return session\n\n    with TestClient(app) as client:\n        app.dependency_overrides[get_session] = get_session_override#(2)!\n        yield client\n\n    app.dependency_overrides.clear()#(3)!\n\n# ...\n
  1. Criamos uma fun\u00e7\u00e3o para retornar nossa fixture de session definida anteriormente.
  2. Substitui a fun\u00e7\u00e3o get_session que usamos para a aplica\u00e7\u00e3o real, pela nossa fun\u00e7\u00e3o que retorna a fixture de testes.
  3. Limpa a sobrescrita que fizemos no app para usar a fixture de session.

Com isso, quando o FastAPI tentar injetar a sess\u00e3o em nossos endpoints, ele injetar\u00e1 a sess\u00e3o de teste que definimos, em vez da sess\u00e3o real. E como estamos usando um banco de dados em mem\u00f3ria para os testes, nossos testes n\u00e3o v\u00e3o interferir nos dados reais do nosso aplicativo.

tests/test_app.py
def test_create_user(client):\n    response = client.post(\n        '/users',\n        json={\n            'username': 'alice',\n            'email': 'alice@example.com',\n            'password': 'secret',\n        },\n    )\n    assert response.status_code == HTTPStatus.CREATED\n    assert response.json() == {\n        'username': 'alice',\n        'email': 'alice@example.com',\n        'id': 1,\n    }\n

Agora que temos a nossa fixture configurada, atualizaremos o nosso teste test_create_user para usar o novo cliente de teste e verificar que o usu\u00e1rio est\u00e1 sendo realmente criado no banco de dados.

$ Execu\u00e7\u00e3o no terminal!
task test\n\n# ...\n\ntests/test_app.py::test_root_deve_retornar_ok_e_ola_mundo PASSED\ntests/test_app.py::test_create_user FAILED\n

O nosso teste ainda n\u00e3o consegue ser executado, mas existe um motivo para isso.

"},{"location":"05/#threads-e-conexoes","title":"Threads e conex\u00f5es","text":"

No ambiente de testes do FastAPI, a aplica\u00e7\u00e3o e os testes podem rodar em threads diferentes. Isso pode levar a um erro com o SQLite, pois os objetos SQLite criados em uma thread s\u00f3 podem ser usados na mesma thread.

Para contornar isso, adicionaremos os seguintes par\u00e2metros na cria\u00e7\u00e3o da engine:

  1. connect_args={'check_same_thread': False}: essa configura\u00e7\u00e3o desativa a verifica\u00e7\u00e3o de que o objeto SQLite est\u00e1 sendo usado na mesma thread em que foi criado. Isso permite que a conex\u00e3o seja compartilhada entre threads diferentes sem levar a erros.

  2. poolclass=StaticPool: esse par\u00e2metro faz com que a engine use um pool de conex\u00f5es est\u00e1tico, ou seja, reutilize a mesma conex\u00e3o para todas as solicita\u00e7\u00f5es. Isso garante que as duas threads usem o mesmo canal de comunica\u00e7\u00e3o, evitando erros relacionados ao uso de diferentes conex\u00f5es em threads diferentes.

Assim, nossa fixture deve ficar dessa forma:

tests/conftest.py
from sqlalchemy.pool import StaticPool\n\n# ...\n\n@pytest.fixture\ndef session():\n    engine = create_engine(\n        'sqlite:///:memory:',\n        connect_args={'check_same_thread': False},\n        poolclass=StaticPool,\n    )\n    table_registry.metadata.create_all(engine)\n\n    with Session(engine) as session:\n        yield session\n\n    table_registry.metadata.drop_all(engine)\n

Depois de realizar essas mudan\u00e7as, podemos executar nossos testes e verificar se est\u00e3o passando. Por\u00e9m, embora o teste test_create_user tenha passado, precisamos agora ajustar os outros endpoints para que eles tamb\u00e9m utilizem a nossa sess\u00e3o de banco de dados.

$ Execu\u00e7\u00e3o no terminal!
task test\n\n# ...\n\ntests/test_app.py::test_create_user PASSED\ntests/test_app.py::test_read_users FAILED\ntests/test_app.py::test_update_user FAILED\n\n# ...\n

Nos pr\u00f3ximos passos, vamos realizar essas modifica\u00e7\u00f5es para garantir que todo o nosso aplicativo esteja usando o banco de dados real.

"},{"location":"05/#modificando-o-endpoint-get-users","title":"Modificando o Endpoint GET /users","text":"

Agora que temos o nosso banco de dados configurado e funcionando, \u00e9 o momento de atualizar o nosso endpoint de GET para interagir com o banco de dados real. Em vez de trabalhar com uma lista fict\u00edcia de usu\u00e1rios, queremos buscar os usu\u00e1rios diretamente do nosso banco de dados, permitindo uma intera\u00e7\u00e3o din\u00e2mica e real com os dados.

fast_zero/app.py
@app.get('/users/', response_model=UserList)\ndef read_users(\n    skip: int = 0, limit: int = 100, session: Session = Depends(get_session)\n):\n    users = session.scalars(select(User).offset(skip).limit(limit)).all()\n    return {'users': users}\n

Neste c\u00f3digo, adicionamos algumas funcionalidades essenciais para a busca de dados. Os par\u00e2metros offset e limit s\u00e3o utilizados para paginar os resultados, o que \u00e9 especialmente \u00fatil quando se tem um grande volume de dados.

Os par\u00e2metros sendo passados na fun\u00e7\u00e3o, como skip e limit, se tornam par\u00e2metros de query. Se olharmos no swagger. Podemos ver que isso agora \u00e9 parametriz\u00e1vel durante a chamada:

Isso faz com que as chamadas para o endpoint possa ser realizadas desta forma: http://localhost:8000/users/?skip=0&limit=100. Passando skip=0 ou limit=100. Esses valores podem mudar a quantidade e os registros que ser\u00e3o retornados pelo banco de dados. Por padr\u00e3o ser\u00e3o retornados 100 registros iniciando no 0.

Recomendo que voc\u00ea insira diversos valores no banco de dados e teste a varia\u00e7\u00e3o dos par\u00e2metros. Pode ser bastante divertido.

Essas adi\u00e7\u00f5es tornam o nosso endpoint mais flex\u00edvel e otimizado para lidar com diferentes cen\u00e1rios de uso.

"},{"location":"05/#testando-o-endpoint-get-users","title":"Testando o Endpoint GET /users","text":"

Com a mudan\u00e7a para o banco de dados real, nosso banco de dados de teste ser\u00e1 sempre resetado para cada teste. Portanto, n\u00e3o podemos mais executar o teste que t\u00ednhamos antes, pois n\u00e3o haver\u00e1 usu\u00e1rios no banco. Para verificar se o nosso endpoint est\u00e1 funcionando corretamente, criaremos um novo teste que solicita uma lista de usu\u00e1rios de um banco vazio:

tests/test_app.py
def test_read_users(client):\n    response = client.get('/users')\n    assert response.status_code == HTTPStatus.OK\n    assert response.json() == {'users': []}\n

Agora que temos nosso novo teste, podemos execut\u00e1-lo para verificar se o nosso endpoint GET est\u00e1 funcionando corretamente. Com esse novo teste, a fun\u00e7\u00e3o test_read_users deve passar.

$ Execu\u00e7\u00e3o no terminal!
task test\n\n# ...\n\ntests/test_app.py::test_create_user PASSED\ntests/test_app.py::test_read_users PASSED\ntests/test_app.py::test_update_user FAILED\n

Por\u00e9m, \u00e9 claro, queremos tamb\u00e9m testar o caso em que existem usu\u00e1rios no banco. Para isso, criaremos uma nova fixture que cria um usu\u00e1rio em nosso banco de dados de teste.

"},{"location":"05/#criando-uma-fixture-para-user","title":"Criando uma fixture para User","text":"

Para criar essa fixture, aproveitaremos a nossa fixture de sess\u00e3o do SQLAlchemy, e criar um novo usu\u00e1rio dentro dela:

tests/conftest.py
from fast_zero.models import User, table_registry\n\n# ...\n\n@pytest.fixture\ndef user(session):\n    user = User(username='Teste', email='teste@test.com', password='testtest')\n    session.add(user)\n    session.commit()\n    session.refresh(user)\n\n    return user\n

Com essa fixture, sempre que precisarmos de um usu\u00e1rio em nossos testes, podemos simplesmente passar user como um argumento para nossos testes, e o Pytest se encarregar\u00e1 de criar um novo usu\u00e1rio para n\u00f3s.

Agora podemos criar um novo teste para verificar se o nosso endpoint est\u00e1 retornando o usu\u00e1rio correto quando existe um usu\u00e1rio no banco:

tests/test_app.py
from fast_zero.schemas import UserPublic\n\n# ...\n\n\ndef test_read_users_with_users(client, user):\n    user_schema = UserPublic.model_validate(user).model_dump()\n    response = client.get('/users/')\n    assert response.json() == {'users': [user_schema]}\n

Agora podemos rodar o nosso teste novamente e verificar se ele est\u00e1 passando:

$ Execu\u00e7\u00e3o no terminal!
task test\n\n# ...\n\ntests/test_app.py::test_create_user PASSED\ntests/test_app.py::test_read_users PASSED\ntests/test_app.py::test_read_users_with_users FAILED\n

No entanto, mesmo que nosso c\u00f3digo pare\u00e7a correto, podemos encontrar um problema: o Pydantic n\u00e3o consegue converter diretamente nosso modelo SQLAlchemy para um modelo Pydantic. Resolveremos isso agora.

"},{"location":"05/#integrando-o-schema-ao-model","title":"Integrando o Schema ao Model","text":"

A integra\u00e7\u00e3o direta do ORM com o nosso esquema Pydantic n\u00e3o \u00e9 imediata e exige algumas modifica\u00e7\u00f5es. O Pydantic, por padr\u00e3o, n\u00e3o sabe como lidar com os modelos do SQLAlchemy, o que nos leva ao erro observado nos testes.

A solu\u00e7\u00e3o para esse problema passa por fazer uma altera\u00e7\u00e3o no esquema UserPublic que utilizamos, para que ele possa reconhecer e trabalhar com os modelos do SQLAlchemy. Isso permite que os objetos do SQLAlchemy sejam convertidos corretamente para os esquemas Pydantic.

Para resolver o problema de convers\u00e3o entre SQLAlchemy e Pydantic, precisamos atualizar o nosso esquema UserPublic para que ele possa reconhecer os modelos do SQLAlchemy. Para isso, adicionaremos a linha model_config = ConfigDict(from_attributes=True) ao nosso esquema:

fast_zero/schemas.py
from pydantic import BaseModel, ConfigDict, EmailStr\n\n# ...\n\nclass UserPublic(BaseModel):\n    id: int\n    username: str\n    email: EmailStr\n    model_config = ConfigDict(from_attributes=True)\n

Com essa mudan\u00e7a, nosso esquema Pydantic agora pode ser convertido a partir de um modelo SQLAlchemy. Agora podemos executar nosso teste novamente e verificar se ele est\u00e1 passando.

$ Execu\u00e7\u00e3o no terminal!
task test\n\n# ...\n\ntests/test_app.py::test_root_deve_retornar_ok_e_ola_mundo PASSED\ntests/test_app.py::test_create_user PASSED\ntests/test_app.py::test_read_users PASSED\ntests/test_app.py::test_read_users_with_users PASSED\ntests/test_app.py::test_update_user FAILED\n

Agora que temos nosso endpoint GET funcionando corretamente e testado, podemos seguir para o endpoint PUT, e continuar com o processo de atualiza\u00e7\u00e3o dos nossos endpoints.

"},{"location":"05/#modificando-o-endpoint-put-users","title":"Modificando o Endpoint PUT /users","text":"

Agora, modificaremos o endpoint de PUT para suportar o banco de dados, como fizemos com os endpoints POST e GET:

fast_zero/app.py
@app.put('/users/{user_id}', response_model=UserPublic)\ndef update_user(\n    user_id: int, user: UserSchema, session: Session = Depends(get_session)\n):\n\n    db_user = session.scalar(select(User).where(User.id == user_id))\n    if not db_user:\n        raise HTTPException(\n            status_code=HTTPStatus.NOT_FOUND, detail='User not found'\n        )\n\n    db_user.username = user.username\n    db_user.password = user.password\n    db_user.email = user.email\n    session.commit()\n    session.refresh(db_user)\n\n    return db_user\n

Semelhante ao que fizemos antes, estamos injetando a sess\u00e3o do SQLAlchemy em nosso endpoint e utilizando-a para buscar o usu\u00e1rio a ser atualizado. Se o usu\u00e1rio n\u00e3o for encontrado, retornamos um erro 404.

As linhas destacadas mostram que estamos atribuindo novos valores aos atributos username, password e email. Essa opera\u00e7\u00e3o, seguida por um commit, altera os valores existentes no banco. \u00c9 como se a atribui\u00e7\u00e3o nova, fosse uma atualiza\u00e7\u00e3o do dado da tabela.

Antes de partirmos para o pr\u00f3ximo passo, ao executar nosso linter, ele ir\u00e1 apontar um erro informando que importamos UserDB mas, nunca o usamos.

$ Execu\u00e7\u00e3o no terminal!
task lint\nfast_zero/app.py:9:40: F401 [*] `fast_zero.schemas.UserDB` imported but unused\nFound 1 error.\n[*] 1 fixable with the `--fix` option.\n--- fast_zero/app.py\n+++ fast_zero/app.py\n@@ -6,7 +6,7 @@\n\n from fast_zero.database import get_session\n from fast_zero.models import User\n-from fast_zero.schemas import Message, UserDB, UserList, UserPublic, UserSchema\n+from fast_zero.schemas import Message, UserList, UserPublic, UserSchema\n\n app = FastAPI()\n\n\nWould fix 1 error.\n

Isso ocorre porque a rota PUT era a \u00fanica que estava utilizando UserDB, e agora que modificamos esta rota, podemos remover UserDB dos nossos imports e tamb\u00e9m excluir sua defini\u00e7\u00e3o no arquivo schemas.py

Sobre o arquivo schemas.py

Caso fique em d\u00favida sobre o que remover, seu arquivo schemas.py deve estar parecido com isso, ap\u00f3s a remo\u00e7\u00e3o de UserDB:

schemas.py
from pydantic import BaseModel, ConfigDict, EmailStr\n\n\nclass Message(BaseModel):\n    message: str\n\nclass UserSchema(BaseModel):\n    username: str\n    email: EmailStr\n    password: str\n\n\nclass UserPublic(BaseModel):\n    id: int\n    username: str\n    email: EmailStr\n    model_config = ConfigDict(from_attributes=True)\n\n\nclass UserList(BaseModel):\n    users: list[UserPublic]\n
"},{"location":"05/#adicionando-o-teste-do-put","title":"Adicionando o teste do PUT","text":"

Tamb\u00e9m precisamos alterar o teste para o endpoint de PUT, para que exista um usu\u00e1rio na base para ser alterado:

tests/test_app.py
def test_update_user(client, user):\n    response = client.put(\n        '/users/1',\n        json={\n            'username': 'bob',\n            'email': 'bob@example.com',\n            'password': 'mynewpassword',\n        },\n    )\n    assert response.status_code == HTTPStatus.OK\n    assert response.json() == {\n        'username': 'bob',\n        'email': 'bob@example.com',\n        'id': 1,\n    }\n
"},{"location":"05/#modificando-o-endpoint-delete-users","title":"Modificando o Endpoint DELETE /users","text":"

Em seguida, modificamos o endpoint DELETE da mesma maneira:

fast_zero/app.py
@app.delete('/users/{user_id}', response_model=Message)\ndef delete_user(user_id: int, session: Session = Depends(get_session)):\n    db_user = session.scalar(select(User).where(User.id == user_id))\n\n    if not db_user:\n        raise HTTPException(\n            status_code=HTTPStatus.NOT_FOUND, detail='User not found'\n        )\n\n    session.delete(db_user)#(1)!\n    session.commit()\n\n    return {'message': 'User deleted'}\n
  1. O m\u00e9todo delete da session adiciona uma opera\u00e7\u00e3o de dele\u00e7\u00e3o na sess\u00e3o. Na sequ\u00eancia o m\u00e9todo commit tem que ser chamado para que a opera\u00e7\u00e3o seja performada.

Neste caso, estamos novamente usando a sess\u00e3o do SQLAlchemy para encontrar o usu\u00e1rio a ser deletado e, em seguida, exclu\u00edmos esse usu\u00e1rio do banco de dados.

"},{"location":"05/#adicionando-testes-para-delete","title":"Adicionando testes para DELETE","text":"

Assim como para o endpoint PUT, precisamos alterar o teste para o nosso endpoint DELETE, pois n\u00e3o existe um user na base:

tests/test_app.py
def test_delete_user(client, user):\n    response = client.delete('/users/1')\n    assert response.status_code == HTTPStatus.OK\n    assert response.json() == {'message': 'User deleted'}\n
"},{"location":"05/#cobertura-e-testes-nao-feitos","title":"Cobertura e testes n\u00e3o feitos","text":"

Com o banco de dados agora em funcionamento, podemos verificar a cobertura de c\u00f3digo do arquivo fast_zero/app.py. Se olharmos para a imagem abaixo, vemos que ainda h\u00e1 alguns casos que n\u00e3o testamos. Por exemplo, o que acontece quando tentamos atualizar ou excluir um usu\u00e1rio que n\u00e3o existe?

Esses tr\u00eas casos ficam como exerc\u00edcios para quem est\u00e1 acompanhando este curso.

Al\u00e9m disso, n\u00e3o devemos esquecer de remover a implementa\u00e7\u00e3o do banco de dados falso database = [] que usamos inicialmente e remover tamb\u00e9m as defini\u00e7\u00f5es de TestClient em test_app.py, pois tudo est\u00e1 usando as fixtures agora!

"},{"location":"05/#commit","title":"Commit","text":"

Agora que terminamos a atualiza\u00e7\u00e3o dos nossos endpoints, faremos o commit das nossas altera\u00e7\u00f5es. O processo \u00e9 o seguinte:

$ Execu\u00e7\u00e3o no terminal!
git add .\ngit commit -m \"Atualizando endpoints para usar o banco de dados real\"\ngit push\n

Com isso, terminamos a atualiza\u00e7\u00e3o dos nossos endpoints para usar o nosso banco de dados real.

"},{"location":"05/#exercicios","title":"Exerc\u00edcios","text":"
  1. Escrever um teste para o endpoint de POST (create_user) que contemple o cen\u00e1rio onde o username j\u00e1 foi registrado. Validando o erro 400;
  2. Escrever um teste para o endpoint de POST (create_user) que contemple o cen\u00e1rio onde o e-mail j\u00e1 foi registrado. Validando o erro 400;
  3. Atualizar os testes criados nos exerc\u00edcios 1 e 2 da aula 03 para suportarem o banco de dados;
  4. Implementar o banco de dados para o endpoint de listagem por id, criado no exerc\u00edcio 3 da aula 03.

Exerc\u00edcios resolvidos

"},{"location":"05/#conclusao","title":"Conclus\u00e3o","text":"

Parab\u00e9ns por chegar ao final desta aula! Voc\u00ea deu um passo significativo no desenvolvimento de nossa aplica\u00e7\u00e3o, substituindo a implementa\u00e7\u00e3o do banco de dados falso pela integra\u00e7\u00e3o com um banco de dados real usando SQLAlchemy. Tamb\u00e9m vimos como ajustar os nossos testes para considerar essa nova realidade.

Nesta aula, abordamos como modificar os endpoints para interagir com o banco de dados real e como utilizar a inje\u00e7\u00e3o de depend\u00eancias do FastAPI para gerenciar nossas sess\u00f5es do SQLAlchemy. Tamb\u00e9m discutimos a import\u00e2ncia dos testes para garantir que nossos endpoints est\u00e3o funcionando corretamente, e como as fixtures do Pytest podem nos auxiliar na prepara\u00e7\u00e3o do ambiente para esses testes.

Al\u00e9m disso, nos deparamos com situa\u00e7\u00f5es onde o Pydantic e o SQLAlchemy n\u00e3o interagem perfeitamente bem, e como solucionar esses casos.

No final desta aula, voc\u00ea deve estar confort\u00e1vel em integrar um banco de dados real a uma aplica\u00e7\u00e3o FastAPI, saber como escrever testes robustos que levem em considera\u00e7\u00e3o a intera\u00e7\u00e3o com o banco de dados, e estar ciente de poss\u00edveis desafios ao trabalhar com Pydantic e SQLAlchemy juntos.

Agora que a aula acabou, \u00e9 um bom momento para voc\u00ea relembrar alguns conceitos e fixar melhor o conte\u00fado respondendo ao question\u00e1rio referente a ela.

Quiz

"},{"location":"06/","title":"Autentica\u00e7\u00e3o e Autoriza\u00e7\u00e3o com JWT","text":""},{"location":"06/#autenticacao-e-autorizacao-com-jwt","title":"Autentica\u00e7\u00e3o e Autoriza\u00e7\u00e3o com JWT","text":"

Objetivos da Aula:

Caso prefira ver a aula em v\u00eddeo

Esse aula ainda n\u00e3o est\u00e1 dispon\u00edvel em formato de v\u00eddeo, somente em texto ou live!

Aula Slides C\u00f3digo Quiz Exerc\u00edcios

"},{"location":"06/#introducao","title":"Introdu\u00e7\u00e3o","text":"

Nesta aula, abordaremos dois aspectos cruciais de qualquer aplica\u00e7\u00e3o web: a autentica\u00e7\u00e3o e a autoriza\u00e7\u00e3o. At\u00e9 agora, nossos usu\u00e1rios podem criar, ler, atualizar e deletar suas contas, mas qualquer pessoa pode fazer essas a\u00e7\u00f5es. N\u00e3o queremos que qualquer usu\u00e1rio possa deletar ou modificar a conta de outro usu\u00e1rio. Para evitar isso, vamos implementar autentica\u00e7\u00e3o e autoriza\u00e7\u00e3o em nossa aplica\u00e7\u00e3o.

A autentica\u00e7\u00e3o \u00e9 o processo de verificar quem um usu\u00e1rio \u00e9, enquanto a autoriza\u00e7\u00e3o \u00e9 o processo de verificar o que ele tem permiss\u00e3o para fazer. Usaremos o JSON Web Token (JWT) para implementar a autentica\u00e7\u00e3o, e adicionaremos l\u00f3gica de autoriza\u00e7\u00e3o aos nossos endpoints.

Al\u00e9m disso, at\u00e9 agora, estamos armazenando as senhas dos usu\u00e1rios como texto puro no banco de dados, o que \u00e9 uma pr\u00e1tica insegura. Corrigiremos isso utilizando a biblioteca pwdlib para encriptar as senhas.

"},{"location":"06/#o-que-e-um-jwt","title":"O que \u00e9 um JWT","text":"

O JWT \u00e9 um padr\u00e3o (RFC 7519) que define uma maneira compacta e aut\u00f4noma de transmitir informa\u00e7\u00f5es entre as partes de maneira segura. Essas informa\u00e7\u00f5es s\u00e3o transmitidas como um objeto JSON que \u00e9 digitalmente assinado usando um segredo (com o algoritmo HMAC) ou um par de chaves p\u00fablica/privada usando RSA, ou ECDSA.

Um JWT consiste em tr\u00eas partes:

  1. Header: O cabe\u00e7alho do JWT consiste tipicamente em dois componentes: o tipo de token, que \u00e9 JWT neste caso, e o algoritmo de assinatura, como HMAC SHA256 ou RSA. Essas informa\u00e7\u00f5es s\u00e3o codificadas em Base64Url e formam a primeira parte do JWT.

    {\n   \"alg\": \"HS256\",\n   \"typ\": \"JWT\"\n}\n
  2. Payload: O payload de um JWT \u00e9 onde as reivindica\u00e7\u00f5es (em ingl\u00eas claims) s\u00e3o armazenadas. As reivindica\u00e7\u00f5es s\u00e3o informa\u00e7\u00f5es que queremos transmitir e que s\u00e3o relevantes para a intera\u00e7\u00e3o entre o cliente e o servidor. As reivindica\u00e7\u00f5es s\u00e3o codificadas em Base64Url e formam a segunda parte do JWT.

    {\n  \"sub\": \"teste@test.com\",\n  \"exp\": 1690258153\n}\n
  3. Signature: A assinatura \u00e9 utilizada para verificar que o remetente do JWT \u00e9 quem afirma ser e para garantir que a mensagem n\u00e3o foi alterada ao longo do caminho. Para criar a assinatura, voc\u00ea precisa codificar o cabe\u00e7alho, o payload, e um segredo utilizando o algoritmo especificado no cabe\u00e7alho. A assinatura \u00e9 a terceira parte do JWT. Uma assinatura de JWT pode ser criada como se segue:

    HMACSHA256(\n    base64UrlEncode(header) + \".\" +\n    base64UrlEncode(payload),\n nosso-segredo\n)\n

Essas tr\u00eas partes s\u00e3o separadas por pontos (.) e juntas formam um token JWT.

Formando a estrutura: HEADER.PAYLOAD.SIGNATURE que formam um token parecido com

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ0ZXN0ZUB0ZXN0LmNvbSIsImV4cCI6MTY5MDI1ODE1M30.Nx0P_ornVwJBH_LLLVrlJoh6RmJeXR-Nr7YJ_mlGY04\n

\u00c9 importante ressaltar que, apesar de a informa\u00e7\u00e3o em um JWT estar codificada, ela n\u00e3o est\u00e1 criptografada. Isso significa que qualquer pessoa com acesso ao token pode decodificar e ler as informa\u00e7\u00f5es nele. No entanto, sem o segredo usado para assinar o token, eles n\u00e3o podem alterar as informa\u00e7\u00f5es ou forjar um novo token. Portanto, n\u00e3o devemos incluir informa\u00e7\u00f5es sens\u00edveis ou confidenciais no payload do JWT.

Se quisermos ver o header, o payload e a assinatura contidas nesse token podemos acessar o debuger do jwt e checar quais as informa\u00e7\u00f5es que est\u00e3o nesse token:

"},{"location":"06/#claims","title":"Claims","text":"

As Claims do JWT s\u00e3o as informa\u00e7\u00f5es que ser\u00e3o adicionadas ao token via payload. Como:

{\n    \"sub\": \"teste@test.com\",\n    \"exp\": 1690258153\n}\n

Onde as chaves deste exemplo:

Em nossos exemplos iremos usar somente essas duas claims, mais existem muitas outras. Voc\u00ea pode ver a lista completa das claims aqui caso queira aprender mais.

"},{"location":"06/#como-funciona-o-jwt","title":"Como funciona o JWT","text":"

Em uma aplica\u00e7\u00e3o web, o processo de autentica\u00e7\u00e3o geralmente funciona da seguinte maneira:

  1. O usu\u00e1rio envia suas credenciais (e-mail e senha) para o servidor em um endpoint de gera\u00e7\u00e3o de token (/token por exemplo);
  2. O servidor verifica as credenciais e, se estiverem corretas, gera um token JWT e o envia de volta ao cliente;
  3. Nas solicita\u00e7\u00f5es subsequentes, o cliente deve incluir esse token no cabe\u00e7alho de autoriza\u00e7\u00e3o de suas solicita\u00e7\u00f5es. Como, por exemplo: Authorization: Bearer <token>;
  4. Quando o servidor recebe uma solicita\u00e7\u00e3o com um token JWT, ele pode verificar a assinatura e se o token \u00e9 v\u00e1lido e n\u00e3o expirou, ele processa a solicita\u00e7\u00e3o.
sequenceDiagram\n  participant Cliente as Cliente\n  participant Servidor as Servidor\n  Cliente->>Servidor: Envia credenciais (e-mail e senha)\n  Servidor->>Cliente: Verifica as credenciais\n  Servidor->>Cliente: Envia token JWT\n  Cliente->>Servidor: Envia solicita\u00e7\u00e3o com token JWT no cabe\u00e7alho de autoriza\u00e7\u00e3o\n  Servidor->>Cliente: Verifica o token JWT e processa a solicita\u00e7\u00e3o

Nos pr\u00f3ximos t\u00f3picos, vamos detalhar como podemos gerar e verificar tokens JWT em nossa aplica\u00e7\u00e3o FastAPI, bem como adicionar autentica\u00e7\u00e3o e autoriza\u00e7\u00e3o aos nossos endpoints.

"},{"location":"06/#gerando-tokens-jwt","title":"Gerando tokens JWT","text":"

Para gerar tokens JWT, precisamos de duas bibliotecas extras: pyjwt e pwdlib. A primeira ser\u00e1 usada para a gera\u00e7\u00e3o do token, enquanto a segunda ser\u00e1 usada para criptografar as senhas dos usu\u00e1rios. Para instal\u00e1-las, execute o seguinte comando no terminal:

$ Execu\u00e7\u00e3o no terminal!
poetry add pyjwt \"pwdlib[argon2]\"\n

Agora, criaremos uma fun\u00e7\u00e3o para gerar nossos tokens JWT. Criaremos um novo arquivo para gerenciar a seguran\u00e7a: security.py. Nesse arquivo iniciaremos a gera\u00e7\u00e3o dos tokens:

fast_zero/security.py
from datetime import datetime, timedelta\n\nfrom jwt import encode\nfrom pwdlib import PasswordHash\nfrom zoneinfo import ZoneInfo\n\nSECRET_KEY = 'your-secret-key'  # Isso \u00e9 provis\u00f3rio, vamos ajustar!\nALGORITHM = 'HS256'\nACCESS_TOKEN_EXPIRE_MINUTES = 30\npwd_context = PasswordHash.recommended()\n\n\ndef create_access_token(data: dict):\n    to_encode = data.copy()\n    expire = datetime.now(tz=ZoneInfo('UTC')) + timedelta(\n        minutes=ACCESS_TOKEN_EXPIRE_MINUTES\n    )\n    to_encode.update({'exp': expire})\n    encoded_jwt = encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)\n    return encoded_jwt\n

A fun\u00e7\u00e3o create_access_token \u00e9 respons\u00e1vel por criar um novo token JWT que ser\u00e1 usado para autenticar o usu\u00e1rio. Ela recebe um dicion\u00e1rio de dados, adiciona um tempo de expira\u00e7\u00e3o ao token (baseado na constante ACCESS_TOKEN_EXPIRE_MINUTES). Esses dados, em conjunto, formam o payload do JWT. Em seguida, usa a biblioteca pyjwt para codificar essas informa\u00e7\u00f5es em um token JWT, que \u00e9 ent\u00e3o retornado.

Note que a constante SECRET_KEY \u00e9 usada para assinar o token, e o algoritmo HS256 \u00e9 usado para a codifica\u00e7\u00e3o. Em um cen\u00e1rio de produ\u00e7\u00e3o, voc\u00ea deve manter a SECRET_KEY em um local seguro e n\u00e3o exp\u00f4-la em seu c\u00f3digo.

"},{"location":"06/#testando-a-geracao-de-tokens","title":"Testando a gera\u00e7\u00e3o de tokens","text":"

Embora esse c\u00f3digo ser\u00e1 coberto no futuro com a utiliza\u00e7\u00e3o do token, \u00e9 interessante criarmos um teste para essa fun\u00e7\u00e3o com uma finalidade puramente did\u00e1tica. De forma que vejamos os tokens gerados pelo pyjwt e interagirmos com ele.

Com isso criaremos um arquivo chamado tests/test_security.py para efetuar esse teste:

tests/test_security.py
from jwt import decode\n\nfrom fast_zero.security import SECRET_KEY, create_access_token\n\n\ndef test_jwt():\n    data = {'test': 'test'}\n    token = create_access_token(data)\n\n    decoded = decode(token, SECRET_KEY, algorithms=['HS256'])\n\n    assert decoded['test'] == data['test']\n    assert decoded['exp']  # Testa se o valor de exp foi adicionado ao token\n

Na pr\u00f3xima se\u00e7\u00e3o, veremos como podemos usar a biblioteca pwdlib para tratar as senhas dos usu\u00e1rios.

"},{"location":"06/#hashing-de-senhas","title":"Hashing de Senhas","text":"

Armazenar senhas em texto puro \u00e9 uma pr\u00e1tica de seguran\u00e7a extremamente perigosa. Em vez disso, \u00e9 uma pr\u00e1tica padr\u00e3o criptografar (\"hash\") as senhas antes de armazen\u00e1-las. Quando um usu\u00e1rio tenta se autenticar, a senha inserida \u00e9 criptografada novamente e comparada com a vers\u00e3o criptografada armazenada no banco de dados. Se as duas correspondem, o usu\u00e1rio \u00e9 autenticado.

Implementaremos essa funcionalidade usando a biblioteca pwdlib. Criaremos duas fun\u00e7\u00f5es: uma para criar o hash da senha e outra para verificar se uma senha inserida corresponde ao hash armazenado. Adicione o seguinte c\u00f3digo ao arquivo security.py:

fast_zero/security.py
def get_password_hash(password: str):\n    return pwd_context.hash(password)\n\n\ndef verify_password(plain_password: str, hashed_password: str):\n    return pwd_context.verify(plain_password, hashed_password)\n

A fun\u00e7\u00e3o get_password_hash recebe uma senha em texto puro como argumento e retorna uma vers\u00e3o criptografada dessa senha. A fun\u00e7\u00e3o verify_password recebe uma senha em texto puro e uma senha criptografada como argumentos, e verifica se a senha em texto puro, quando criptografada, corresponde \u00e0 senha criptografada. Ambas as fun\u00e7\u00f5es utilizam o objeto pwd_context, que definimos anteriormente usando a biblioteca pwdlib.

Agora, quando um usu\u00e1rio se registra em nossa aplica\u00e7\u00e3o, devemos usar a fun\u00e7\u00e3o get_password_hash para armazenar uma vers\u00e3o criptografada da senha. Quando um usu\u00e1rio tenta se autenticar, devemos usar a fun\u00e7\u00e3o verify_password para verificar se a senha inserida corresponde \u00e0 senha armazenada.

Na pr\u00f3xima se\u00e7\u00e3o, modificaremos nossos endpoints para fazer uso dessas fun\u00e7\u00f5es.

"},{"location":"06/#modificando-o-endpoint-de-post-para-encriptar-a-senha","title":"Modificando o endpoint de POST para encriptar a senha","text":"

Com as fun\u00e7\u00f5es de cria\u00e7\u00e3o de hash de senha e verifica\u00e7\u00e3o de senha em vigor, agora podemos atualizar nossos endpoints para usar essa nova funcionalidade de encripta\u00e7\u00e3o.

Primeiro, modificaremos a fun\u00e7\u00e3o create_user para criar um hash da senha antes de armazen\u00e1-la no banco de dados. Para fazer isso precisamos importar a fun\u00e7\u00e3o de gera\u00e7\u00e3o de hash get_password_hash e no momento da cria\u00e7\u00e3o do registro na tabela a senha deve ser passada com o hash gerado:

fast_zero/app.py
from fast_zero.security import get_password_hash\n\n# ...\n\n@app.post('/users/', status_code=HTTPStatus.CREATED, response_model=UserPublic)\ndef create_user(user: UserSchema, session: Session = Depends(get_session)):\n    db_user = session.scalar(\n        select(User).where(\n            (User.username == user.username) | (User.email == user.email)\n        )\n    )\n\n    if db_user:\n        if db_user.username == user.username:\n            raise HTTPException(\n                status_code=HTTPStatus.BAD_REQUEST,\n                detail='Username already exists',\n            )\n        elif db_user.email == user.email:\n            raise HTTPException(\n                status_code=HTTPStatus.BAD_REQUEST,\n                detail='Email already exists',\n            )\n\n    hashed_password = get_password_hash(user.password)\n\n    db_user = User(\n        email=user.email,\n        username=user.username,\n        password=hashed_password,\n    )\n\n    session.add(db_user)\n    session.commit()\n    session.refresh(db_user)\n\n    return db_user\n

Desta forma, a senha n\u00e3o ser\u00e1 mais criada em texto plano no objeto User. Fazendo com que caso exista algum problema relacionado a vazamento de dados, as senhas das pessoas nunca sejam expostas.

"},{"location":"06/#sobre-o-teste-da-post-users","title":"Sobre o teste da POST /users/","text":"

Por n\u00e3o validar o password, usando o retorno UserPublic, o teste j\u00e1 escrito deve passar normalmente:

$ Execu\u00e7\u00e3o no terminal!
task test\n\n# ...\n\ntests/test_app.py::test_root_deve_retornar_ok_e_ola_mundo PASSED\ntests/test_app.py::test_create_user PASSED\n
"},{"location":"06/#modificando-o-endpoint-de-atualizacao-de-usuarios","title":"Modificando o endpoint de atualiza\u00e7\u00e3o de usu\u00e1rios","text":"

\u00c9 igualmente importante modificar a fun\u00e7\u00e3o update_user para tamb\u00e9m criar um hash da senha antes de atualizar User no banco de dados. Caso contr\u00e1rio, a senha em texto puro seria armazenada no banco de dados no momento da atualiza\u00e7\u00e3o.

fast_zero/app.py
@app.put('/users/{user_id}', response_model=UserPublic)\ndef update_user(\n    user_id: int,\n    user: UserSchema,\n    session: Session = Depends(get_session),\n):\n    db_user = session.scalar(select(User).where(User.id == user_id))\n    if not db_user:\n        raise HTTPException(\n            status_code=HTTPStatus.NOT_FOUND, detail='User not found'\n        )\n\n    db_user.username = user.username\n    db_user.password = get_password_hash(user.password)\n    db_user.email = user.email\n    session.commit()\n    session.refresh(db_user)\n    return db_user\n

Assim, a atualiza\u00e7\u00e3o de um User, via m\u00e9todo PUT, tamb\u00e9m criar\u00e1 o hash da senha no momento da atualiza\u00e7\u00e3o. Pois, nesse caso em espec\u00edfico, existe a possibilidade de alterar qualquer coluna da tabela, inclusive o campo password.

"},{"location":"06/#sobre-os-testes-da-put-usersuser_id","title":"Sobre os testes da PUT /users/{user_id}","text":"

Assim como no teste da rota de cria\u00e7\u00e3o, os testes tamb\u00e9m passam normalmente por n\u00e3o validarem o campo password.

$ Execu\u00e7\u00e3o no terminal!
task test\n\n# ...\n\ntests/test_app.py::test_root_deve_retornar_ok_e_ola_mundo PASSED\ntests/test_app.py::test_create_user PASSED\ntests/test_app.py::test_read_users PASSED\ntests/test_app.py::test_read_users_with_users PASSED\ntests/test_app.py::test_update_user PASSED\n
"},{"location":"06/#criando-um-endpoint-de-geracao-do-token","title":"Criando um endpoint de gera\u00e7\u00e3o do token","text":"

Antes de criar o endpoint, precisamos criar um schema para o nosso token. Em um contexto JWT, access_token \u00e9 o pr\u00f3prio token que representa a sess\u00e3o do usu\u00e1rio e cont\u00e9m informa\u00e7\u00f5es sobre o usu\u00e1rio, enquanto token_type \u00e9 um tipo de autentica\u00e7\u00e3o que ser\u00e1 inclu\u00eddo no cabe\u00e7alho de autoriza\u00e7\u00e3o de cada solicita\u00e7\u00e3o. Em geral, o token_type para JWT \u00e9 \"bearer\".

fast_zero/schemas.py
class Token(BaseModel):\n    access_token: str\n    token_type: str\n
"},{"location":"06/#utilizando-oauth2passwordrequestform","title":"Utilizando OAuth2PasswordRequestForm","text":"

A classe OAuth2PasswordRequestForm \u00e9 uma classe especial do FastAPI que gera automaticamente um formul\u00e1rio para solicitar o username (email neste caso) e a senha. Este formul\u00e1rio ser\u00e1 apresentado automaticamente no Swagger UI e Redoc, facilitando a realiza\u00e7\u00e3o de testes de autentica\u00e7\u00e3o.

Para usar os formul\u00e1rios no FastAPI, precisamos instalar o python-multipart:

$ Execu\u00e7\u00e3o no terminal!
poetry add python-multipart\n
"},{"location":"06/#criando-um-endpoint-de-geracao-do-token_1","title":"Criando um endpoint de gera\u00e7\u00e3o do token","text":"

Agora criaremos o endpoint que ir\u00e1 autenticar o usu\u00e1rio e fornecer um token de acesso JWT. Este endpoint ir\u00e1 receber as informa\u00e7\u00f5es de login do usu\u00e1rio, verificar se as credenciais s\u00e3o v\u00e1lidas e, em caso afirmativo, retornar um token de acesso JWT.

fast_zero/app.py
from fastapi.security import OAuth2PasswordRequestForm\nfrom fast_zero.schemas import Message, Token, UserList, UserPublic, UserSchema\nfrom fast_zero.security import (\n    create_access_token,\n    get_password_hash,\n    verify_password,\n)\n\n# ...\n\n@app.post('/token', response_model=Token)\ndef login_for_access_token(\n    form_data: OAuth2PasswordRequestForm = Depends(),\n    session: Session = Depends(get_session),\n):\n    user = session.scalar(select(User).where(User.email == form_data.username))\n\n    if not user:\n        raise HTTPException(\n            status_code=HTTPStatus.BAD_REQUEST,\n            detail='Incorrect email or password'\n        )\n\n    if not verify_password(form_data.password, user.password):\n        raise HTTPException(\n            status_code=HTTPStatus.BAD_REQUEST,\n            detail='Incorrect email or password'\n        )\n\n    access_token = create_access_token(data={'sub': user.email})\n\n    return {'access_token': access_token, 'token_type': 'bearer'}\n

Esse endpoint recebe os dados do formul\u00e1rio atrav\u00e9s do form_data (que s\u00e3o injetados automaticamente gra\u00e7as ao Depends()) e tenta recuperar um usu\u00e1rio com o email fornecido. Se o usu\u00e1rio n\u00e3o for encontrado ou a senha n\u00e3o corresponder ao hash armazenado no banco de dados, uma exce\u00e7\u00e3o \u00e9 lan\u00e7ada. Caso contr\u00e1rio, um token de acesso \u00e9 criado usando o create_access_token() que criamos anteriormente e retornado como uma resposta.

"},{"location":"06/#testando-token","title":"Testando /token","text":"

Agora escreveremos um teste para verificar se o nosso novo endpoint est\u00e1 funcionando corretamente.

tests/test_app.py
def test_get_token(client, user):\n    response = client.post(\n        '/token',\n        data={'username': user.email, 'password': user.password},\n    )\n    token = response.json()\n\n    assert response.status_code == HTTPStatus.OK\n    assert 'access_token' in token\n    assert 'token_type' in token\n

Nesse teste, n\u00f3s enviamos uma requisi\u00e7\u00e3o POST para o endpoint \"/token\" com um username e uma senha v\u00e1lidos. Ent\u00e3o, n\u00f3s verificamos que a resposta cont\u00e9m um \"access_token\" e um \"token_type\", que s\u00e3o os campos que esperamos de um JWT v\u00e1lido.

No entanto, h\u00e1 um problema. Agora que a senha est\u00e1 sendo criptografada, nosso teste falhar\u00e1:

$ Execu\u00e7\u00e3o no terminal!
task test\n\n# ...\ntests/test_app.py::test_get_token FAILED\n

Para corrigir isso, precisamos garantir que a senha esteja sendo criptografada na fixture antes de ser salva:

tests/conftest.py
from fast_zero.security import get_password_hash\n\n# ...\n\n@pytest.fixture\ndef user(session):\n    user = User(\n        username='Teste',\n        email='teste@test.com',\n        password=get_password_hash('testtest'),\n    )\n    session.add(user)\n    session.commit()\n    session.refresh(user)\n\n    return user\n

Rodaremos o teste novamente. No entanto, ainda teremos um problema. Agora s\u00f3 temos a vers\u00e3o criptografada da senha, que n\u00e3o \u00e9 \u00fatil para fazer o login, j\u00e1 que o login exige a senha em texto puro:

$ Execu\u00e7\u00e3o no terminal!
task test\n\n# ...\ntests/test_app.py::test_get_token FAILED\n

Para resolver isso, faremos uma modifica\u00e7\u00e3o no objeto user (um monkey patch) para adicionar a senha em texto puro:

tests/conftest.py
@pytest.fixture\ndef user(session):\n    password = 'testtest'\n    user = User(\n        username='Teste',\n        email='teste@test.com',\n        password=get_password_hash(password),\n    )\n    session.add(user)\n    session.commit()\n    session.refresh(user)\n\n    user.clean_password = 'testtest'\n\n    return user\n

Monkey patching \u00e9 uma t\u00e9cnica em que modificamos ou estendemos o c\u00f3digo em tempo de execu\u00e7\u00e3o. Neste caso, estamos adicionando um novo atributo clean_password ao objeto user para armazenar a senha em texto puro.

Agora, podemos alterar o teste para usar clean_password:

tests/test_app.py
def test_get_token(client, user):\n    response = client.post(\n        '/token',\n        data={'username': user.email, 'password': user.clean_password},\n    )\n    token = response.json()\n\n    assert response.status_code == HTTPStatus.OK\n    assert 'access_token' in token\n    assert 'token_type' in token\n

E agora todos os testes devem passar normalmente:

$ Execu\u00e7\u00e3o no terminal!
task test\n\n# ...\n\ntests/test_app.py::test_root_deve_retornar_ok_e_ola_mundo PASSED\ntests/test_app.py::test_create_user PASSED\ntests/test_app.py::test_read_users PASSED\ntests/test_app.py::test_read_users_with_users PASSED\ntests/test_app.py::test_update_user PASSED\ntests/test_app.py::test_delete_user PASSED\ntests/test_app.py::test_get_token PASSED\ntests/test_db.py::test_create_user PASSED\n

Isso conclui a parte de autentica\u00e7\u00e3o de nossa API. No pr\u00f3ximo passo, implementaremos a autoriza\u00e7\u00e3o nos endpoints.

"},{"location":"06/#protegendo-os-endpoints","title":"Protegendo os Endpoints","text":"

Agora que temos uma forma de autenticar nossos usu\u00e1rios e emitir tokens JWT, \u00e9 hora de usar essa infraestrutura para proteger nossos endpoints. Neste passo, adicionaremos autentica\u00e7\u00e3o aos endpoints PUT e DELETE.

Para garantir que as informa\u00e7\u00f5es do usu\u00e1rio sejam extra\u00eddas corretamente do token JWT, precisamos de um schema especial, o TokenData. Esse schema ser\u00e1 utilizado para tipificar os dados extra\u00eddos do token JWT e garantir que temos um campo username que ser\u00e1 usado para identificar o usu\u00e1rio.

fast_zero/schemas.py
class TokenData(BaseModel):\n    username: str | None = None\n

Nesse ponto, criaremos uma a fun\u00e7\u00e3o get_current_user que ser\u00e1 respons\u00e1vel por extrair o token JWT do header Authorization da requisi\u00e7\u00e3o, decodificar esse token, extrair as informa\u00e7\u00f5es do usu\u00e1rio e obter finalmente o usu\u00e1rio do banco de dados. Se qualquer um desses passos falhar, uma exce\u00e7\u00e3o ser\u00e1 lan\u00e7ada e a requisi\u00e7\u00e3o ser\u00e1 negada. A adicionaremos ao security.py:

fast_zero/security.py
from datetime import datetime, timedelta\nfrom http import HTTPStatus\n\nfrom fastapi import Depends, HTTPException\nfrom fastapi.security import OAuth2PasswordBearer\nfrom jwt import DecodeError, decode, encode\nfrom pwdlib import PasswordHash\nfrom sqlalchemy import select\nfrom sqlalchemy.orm import Session\n\nfrom fast_zero.database import get_session\nfrom fast_zero.models import User\nfrom fast_zero.schemas import TokenData\n\n# ...\n\noauth2_scheme = OAuth2PasswordBearer(tokenUrl='token')\n\ndef get_current_user(\n    session: Session = Depends(get_session),\n    token: str = Depends(oauth2_scheme), #(1)!\n):\n    credentials_exception = HTTPException(  #(2)!\n        status_code=HTTPStatus.UNAUTHORIZED,\n        detail='Could not validate credentials',\n        headers={'WWW-Authenticate': 'Bearer'},\n    )\n\n    try:\n        payload = decode(token, SECRET_KEY, algorithms=[ALGORITHM])\n        username: str = payload.get('sub')\n        if not username:\n            raise credentials_exception  #(3)!\n        token_data = TokenData(username=username)\n    except DecodeError:\n        raise credentials_exception  #(4)!\n\n    user = session.scalar(\n        select(User).where(User.email == token_data.username)\n    )\n\n    if not user:\n        raise credentials_exception  #(5)!\n\n    return user\n
  1. A inje\u00e7\u00e3o de oauth2_scheme garante que um token foi enviado, caso n\u00e3o tenha sido enviado ele redirecionar\u00e1 a tokenUrl do objeto OAuth2PasswordBearer.
  2. Como essa opera\u00e7\u00e3o pode apresentar erros em diversos momentos, foi atribu\u00eddo um \u00fanico erro a vari\u00e1vel credentials_exception.
  3. Nessa primeira valida\u00e7\u00e3o \u00e9 checado se ap\u00f3s o decode do token a chave username est\u00e1 presente. Caso n\u00e3o esteja, o erro ser\u00e1 levantado.
  4. Nessa valida\u00e7\u00e3o \u00e9 testada se o token \u00e9 um token JWT v\u00e1lido. Caso n\u00e3o seja, o erro tamb\u00e9m ser\u00e1 levantado.
  5. Nossa \u00faltima valida\u00e7\u00e3o checa, ap\u00f3s de garantir que existe username, se ele est\u00e1 presente em nossa base de dados. Caso n\u00e3o, o erro ser\u00e1 levantado.

Aqui, a fun\u00e7\u00e3o get_current_user \u00e9 definida como ass\u00edncrona, indicando que ela pode realizar opera\u00e7\u00f5es de IO (como consultar um banco de dados) de forma n\u00e3o bloqueante. Esta fun\u00e7\u00e3o aceita dois argumentos: session e token. O session \u00e9 obtido atrav\u00e9s da fun\u00e7\u00e3o get_session (n\u00e3o mostrada aqui), que deve retornar uma sess\u00e3o de banco de dados ativa. O token \u00e9 obtido do header de autoriza\u00e7\u00e3o da requisi\u00e7\u00e3o, que \u00e9 esperado ser do tipo Bearer (indicado pelo esquema OAuth2).

A vari\u00e1vel credentials_exception \u00e9 definida como uma exce\u00e7\u00e3o HTTP que ser\u00e1 lan\u00e7ada sempre que houver um problema com as credenciais fornecidas pelo usu\u00e1rio. O status 401 indica que a autentica\u00e7\u00e3o falhou e a mensagem \"Could not validate credentials\" \u00e9 retornada ao cliente. Al\u00e9m disso, um cabe\u00e7alho 'WWW-Authenticate' \u00e9 inclu\u00eddo na resposta, indicando que o cliente deve fornecer autentica\u00e7\u00e3o.

No bloco try, tentamos decodificar o token JWT usando a chave secreta e o algoritmo especificado. O token decodificado \u00e9 armazenado na vari\u00e1vel payload. Extra\u00edmos o campo 'sub' (normalmente usado para armazenar o identificador do usu\u00e1rio no token JWT) e verificamos se ele existe. Se n\u00e3o, lan\u00e7amos a exce\u00e7\u00e3o credentials_exception. Em seguida, criamos um objeto TokenData com o username.

Por fim, realizamos uma consulta ao banco de dados para encontrar o usu\u00e1rio com o e-mail correspondente ao username contido no token. session.scalar \u00e9 usado para retornar a primeira coluna do primeiro resultado da consulta. Se nenhum usu\u00e1rio for encontrado, lan\u00e7amos a exce\u00e7\u00e3o credentials_exception. Se um usu\u00e1rio for encontrado, retornamos esse usu\u00e1rio.

"},{"location":"06/#aplicacao-da-protecao-ao-endpoint","title":"Aplica\u00e7\u00e3o da prote\u00e7\u00e3o ao endpoint","text":"

Primeiro, aplicaremos a autentica\u00e7\u00e3o no endpoint PUT. Se o user_id da rota n\u00e3o corresponder ao id do usu\u00e1rio autenticado, retornaremos um erro 400. Se tudo estiver correto, o usu\u00e1rio ser\u00e1 atualizado normalmente.

fast_zero/app.py
from fast_zero.security import (\n    create_access_token,\n    get_current_user,\n    get_password_hash,\n    verify_password,\n)\n\n# ...\n\n@app.put('/users/{user_id}', response_model=UserPublic)\ndef update_user(\n    user_id: int,\n    user: UserSchema,\n    session: Session = Depends(get_session),\n    current_user: User = Depends(get_current_user),\n):\n    if current_user.id != user_id:\n        raise HTTPException(\n            status_code=HTTPStatus.FORBIDDEN, detail='Not enough permissions'\n        )\n\n    current_user.username = user.username\n    current_user.password = get_password_hash(user.password)\n    current_user.email = user.email\n    session.commit()\n    session.refresh(current_user)\n\n    return current_user\n

Com isso, podemos remover a query feita no endpoint para encontrar o User, pois ela j\u00e1 est\u00e1 sendo feita no get_current_user, simplificando ainda mais nosso endpoint.

Agora, aplicaremos a autentica\u00e7\u00e3o no endpoint DELETE. Semelhante ao PUT, se o user_id da rota n\u00e3o corresponder ao id do usu\u00e1rio autenticado, retornaremos um erro 400. Se tudo estiver correto, o usu\u00e1rio ser\u00e1 deletado.

fast_zero/app.py
@app.delete('/users/{user_id}', response_model=Message)\ndef delete_user(\n    user_id: int,\n    session: Session = Depends(get_session),\n    current_user: User = Depends(get_current_user),\n):\n    if current_user.id != user_id:\n        raise HTTPException(\n            status_code=HTTPStatus.FORBIDDEN, detail='Not enough permissions'\n        )\n\n    session.delete(current_user)\n    session.commit()\n\n    return {'message': 'User deleted'}\n

Com essa nova depend\u00eancia, o FastAPI automaticamente garantir\u00e1 que um token de autentica\u00e7\u00e3o v\u00e1lido seja fornecido antes de permitir o acesso a esses endpoints. Se o token n\u00e3o for v\u00e1lido, ou se o usu\u00e1rio tentar modificar ou deletar um usu\u00e1rio diferente, um erro ser\u00e1 retornado.

"},{"location":"06/#atualizando-os-testes","title":"Atualizando os Testes","text":"

Os testes precisam ser atualizados para refletir essas mudan\u00e7as. Primeiro, precisamos criar uma nova fixture que gere um token para um usu\u00e1rio de teste.

tests/conftest.py
@pytest.fixture\ndef token(client, user):\n    response = client.post(\n        '/token',\n        data={'username': user.email, 'password': user.clean_password},\n    )\n    return response.json()['access_token']\n

Agora, podemos atualizar os testes para o endpoint PUT e DELETE para incluir a autentica\u00e7\u00e3o.

tests/test_app.py
def test_update_user(client, user, token):\n    response = client.put(\n        f'/users/{user.id}',\n        headers={'Authorization': f'Bearer {token}'},\n        json={\n            'username': 'bob',\n            'email': 'bob@example.com',\n            'password': 'mynewpassword',\n        },\n    )\n    assert response.status_code == HTTPStatus.OK\n    assert response.json() == {\n        'username': 'bob',\n        'email': 'bob@example.com',\n        'id': user.id,\n    }\n\n\ndef test_delete_user(client, user, token):\n    response = client.delete(\n        f'/users/{user.id}',\n        headers={'Authorization': f'Bearer {token}'},\n    )\n    assert response.status_code == HTTPStatus.OK\n    assert response.json() == {'message': 'User deleted'}\n

Finalmente, podemos rodar todos os testes para garantir que tudo esteja funcionando corretamente.

$ Execu\u00e7\u00e3o no terminal!
task test\n\n# ...\n\ntests/test_app.py::test_root_deve_retornar_ok_e_ola_mundo PASSED\ntests/test_app.py::test_create_user PASSED\ntests/test_app.py::test_read_users PASSED\ntests/test_app.py::test_read_users_with_users PASSED\ntests/test_app.py::test_update_user PASSED\ntests/test_app.py::test_delete_user PASSED\ntests/test_app.py::test_get_token PASSED\ntests/test_db.py::test_create_user PASSED\n

Com essas altera\u00e7\u00f5es, nossos endpoints agora est\u00e3o seguramente protegidos pela autentica\u00e7\u00e3o. Apenas os usu\u00e1rios autenticados podem alterar ou deletar seus pr\u00f3prios dados. Isso traz uma camada adicional de seguran\u00e7a e integridade para o nosso aplicativo.

"},{"location":"06/#checagem-de-tokens-invalidos","title":"Checagem de tokens inv\u00e1lidos","text":"

Uma coisa que pode ter passado em branco durante a fase de testes \u00e9 a valida\u00e7\u00e3o do token JWT. Ele pode estar em um formato inv\u00e1lido, \u00e0s vezes por erro do cliente, outras por algum problema de seguran\u00e7a. \u00c9 importante validarmos como a aplica\u00e7\u00e3o vai reagir a um token inv\u00e1lido.

Durante a constru\u00e7\u00e3o da fun\u00e7\u00e3o get_current_user, criamos um fluxo para esse erro:

Parte do c\u00f3digo da fun\u00e7\u00e3o `get_current_user` em security.py
credentials_exception = HTTPException(\n    status_code=HTTPStatus.UNAUTHORIZED,\n    detail='Could not validate credentials',\n    headers={'WWW-Authenticate': 'Bearer'},\n)\n\ntry:\n    payload = decode(token, SECRET_KEY, algorithms=[ALGORITHM])\n    # C\u00f3digo para caso o token seja decodificado\nexcept DecodeError:\n    raise credentials_exception\n

Caso n\u00e3o seja poss\u00edvel decodificar o token, por algum motivo, \u00e9 importante validarmos se o retorno ser\u00e1 um 401 UNAUTHORIZED. Para isso, seria importante criar um teste que verifica essa camada.

Poder\u00edamos fazer de duas formas, chamando diretamente a fun\u00e7\u00e3o get_current_user passando um token inv\u00e1lido, ou fazer a valida\u00e7\u00e3o diretamente via a requisi\u00e7\u00e3o de algu\u00e9m que dependa de um token v\u00e1lido. Optarei pela segunda op\u00e7\u00e3o, por ser mais pr\u00f3xima de uma requisi\u00e7\u00e3o real.

tests/test_security.py
from http import HTTPStatus\n\n# ...\n\ndef test_jwt_invalid_token(client):\n    response = client.delete(\n        '/users/1', headers={'Authorization': 'Bearer token-invalido'}\n    )\n\n    assert response.status_code == HTTPStatus.UNAUTHORIZED\n    assert response.json() == {'detail': 'Could not validate credentials'}\n

Usamos um assert para validar se o c\u00f3digo \u00e9 mesmo 401 e outro para validar nossa mensagem de erro. Assim, temos uma garantia que, caso algu\u00e9m envie um token inv\u00e1lido, a resposta seguir\u00e1 como esperado.

Antes de finalizar, ideal seria executarmos os testes:

$ Execu\u00e7\u00e3o no terminal!
task test\n\n# ...\n\ntests/test_app.py::test_get_token PASSED\ntests/test_db.py::test_create_user PASSED\ntests/test_security.py::test_jwt_invalid_token PASSED\n
"},{"location":"06/#exercicios","title":"Exerc\u00edcios","text":"
  1. Fa\u00e7a um teste para cobrir o cen\u00e1rio que levanta exception credentials_exception na autentica\u00e7\u00e3o caso o User n\u00e3o seja encontrado. Ao olhar a cobertura de security.py voc\u00ea vai notar que esse contexto n\u00e3o est\u00e1 coberto.

  2. Reveja os testes criados at\u00e9 a aula 5 e veja se eles ainda fazem sentido (testes envolvendo 400)

"},{"location":"06/#commit","title":"Commit","text":"

Depois de finalizar a prote\u00e7\u00e3o dos endpoints e atualizar os testes, \u00e9 hora de fazer commit das altera\u00e7\u00f5es. N\u00e3o se esque\u00e7a de revisar as altera\u00e7\u00f5es antes de fazer o commit.

$ Execu\u00e7\u00e3o no terminal!
git status\ngit add .\ngit commit -m \"Protege os endpoints PUT e DELETE com autentica\u00e7\u00e3o\"\n
"},{"location":"06/#conclusao","title":"Conclus\u00e3o","text":"

Nesta aula, demos um passo importante para aumentar a seguran\u00e7a da nossa API. Implementamos a autentica\u00e7\u00e3o e a autoriza\u00e7\u00e3o para os endpoints PUT e DELETE, garantindo que apenas usu\u00e1rios autenticados possam alterar ou excluir seus pr\u00f3prios dados. Tamb\u00e9m atualizamos os testes para incluir a autentica\u00e7\u00e3o. Na pr\u00f3xima aula, continuaremos a expandir a funcionalidade da nossa API. At\u00e9 l\u00e1!

Agora que a aula acabou, \u00e9 um bom momento para voc\u00ea relembrar alguns conceitos e fixar melhor o conte\u00fado respondendo ao question\u00e1rio referente a ela.

Quiz

"},{"location":"07/","title":"Refatorando a Estrutura do Projeto","text":""},{"location":"07/#refatorando-a-estrutura-do-projeto","title":"Refatorando a Estrutura do Projeto","text":"

Objetivos da Aula:

Caso prefira ver a aula em v\u00eddeo

Esse aula ainda n\u00e3o est\u00e1 dispon\u00edvel em formato de v\u00eddeo, somente em texto ou live!

Aula Slides C\u00f3digo Quiz

Ao longo da evolu\u00e7\u00e3o de um projeto, \u00e9 natural que sua estrutura inicial necessite de ajustes para manter a legibilidade, a facilidade de manuten\u00e7\u00e3o e a organiza\u00e7\u00e3o do c\u00f3digo. Nesta aula, faremos exatamente isso em nosso projeto FastAPI: refatoraremos partes dele para melhorar sua estrutura e, em seguida, ampliar a cobertura de nossos testes para garantir que todos os cen\u00e1rios poss\u00edveis sejam tratados corretamente. Vamos come\u00e7ar!

"},{"location":"07/#criando-routers","title":"Criando Routers","text":"

O FastAPI oferece uma ferramenta poderosa conhecida como routers, que facilita a organiza\u00e7\u00e3o e agrupamento de diferentes rotas em uma aplica\u00e7\u00e3o. Pense em um router como um \"subaplicativo\" do FastAPI que pode ser integrado em uma aplica\u00e7\u00e3o principal. Isso n\u00e3o s\u00f3 mant\u00e9m o c\u00f3digo organizado e leg\u00edvel, mas tamb\u00e9m se mostra especialmente \u00fatil \u00e0 medida que a aplica\u00e7\u00e3o se expande e novas rotas s\u00e3o adicionadas.

Esse tipo de organiza\u00e7\u00e3o nos oferece diversos benef\u00edcios:

  1. Organiza\u00e7\u00e3o e Legibilidade: Routers ajudam a manter o c\u00f3digo organizado e leg\u00edvel, o que \u00e9 crucial \u00e0 medida que a aplica\u00e7\u00e3o se expande.
  2. Separa\u00e7\u00e3o de Preocupa\u00e7\u00f5es: Alinhado ao princ\u00edpio de SoC, os routers facilitam o entendimento e teste do c\u00f3digo.
  3. Escalabilidade: A estrutura\u00e7\u00e3o com routers permite adicionar novas rotas e funcionalidades de maneira eficiente conforme o projeto cresce.
"},{"location":"07/#estruturacao-inicial","title":"Estrutura\u00e7\u00e3o Inicial","text":"

Criaremos inicialmente uma nova estrutura de diret\u00f3rios chamada routers dentro do seu projeto fast_zero. Aqui, teremos subaplicativos dedicados a fun\u00e7\u00f5es espec\u00edficas, como gerenciamento de usu\u00e1rios e autentica\u00e7\u00e3o.

\u251c\u2500\u2500 fast_zero\n\u2502  \u251c\u2500\u2500 app.py\n\u2502  \u251c\u2500\u2500 database.py\n\u2502  \u251c\u2500\u2500 models.py\n\u2502  \u251c\u2500\u2500 routers\n\u2502  \u2502  \u251c\u2500\u2500 auth.py\n\u2502  \u2502  \u2514\u2500\u2500 users.py\n

Esta organiza\u00e7\u00e3o facilita a expans\u00e3o do seu projeto e a manuten\u00e7\u00e3o de uma estrutura clara.

"},{"location":"07/#implementando-um-router-para-usuarios","title":"Implementando um Router para Usu\u00e1rios","text":"

No arquivo fast_zero/routers/users.py, implementaremos o recurso APIRouter do FastAPI, a ferramenta chave para criar nosso subaplicativo. O par\u00e2metro prefix que passamos ajuda a agrupar todos os endpoints relacionados aos usu\u00e1rios sob um mesmo teto.

fast_zero/routers/users.py
from fastapi import APIRouter\n\nrouter = APIRouter(prefix='/users', tags=['users'])\n

Com essa simples configura\u00e7\u00e3o, estamos prontos para definir rotas espec\u00edficas para usu\u00e1rios neste router, em vez de sobrecarregar o aplicativo principal. Utilizamos @router ao inv\u00e9s de @app para definir estas rotas. O uso da tag 'users' contribui para a organiza\u00e7\u00e3o e documenta\u00e7\u00e3o autom\u00e1tica no swagger.

Desta forma podemos migrar todos os nossos imports e nossas fun\u00e7\u00f5es de endpoints para o arquivo fast_zero/routers/users.py e os removendo de fast_zero/app.py. Fazendo com que todos esses endpoints estejam no mesmo contexto e isolados da aplica\u00e7\u00e3o principal:

fast_zero/routers/users.py
from http import HTTPStatus\n\nfrom fastapi import APIRouter, Depends, HTTPException\nfrom sqlalchemy import select\nfrom sqlalchemy.orm import Session\n\nfrom fast_zero.database import get_session\nfrom fast_zero.models import User\nfrom fast_zero.schemas import Message, UserList, UserPublic, UserSchema\nfrom fast_zero.security import (\n    get_current_user,\n    get_password_hash,\n)\n\nrouter = APIRouter(prefix='/users', tags=['users'])\n\n@router.post('/', status_code=HTTPStatus.CREATED, response_model=UserPublic)\n# ...\n@router.get('/', response_model=UserList)\n# ...\n@router.put('/{user_id}', response_model=UserPublic)\n# ...\n@router.delete('/{user_id}', response_model=Message)\n# ...\n

Com o prefixo definido no router, os paths dos endpoints se tornam mais simples e diretos. Ao inv\u00e9s de '/users/{user_id}', por exemplo, usamos apenas '/{user_id}'.

Exemplo do arquivo fast_zero/routers/users.py completo fast_zero/routers/users.py
from http import HTTPStatus\n\nfrom fastapi import APIRouter, Depends, HTTPException\nfrom sqlalchemy import select\nfrom sqlalchemy.orm import Session\n\nfrom fast_zero.database import get_session\nfrom fast_zero.models import User\nfrom fast_zero.schemas import Message, UserList, UserPublic, UserSchema\nfrom fast_zero.security import (\n    get_current_user,\n    get_password_hash,\n)\n\nrouter = APIRouter(prefix='/users', tags=['users'])\n\n\n@router.post('/', status_code=HTTPStatus.CREATED, response_model=UserPublic)\ndef create_user(user: UserSchema, session: Session = Depends(get_session)):\n    db_user = session.scalar(select(User).where(User.email == user.email))\n    if db_user:\n        raise HTTPException(\n            status_code=HTTPStatus.BAD_REQUEST,\n            detail='Email already registered'\n        )\n\n    hashed_password = get_password_hash(user.password)\n\n    db_user = User(\n        email=user.email,\n        username=user.username,\n        password=hashed_password,\n    )\n    session.add(db_user)\n    session.commit()\n    session.refresh(db_user)\n    return db_user\n\n\n@router.get('/', response_model=UserList)\ndef read_users(\n    skip: int = 0, limit: int = 100, session: Session = Depends(get_session)\n):\n    users = session.scalars(select(User).offset(skip).limit(limit)).all()\n    return {'users': users}\n\n\n@router.put('/{user_id}', response_model=UserPublic)\ndef update_user(\n    user_id: int,\n    user: UserSchema,\n    session: Session = Depends(get_session),\n    current_user: User = Depends(get_current_user),\n):\n    if current_user.id != user_id:\n        raise HTTPException(\n            status_code=HTTPStatus.FORBIDDEN, detail='Not enough permissions'\n        )\n\n    current_user.username = user.username\n    current_user.password = get_password_hash(user.password)\n    current_user.email = user.email\n    session.commit()\n    session.refresh(current_user)\n\n    return current_user\n\n\n@router.delete('/{user_id}', response_model=Message)\ndef delete_user(\n    user_id: int,\n    session: Session = Depends(get_session),\n    current_user: User = Depends(get_current_user),\n):\n    if current_user.id != user_id:\n        raise HTTPException(\n            status_code=HTTPStatus.FORBIDDEN, detail='Not enough permissions'\n        )\n\n    session.delete(current_user)\n    session.commit()\n\n    return {'message': 'User deleted'}\n

Por termos criados as tags, isso reflete na organiza\u00e7\u00e3o do swagger

"},{"location":"07/#criando-um-router-para-auth","title":"Criando um router para Auth","text":"

No momento, temos rotas para / e /token ainda no arquivo fast_zero/app.py. Daremos um passo adiante e criar um router separado para lidar com a autentica\u00e7\u00e3o. Desta forma, conseguiremos manter nosso arquivo principal (app.py) mais limpo e focado em sua responsabilidade principal que \u00e9 iniciar nossa aplica\u00e7\u00e3o.

O router para autentica\u00e7\u00e3o ser\u00e1 criado no arquivo fast_zero/routers/auth.py. Veja como fazer:

fast_zero/routers/auth.py
from http import HTTPStatus\n\nfrom fastapi import APIRouter, Depends, HTTPException\nfrom fastapi.security import OAuth2PasswordRequestForm\nfrom sqlalchemy import select\nfrom sqlalchemy.orm import Session\n\nfrom fast_zero.database import get_session\nfrom fast_zero.models import User\nfrom fast_zero.schemas import Token\nfrom fast_zero.security import create_access_token, verify_password\n\nrouter = APIRouter(prefix='/auth', tags=['auth'])\n\n\n@router.post('/token', response_model=Token)\ndef login_for_access_token(\n    form_data: OAuth2PasswordRequestForm = Depends(),\n    session: Session = Depends(get_session),\n):\n    user = session.scalar(select(User).where(User.email == form_data.username))\n\n    if not user:\n        raise HTTPException(\n            status_code=HTTPStatus.BAD_REQUEST,\n            detail='Incorrect email or password'\n        )\n\n    if not verify_password(form_data.password, user.password):\n        raise HTTPException(\n            status_code=HTTPStatus.BAD_REQUEST,\n            detail='Incorrect email or password'\n        )\n\n    access_token = create_access_token(data={'sub': user.email})\n\n    return {'access_token': access_token, 'token_type': 'bearer'}\n

Neste bloco de c\u00f3digo, n\u00f3s criamos um novo router que lidar\u00e1 exclusivamente com a rota de obten\u00e7\u00e3o de token (/token). O endpoint login_for_access_token \u00e9 definido exatamente da mesma maneira que antes, mas agora como parte deste router de autentica\u00e7\u00e3o.

"},{"location":"07/#alteracao-da-validacao-de-token","title":"Altera\u00e7\u00e3o da valida\u00e7\u00e3o de token","text":"

\u00c9 crucial abordar um aspecto relacionado \u00e0 modifica\u00e7\u00e3o do router: o uso do par\u00e2metro prefix. Ao introduzir o prefixo, o endere\u00e7o do endpoint /token, respons\u00e1vel pela valida\u00e7\u00e3o do bearer token JWT, \u00e9 alterado para /auth/token. Esse caminho est\u00e1 explicitamente definido no OAuth2PasswordBearer dentro de security.py, resultando em uma refer\u00eancia ao caminho antigo /token, anterior \u00e0 cria\u00e7\u00e3o do router.

Esse problema fica evidente ao clicar no bot\u00e3o Authorize no Swagger:

Percebe-se que o caminho para a autoriza\u00e7\u00e3o est\u00e1 incorreto. Como consequ\u00eancia, ao tentar autenticar atrav\u00e9s do Swagger, nos deparamos com um erro na interface:

No entanto, o erro n\u00e3o \u00e9 suficientemente descritivo para identificarmos a origem do problema, retornando apenas uma mensagem gen\u00e9rica de Auth Error. Para compreender melhor o que ocorreu, \u00e9 necess\u00e1rio verificar o log produzido pelo uvicorn no terminal:

Erro mostrado no terminal
task serve\n# ...\nINFO:     127.0.0.1:40132 - \"POST /token HTTP/1.1\" 404 Not Found\n

A solu\u00e7\u00e3o para este problema \u00e9 relativamente simples. Precisamos ajustar o par\u00e2metro tokenUrl na OAuth2PasswordBearer para refletir as mudan\u00e7as feitas no router, direcionando para /auth/token. Faremos isso no arquivo security.py:

security.py
oauth2_scheme = OAuth2PasswordBearer(tokenUrl='auth/token')\n

Ap\u00f3s essa altera\u00e7\u00e3o, ao utilizar o Swagger, a autoriza\u00e7\u00e3o ser\u00e1 direcionada corretamente para o endpoint apropriado.

"},{"location":"07/#alteracao-no-teste-do-token","title":"Altera\u00e7\u00e3o no teste do token","text":"

Essa altera\u00e7\u00e3o far\u00e1 com que o teste referente a cria\u00e7\u00e3o do token tamb\u00e9m falhe. Pois ele procurar\u00e1 pelo endpoint /token. Devemos fazer a altera\u00e7\u00e3o para o novo caminho, que com a cria\u00e7\u00e3o de router, adiciona o prefixo /auth. Ficando assim:

tests/test_app.py
def test_get_token(client, user):\n    response = client.post(\n        '/auth/token',#(1)!\n        data={'username': user.email, 'password': user.clean_password},\n    )\n    token = response.json()\n\n    assert response.status_code == HTTPStatus.OK\n    assert 'access_token' in token\n    assert 'token_type' in token\n
  1. A \u00fanica altera\u00e7\u00e3o \u00e9 mesmo o endpoint!

Desta forma o teste espec\u00edfico do token poder\u00e1 passar corretamente. Mas, existem testes que dependem do token criado pela fixture.

"},{"location":"07/#alteracao-na-fixture-de-token","title":"Altera\u00e7\u00e3o na fixture de token","text":"

A altera\u00e7\u00e3o da fixture de token \u00e9 igual que fizemos em /tests/test_auth.py, precisamos somente corrigir o novo endere\u00e7o do router no arquivo /tests/conftest.py:

/tests/conftest.py
@pytest.fixture\ndef token(client, user):\n    response = client.post(\n        '/auth/token',\n        data={'username': user.email, 'password': user.clean_password},\n    )\n    return response.json()['access_token']\n

Fazendo assim com que os testes que dependem dessa fixture passem a funcionar.

Contudo, essas modifica\u00e7\u00f5es ainda n\u00e3o podem ser executadas, pois precisamos plugar os roteadores no aplicativo antes de executar.

"},{"location":"07/#plugando-as-rotas-em-app","title":"Plugando as rotas em app","text":"

O FastAPI oferece uma maneira f\u00e1cil e direta de incluir routers em nossa aplica\u00e7\u00e3o principal. Isso nos permite organizar nossos endpoints de maneira eficiente e manter nosso arquivo app.py focado apenas em suas responsabilidades principais.

Para incluir os routers em nossa aplica\u00e7\u00e3o principal, precisamos import\u00e1-los e usar a fun\u00e7\u00e3o include_router(). Aqui est\u00e1 como o nosso arquivo app.py fica depois de incluir os routers:

fast_zero/fast_zero/app.py
from http import HTTPStatus\n\nfrom fastapi import FastAPI\n\nfrom fast_zero.routers import auth, users\nfrom fast_zero.schemas import Message\n\napp = FastAPI()\n\napp.include_router(users.router)\napp.include_router(auth.router)\n\n\n@app.get('/', status_code=HTTPStatus.OK, response_model=Message)\ndef read_root():\n    return {'message': 'Ol\u00e1 Mundo!'}\n

Como voc\u00ea pode ver, nosso arquivo app.py \u00e9 muito mais simples agora. Ele agora delega as rotas para os respectivos routers, mantendo o foco em iniciar nossa aplica\u00e7\u00e3o FastAPI.

"},{"location":"07/#executando-os-testes","title":"Executando os testes","text":"

Ap\u00f3s refatorar nosso c\u00f3digo, \u00e9 crucial verificar se tudo continua funcionando como esperado. Para isso, executamos nossos testes novamente.

$ Execu\u00e7\u00e3o no terminal!
task test\n\n# ...\n\ntests/test_app.py::test_root_deve_retornar_ok_e_ola_mundo PASSED\ntests/test_app.py::test_create_user PASSED\ntests/test_app.py::test_read_users PASSED\ntests/test_app.py::test_read_users_with_users PASSED\ntests/test_app.py::test_update_user PASSED\ntests/test_app.py::test_delete_user PASSED\ntests/test_app.py::test_get_token PASSED\ntests/test_db.py::test_create_user PASSED\n

Como voc\u00ea pode ver, todos os testes passaram. Isso significa que as altera\u00e7\u00f5es que fizemos no nosso c\u00f3digo n\u00e3o afetaram o funcionamento do nosso aplicativo. O router manteve todos os endpoints nas mesmas rotas, garantindo a continuidade do comportamento esperado.

Agora, para melhor alinhar nossos testes com a nova estrutura do nosso c\u00f3digo, devemos reorganizar os arquivos de teste de acordo. Ou seja, tamb\u00e9m devemos criar arquivos de teste espec\u00edficos para cada router, em vez de manter todos os testes no arquivo tests/test_app.py. Essa estrutura facilitar\u00e1 a manuten\u00e7\u00e3o e compreens\u00e3o dos testes \u00e0 medida que nossa aplica\u00e7\u00e3o cresce.

"},{"location":"07/#reestruturando-os-arquivos-de-testes","title":"Reestruturando os arquivos de testes","text":"

Para acompanhar a nova estrutura routers, podemos desacoplar os testes do m\u00f3dulo test/test_app.py e criar arquivos de teste espec\u00edficos para cada um dos dom\u00ednios:

Vamos adaptar os testes para se encaixarem nessa nova estrutura.

"},{"location":"07/#ajustando-os-testes-para-auth","title":"Ajustando os testes para Auth","text":"

Come\u00e7aremos criando o arquivo /tests/test_auth.py. Esse arquivo ser\u00e1 respons\u00e1vel por testar todas as funcionalidades relacionadas \u00e0 autentica\u00e7\u00e3o do usu\u00e1rio.

/tests/test_auth.py
from http import HTTPStatus\n\n\ndef test_get_token(client, user):\n    response = client.post(\n        '/auth/token',\n        data={'username': user.email, 'password': user.clean_password},\n    )\n    token = response.json()\n\n    assert response.status_code == HTTPStatus.OK\n    assert 'access_token' in token\n    assert 'token_type' in token\n

\u00c9 importante notar que com a cria\u00e7\u00e3o do router usando prefix='/auth' devemos alterar o endpoint onde o request \u00e9 feito de '/token' para '/auth/token'. Fazendo com que a requisi\u00e7\u00e3o seja encaminhada para o lugar certo.

"},{"location":"07/#ajustando-os-testes-para-user","title":"Ajustando os testes para User","text":"

Em seguida, moveremos os testes relacionados ao dom\u00ednio do usu\u00e1rio para o arquivo /tests/test_users.py.

/tests/test_users.py
from http import HTTPStatus\n\nfrom fast_zero.schemas import UserPublic\n\n\ndef test_create_user(client):\n    response = client.post(\n        '/users/',\n        json={\n            'username': 'alice',\n            'email': 'alice@example.com',\n            'password': 'secret',\n        },\n    )\n    assert response.status_code == HTTPStatus.CREATED\n    assert response.json() == {\n        'username': 'alice',\n        'email': 'alice@example.com',\n        'id': 1,\n    }\n\n\ndef test_read_users(client):\n    response = client.get('/users')\n    assert response.status_code == HTTPStatus.OK\n    assert response.json() == {'users': []}\n\n\ndef test_read_users_with_users(client, user):\n    user_schema = UserPublic.model_validate(user).model_dump()\n    response = client.get('/users/')\n    assert response.json() == {'users': [user_schema]}\n\n\ndef test_update_user(client, user, token):\n    response = client.put(\n        f'/users/{user.id}',\n        headers={'Authorization': f'Bearer {token}'},\n        json={\n            'username': 'bob',\n            'email': 'bob@example.com',\n            'password': 'mynewpassword',\n        },\n    )\n    assert response.status_code == HTTPStatus.OK\n    assert response.json() == {\n        'username': 'bob',\n        'email': 'bob@example.com',\n        'id': 1,\n    }\n\n\ndef test_delete_user(client, user, token):\n    response = client.delete(\n        f'/users/{user.id}',\n        headers={'Authorization': f'Bearer {token}'},\n    )\n\n    assert response.status_code == HTTPStatus.OK\n    assert response.json() == {'message': 'User deleted'}\n

Para a constru\u00e7\u00e3o desse arquivo, nenhum teste foi modificado. Eles foram somente movidos para o dom\u00ednio espec\u00edfico do router. Importante, por\u00e9m, notar que alguns destes testes usam a fixture token para checar a autoriza\u00e7\u00e3o, como o endpoint do token foi alterado, devemos alterar a fixture de token para que esses testes continuem passando.

"},{"location":"07/#executando-os-testes_1","title":"Executando os testes","text":"

Ap\u00f3s essa reestrutura\u00e7\u00e3o, \u00e9 importante garantir que tudo continua funcionando corretamente. Executaremos os testes novamente para confirmar isso.

$ Execu\u00e7\u00e3o no terminal!
task test\n\n# ...\n\ntests/test_app.py::test_root_deve_retornar_ok_e_ola_mundo PASSED\ntests/test_auth.py::test_get_token PASSED\ntests/test_db.py::test_create_user PASSED\ntests/test_users.py::test_create_user PASSED\ntests/test_users.py::test_read_users PASSED\ntests/test_users.py::test_read_users_with_users PASSED\ntests/test_users.py::test_update_user PASSED\ntests/test_users.py::test_delete_user PASSED\n

Como podemos ver, todos os testes continuam passando com sucesso, mesmo ap\u00f3s terem sido movidos para arquivos diferentes. Isso \u00e9 uma confirma\u00e7\u00e3o de que nossa reestrutura\u00e7\u00e3o foi bem-sucedida e que nossa aplica\u00e7\u00e3o continua funcionando como esperado.

"},{"location":"07/#refinando-a-definicao-de-rotas-com-annotated","title":"Refinando a Defini\u00e7\u00e3o de Rotas com Annotated","text":"

O FastAPI suporta um recurso fascinante da biblioteca nativa typing, conhecido como Annotated. Esse recurso prova ser especialmente \u00fatil quando buscamos simplificar a utiliza\u00e7\u00e3o de depend\u00eancias.

Ao definir uma anota\u00e7\u00e3o de tipo, seguimos a seguinte formata\u00e7\u00e3o: nome_do_argumento: Tipo = Depends(o_que_dependemos). Em todos os endpoints, acrescentamos a inje\u00e7\u00e3o de depend\u00eancia da sess\u00e3o da seguinte forma:

session: Session = Depends(get_session)\n

O tipo Annotated nos permite combinar um tipo e os metadados associados a ele em uma \u00fanica defini\u00e7\u00e3o. Atrav\u00e9s da aplica\u00e7\u00e3o do FastAPI, podemos utilizar o Depends no campo dos metadados. Isso nos permite encapsular o tipo da vari\u00e1vel e o Depends em uma \u00fanica entidade, facilitando a defini\u00e7\u00e3o dos endpoints.

Veja o exemplo a seguir:

fast_zero/routers/users.py
from typing import Annotated\n\nSession = Annotated[Session, Depends(get_session)]\nCurrentUser = Annotated[User, Depends(get_current_user)]\n

Desse modo, conseguimos refinar a defini\u00e7\u00e3o dos endpoints para que se tornem mais concisos, sem alterar seu funcionamento:

fast_zero/routers/users.py
@router.post('/', status_code=HTTPStatus.CREATED, response_model=UserPublic)\ndef create_user(user: UserSchema, session: Session):\n# ...\n\n@router.get('/', response_model=UserList)\ndef read_users(session: Session, skip: int = 0, limit: int = 100):\n# ...\n\n@router.put('/{user_id}', response_model=UserPublic)\ndef update_user(\n    user_id: int,\n    user: UserSchema,\n    session: Session,\n    current_user: CurrentUser\n):\n# ...\n\n@router.delete('/{user_id}', response_model=Message)\ndef delete_user(user_id: int, session: Session, current_user: CurrentUser):\n# ...\n

Da mesma forma, podemos otimizar o roteador de autentica\u00e7\u00e3o:

fast_zero/routers/auth.py
from typing import Annotated\n\n# ...\n\nOAuth2Form = Annotated[OAuth2PasswordRequestForm, Depends()]\nSession = Annotated[Session, Depends(get_session)]\n\n@router.post('/token', response_model=Token)\ndef login_for_access_token(form_data: OAuth2Form, session: Session):\n#...\n

Atrav\u00e9s do uso de tipos Annotated, conseguimos reutilizar os mesmos consistentemente, reduzindo a repeti\u00e7\u00e3o de c\u00f3digo e aumentando a efici\u00eancia do nosso trabalho.

"},{"location":"07/#movendo-as-constantes-para-variaveis-de-ambiente","title":"Movendo as constantes para vari\u00e1veis de ambiente","text":"

Conforme mencionamos na aula sobre os 12 fatores, \u00e9 uma boa pr\u00e1tica manter as constantes que podem mudar dependendo do ambiente em vari\u00e1veis de ambiente. Isso torna o seu projeto mais seguro e modular, pois voc\u00ea pode alterar essas constantes sem ter que modificar o c\u00f3digo-fonte.

Por exemplo, temos estas constantes em nosso m\u00f3dulo security.py:

SECRET_KEY = 'your-secret-key'  # Isso \u00e9 provis\u00f3rio, vamos ajustar!\nALGORITHM = 'HS256'\nACCESS_TOKEN_EXPIRE_MINUTES = 30\n

Estes valores n\u00e3o devem estar diretamente no c\u00f3digo-fonte, ent\u00e3o vamos mov\u00ea-los para nossas vari\u00e1veis de ambiente e represent\u00e1-los na nossa classe Settings.

"},{"location":"07/#adicionando-as-constantes-a-settings","title":"Adicionando as constantes a Settings","text":"

J\u00e1 temos uma classe ideal para fazer isso em fast_zero/settings.py. Alteraremos essa classe para incluir estas constantes.

fast_zero/settings.py
from pydantic_settings import BaseSettings, SettingsConfigDict\n\n\nclass Settings(BaseSettings):\n    model_config = SettingsConfigDict(\n        env_file='.env', env_file_encoding='utf-8'\n    )\n\n    DATABASE_URL: str\n    SECRET_KEY: str\n    ALGORITHM: str\n    ACCESS_TOKEN_EXPIRE_MINUTES: int\n

Agora, precisamos adicionar estes valores ao nosso arquivo .env.

.env
DATABASE_URL=\"sqlite:///database.db\"\nSECRET_KEY=\"your-secret-key\"\nALGORITHM=\"HS256\"\nACCESS_TOKEN_EXPIRE_MINUTES=30\n

Com isso, podemos alterar o nosso c\u00f3digo em fast_zero/security.py para ler as constantes a partir da classe Settings.

"},{"location":"07/#removendo-as-constantes-do-codigo","title":"Removendo as constantes do c\u00f3digo","text":"

Primeiramente, carregaremos as configura\u00e7\u00f5es da classe Settings no in\u00edcio do m\u00f3dulo security.py.

fast_zero/security.py
from fast_zero.settings import Settings\n\nsettings = Settings()\n

Com isso, todos os lugares onde as constantes eram usadas devem ser substitu\u00eddos por settings.CONSTANTE. Por exemplo, na fun\u00e7\u00e3o create_access_token, alteraremos para usar as constantes da classe Settings:

fast_zero/security.py
def create_access_token(data: dict):\n    to_encode = data.copy()\n    expire = datetime.utcnow() + timedelta(\n        minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES\n    )\n    to_encode.update({'exp': expire})\n    encoded_jwt = encode(\n        to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM\n    )\n    return encoded_jwt\n

Desta forma, eliminamos todas as constantes do c\u00f3digo-fonte e passamos a usar as configura\u00e7\u00f5es a partir da classe Settings. Isso torna nosso c\u00f3digo mais seguro, pois as constantes sens\u00edveis, como a chave secreta, est\u00e3o agora seguras em nosso arquivo .env, e nosso c\u00f3digo fica mais modular, pois podemos facilmente alterar estas constantes simplesmente mudando os valores no arquivo .env. Al\u00e9m disso, essa abordagem facilita o gerenciamento de diferentes ambientes (como desenvolvimento, teste e produ\u00e7\u00e3o) pois cada ambiente pode ter seu pr\u00f3prio arquivo .env com suas configura\u00e7\u00f5es espec\u00edficas.

Precisamos alterar o teste para usar as mesmas vari\u00e1veis de ambiente do c\u00f3digo:

/tests/test_security.py
from http import HTTPStatus\n\nfrom jwt import decode\n\nfrom fast_zero.security import create_access_token, settings\n\n\ndef test_jwt():\n    data = {'test': 'test'}\n    token = create_access_token(data)\n\n    decoded = decode(\n        token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM]\n    )\n\n    assert decoded['test'] == data['test']\n    assert decoded['exp']\n
"},{"location":"07/#testando-se-tudo-funciona","title":"Testando se tudo funciona","text":"

Depois de todas essas mudan\u00e7as, \u00e9 muito importante garantir que tudo ainda est\u00e1 funcionando corretamente. Para isso, executaremos todos os testes que temos at\u00e9 agora.

$ Execu\u00e7\u00e3o no terminal!
task test\n\n# ...\n\ntests/test_app.py::test_root_deve_retornar_ok_e_ola_mundo PASSED\ntests/test_auth.py::test_get_token PASSED\ntests/test_db.py::test_create_user PASSED\ntests/test_users.py::test_create_user PASSED\ntests/test_users.py::test_read_users PASSED\ntests/test_users.py::test_read_users_with_users PASSED\ntests/test_users.py::test_update_user PASSED\ntests/test_users.py::test_delete_user PASSED\n

Se tudo estiver certo, todos os testes devem passar. Lembre-se de que a refatora\u00e7\u00e3o n\u00e3o deve alterar a funcionalidade do nosso c\u00f3digo - apenas torn\u00e1-lo mais f\u00e1cil de ler e manter.

"},{"location":"07/#commit","title":"Commit","text":"

Para finalizar, criaremos um commit para registrar todas as altera\u00e7\u00f5es que fizemos na nossa aplica\u00e7\u00e3o. Como essa \u00e9 uma grande mudan\u00e7a que envolve reestruturar a forma como lidamos com as rotas e mover as constantes para vari\u00e1veis de ambiente, podemos usar uma mensagem de commit descritiva que explique todas as principais altera\u00e7\u00f5es:

$ Execu\u00e7\u00e3o no terminal!
git add .\ngit commit -m \"Refatorando estrutura do projeto: Criado routers para Users e Auth; movido constantes para vari\u00e1veis de ambiente.\"\n
"},{"location":"07/#exercicio","title":"Exerc\u00edcio","text":"

Migre os endpoints e testes criados nos exerc\u00edcios anteriores para os locais corretos na nova estrutura da aplica\u00e7\u00e3o.

"},{"location":"07/#conclusao","title":"Conclus\u00e3o","text":"

Nesta aula, vimos como refatorar a estrutura do nosso projeto FastAPI para torn\u00e1-lo mais manuten\u00edvel. Organizamos nosso c\u00f3digo em diferentes arquivos e usamos o sistema de roteadores do FastAPI para separar diferentes partes da nossa API. Tamb\u00e9m mudamos algumas constantes para o arquivo de configura\u00e7\u00e3o, tornando nosso c\u00f3digo mais seguro e flex\u00edvel. Finalmente, atualizamos nossos testes para refletir a nova estrutura do projeto.

Refatorar \u00e9 um processo cont\u00ednuo - sempre h\u00e1 espa\u00e7o para melhorias. No entanto, com a estrutura que estabelecemos aqui, estamos em uma boa posi\u00e7\u00e3o para continuar a expandir nossa API no futuro.

Na pr\u00f3xima aula, exploraremos mais sobre autentica\u00e7\u00e3o e como gerenciar tokens de acesso e de atualiza\u00e7\u00e3o em nossa API FastAPI.

Agora que a aula acabou, \u00e9 um bom momento para voc\u00ea relembrar alguns conceitos e fixar melhor o conte\u00fado respondendo ao question\u00e1rio referente a ela.

Quiz

"},{"location":"08/","title":"Tornando o sistema de autentica\u00e7\u00e3o robusto","text":""},{"location":"08/#tornando-o-sistema-de-autenticacao-robusto","title":"Tornando o sistema de autentica\u00e7\u00e3o robusto","text":"

Objetivos da Aula:

Caso prefira ver a aula em v\u00eddeo

Esse aula ainda n\u00e3o est\u00e1 dispon\u00edvel em formato de v\u00eddeo, somente em texto ou live!

Aula Slides C\u00f3digo Quiz

Em nossas aulas anteriores, abordamos os fundamentos de um sistema de autentica\u00e7\u00e3o, mas existem diversas maneiras de aprimor\u00e1-lo para que ele se torne mais robusto e seguro. Nessa aula, enfrentaremos perguntas importantes, como: Como lidar com falhas e erros? Como garantir a seguran\u00e7a do sistema mesmo em cen\u00e1rios desafiadores? Estas s\u00e3o algumas das quest\u00f5es-chave que exploraremos.

Come\u00e7aremos com uma an\u00e1lise detalhada dos testes de autentica\u00e7\u00e3o. At\u00e9 agora, concentramo-nos em cen\u00e1rios ideais, onde o usu\u00e1rio existe e as condi\u00e7\u00f5es s\u00e3o favor\u00e1veis. Contudo, \u00e9 essencial testar tamb\u00e9m as situa\u00e7\u00f5es adversas e compreender como o sistema responde a falhas. Vamos, portanto, aprender a realizar testes eficazes para esses casos negativos.

Depois, passaremos para a implementa\u00e7\u00e3o de um elemento crucial em qualquer sistema de autentica\u00e7\u00e3o: a renova\u00e7\u00e3o do token. Esse mecanismo \u00e9 imprescind\u00edvel para manter a sess\u00e3o do usu\u00e1rio ativa e segura, mesmo quando o token original expira.

"},{"location":"08/#testes-para-autenticacao","title":"Testes para autentica\u00e7\u00e3o","text":"

Antes de mergulharmos nos testes, conversaremos um pouco sobre por que eles s\u00e3o t\u00e3o importantes. Na programa\u00e7\u00e3o, \u00e9 f\u00e1cil cair na armadilha de pensar que, se algo funciona na maioria das vezes, ent\u00e3o est\u00e1 tudo bem. Mas a verdade \u00e9 que \u00e9 nos casos marginais que os bugs mais dif\u00edceis de encontrar e corrigir costumam se esconder.

Por exemplo, o que acontece se tentarmos autenticar um usu\u00e1rio que n\u00e3o existe? Ou se tentarmos autenticar com as credenciais erradas? Se n\u00e3o testarmos esses cen\u00e1rios, podemos acabar com um sistema que parece funcionar na superf\u00edcie, mas que, na verdade, est\u00e1 cheio de falhas de seguran\u00e7a.

No c\u00f3digo apresentado, se observarmos atentamente, vemos que o erro HTTPException(status_code=HTTPStatus.FORBIDDEN, detail='Not enough permissions') em users.py na rota /{user_id} n\u00e3o est\u00e1 sendo coberto por nossos testes. Essa exce\u00e7\u00e3o \u00e9 lan\u00e7ada quando um usu\u00e1rio n\u00e3o autenticado ou um usu\u00e1rio sem permiss\u00f5es adequadas tenta acessar, ou alterar um recurso que n\u00e3o deveria.

Essa lacuna em nossos testes representa um risco potencial, pois n\u00e3o estamos verificando como nosso sistema se comporta quando algu\u00e9m tenta, por exemplo, alterar os detalhes de um usu\u00e1rio sem ter permiss\u00f5es adequadas. Embora possamos assumir que nosso sistema se comportar\u00e1 corretamente, a falta de testes nos deixa sem uma confirma\u00e7\u00e3o concreta.

"},{"location":"08/#testando-a-alteracao-de-um-usuario-nao-autorizado","title":"Testando a altera\u00e7\u00e3o de um usu\u00e1rio n\u00e3o autorizado","text":"

Agora, escreveremos alguns testes para esses casos. Vamos come\u00e7ar com um cen\u00e1rio simples: o que acontece quando um usu\u00e1rio tenta alterar as informa\u00e7\u00f5es de outro usu\u00e1rio?

Para testar isso, criaremos um novo teste chamado test_update_user_with_wrong_user.

tests/test_users.py
def test_update_user_with_wrong_user(client, user, token):\n    response = client.put(\n        f'/users/{user.id}',\n        headers={'Authorization': f'Bearer {token}'},\n        json={\n            'username': 'bob',\n            'email': 'bob@example.com',\n            'password': 'mynewpassword',\n        },\n    )\n    assert response.status_code == HTTPStatus.FORBIDDEN\n    assert response.json() == {'detail': 'Not enough permissions'}\n
Este teste vai simular um usu\u00e1rio tentando alterar as informa\u00e7\u00f5es de outro usu\u00e1rio. Se nosso sistema estiver funcionando corretamente, ele dever\u00e1 rejeitar essa tentativa e retornar um erro."},{"location":"08/#criando-modelos-por-demanda-com-factory-boy","title":"Criando modelos por demanda com factory-boy","text":"

Embora o teste que escrevemos esteja tecnicamente correto, ele ainda n\u00e3o funcionar\u00e1 adequadamente porque, atualmente, s\u00f3 temos um usu\u00e1rio em nosso banco de dados de testes. Precisamos de uma maneira de criar m\u00faltiplos usu\u00e1rios de teste facilmente, e \u00e9 a\u00ed que entra o factory-boy.

O factory-boy \u00e9 uma biblioteca que nos permite criar objetos de modelo de teste de forma r\u00e1pida e f\u00e1cil. Com ele, podemos criar uma \"f\u00e1brica\" de usu\u00e1rios que produzir\u00e1 novos objetos de usu\u00e1rio sempre que precisarmos. Isso nos permite criar m\u00faltiplos usu\u00e1rios de teste com facilidade, o que \u00e9 perfeito para nosso cen\u00e1rio atual.

Para come\u00e7ar, precisamos instalar o factory-boy em nosso ambiente de desenvolvimento:

$ Execu\u00e7\u00e3o no terminal!
poetry add --group dev factory-boy\n

Ap\u00f3s instalar o factory-boy, podemos criar uma UserFactory. Esta f\u00e1brica ser\u00e1 respons\u00e1vel por criar novos objetos de usu\u00e1rio sempre que precisarmos de um para nossos testes. A estrutura da f\u00e1brica ser\u00e1 a seguinte:

tests/conftest.py
import factory\n\n# ...\n\nclass UserFactory(factory.Factory):\n    class Meta:\n        model = User\n\n    username = factory.Sequence(lambda n: f'test{n}')\n    email = factory.LazyAttribute(lambda obj: f'{obj.username}@test.com')\n    password = factory.LazyAttribute(lambda obj: f'{obj.username}@example.com')\n

Explicando linha a linha, esse c\u00f3digo faz o seguinte:

Essa f\u00e1brica pode ser usada em testes para criar inst\u00e2ncias de User com dados predefinidos, facilitando a escrita de testes que requerem a presen\u00e7a de usu\u00e1rios no banco de dados. Isso \u00e9 extremamente \u00fatil ao escrever testes que requerem o estado pr\u00e9-configurado do banco de dados e ajuda a tornar os testes mais leg\u00edveis e manuten\u00edveis.

A seguir, podemos usar essa nova f\u00e1brica para criar m\u00faltiplos usu\u00e1rios de teste. Para fazer isso, modificamos nossa fixture de usu\u00e1rio existente para usar a UserFactory. Assim, sempre que executarmos nossos testes, teremos usu\u00e1rios diferentes dispon\u00edveis.

tests/conftest.py
@pytest.fixture\ndef user(session):\n    password = 'testtest'\n    user = UserFactory(password=get_password_hash(password))\n\n    session.add(user)\n    session.commit()\n    session.refresh(user)\n\n    user.clean_password = 'testtest'\n\n    return user\n\n\n@pytest.fixture\ndef other_user(session):\n    password = 'testtest'\n    user = UserFactory(password=get_password_hash(password))\n\n    session.add(user)\n    session.commit()\n    session.refresh(user)\n\n    user.clean_password = 'testtest'\n\n    return user\n

A cria\u00e7\u00e3o de outra fixture chamada other_user \u00e9 crucial para simular o cen\u00e1rio de um usu\u00e1rio tentando acessar ou modificar as informa\u00e7\u00f5es de outro usu\u00e1rio no sistema. Ao criar duas fixtures diferentes, user e other_user, podemos efetivamente simular dois usu\u00e1rios diferentes em nossos testes. Isso nos permite avaliar como nosso sistema reage quando um usu\u00e1rio tenta realizar uma a\u00e7\u00e3o n\u00e3o autorizada, como alterar as informa\u00e7\u00f5es de outro usu\u00e1rio.

Um aspecto interessante no uso das f\u00e1bricas \u00e9 que, sempre que forem chamadas, elas retornar\u00e3o um novo User, pois estamos fixando apenas a senha. Dessa forma, cada chamada a essa f\u00e1brica de usu\u00e1rios retornar\u00e1 um User diferente, com base nos atributos \"lazy\" que usamos.

Com essa nova configura\u00e7\u00e3o, podemos finalmente testar o cen\u00e1rio de um usu\u00e1rio tentando alterar as informa\u00e7\u00f5es de outro usu\u00e1rio. E como voc\u00ea pode ver, nossos testes passaram com sucesso, o que indica que nosso sistema est\u00e1 lidando corretamente com essa situa\u00e7\u00e3o.

tests/test_users.py
def test_update_user_with_wrong_user(client, other_user, token):\n    response = client.put(\n        f'/users/{other_user.id}',\n        headers={'Authorization': f'Bearer {token}'},\n        json={\n            'username': 'bob',\n            'email': 'bob@example.com',\n            'password': 'mynewpassword',\n        },\n    )\n    assert response.status_code == HTTPStatus.FORBIDDEN\n    assert response.json() == {'detail': 'Not enough permissions'}\n

Neste caso, n\u00e3o estamos usando a fixture user porque queremos simular um cen\u00e1rio onde o usu\u00e1rio associado ao token (autenticado) est\u00e1 tentando realizar uma a\u00e7\u00e3o sobre outro usu\u00e1rio, representado pela fixture other_user. Ao usar a other_user, garantimos que o id do usu\u00e1rio que estamos tentando modificar ou deletar n\u00e3o seja o mesmo do usu\u00e1rio associado ao token, mas que ainda assim exista no banco de dados.

Para enfatizar, a fixture user est\u00e1 sendo usada para representar o usu\u00e1rio que est\u00e1 autenticado atrav\u00e9s do token. Se us\u00e1ssemos a mesma fixture user neste teste, o sistema consideraria que a a\u00e7\u00e3o est\u00e1 sendo realizada pelo pr\u00f3prio usu\u00e1rio, o que n\u00e3o corresponderia ao cen\u00e1rio que queremos testar. Al\u00e9m disso, \u00e9 importante entender que o escopo das fixtures implica que, quando chamadas no mesmo teste, elas devem retornar o mesmo valor. Portanto, usar a user e other_user permite uma simula\u00e7\u00e3o mais precisa do comportamento desejado.

Com o teste implementado, vamos execut\u00e1-lo para ver se nosso sistema est\u00e1 protegido contra essa a\u00e7\u00e3o indevida:

$ Execu\u00e7\u00e3o no terminal!
task test\n\n# ...\n\ntests/test_app.py::test_root_deve_retornar_ok_e_ola_mundo PASSED\ntests/test_auth.py::test_get_token PASSED\ntests/test_db.py::test_create_user PASSED\ntests/test_users.py::test_create_user PASSED\ntests/test_users.py::test_read_users PASSED\ntests/test_users.py::test_read_users_with_users PASSED\ntests/test_users.py::test_update_user PASSED\ntests/test_users.py::test_update_user_with_wrong_user PASSED\ntests/test_users.py::test_delete_user PASSED\n

Se todos os testes passaram com sucesso, isso indica que nosso sistema est\u00e1 se comportando como esperado, inclusive no caso de tentativas indevidas de deletar um usu\u00e1rio.

"},{"location":"08/#testando-o-delete-com-o-usuario-errado","title":"Testando o DELETE com o usu\u00e1rio errado","text":"

Continuando nossos testes, agora testaremos o que acontece quando tentamos deletar um usu\u00e1rio com um usu\u00e1rio errado.

Talvez voc\u00ea esteja se perguntando, por que precisamos fazer isso? Bem, lembre-se de que a seguran\u00e7a \u00e9 uma parte crucial de qualquer sistema de autentica\u00e7\u00e3o. Precisamos garantir que um usu\u00e1rio n\u00e3o possa deletar a conta de outro usu\u00e1rio - apenas a pr\u00f3pria conta. Portanto, \u00e9 importante que testemos esse cen\u00e1rio para garantir que nosso sistema est\u00e1 seguro.

Aqui est\u00e1 o teste que usaremos:

tests/test_users.py
def test_delete_user_wrong_user(client, other_user, token):\n    response = client.delete(\n        f'/users/{other_user.id}',\n        headers={'Authorization': f'Bearer {token}'},\n    )\n    assert response.status_code == HTTPStatus.FORBIDDEN\n    assert response.json() == {'detail': 'Not enough permissions'}\n

Como voc\u00ea pode ver, esse teste tenta deletar o user de um id diferente usando o token do user. Se nosso sistema estiver funcionando corretamente, ele dever\u00e1 rejeitar essa tentativa e retornar um status 400 com uma mensagem de erro indicando que o usu\u00e1rio n\u00e3o tem permiss\u00f5es suficientes para realizar essa a\u00e7\u00e3o.

Vamos executar esse teste agora e ver o que acontece:

$ Execu\u00e7\u00e3o no terminal!
task test\n\n# ...\n\ntests/test_users.py::test_delete_user_wrong_user PASSED\n

\u00d3timo, nosso teste passou! Isso significa que nosso sistema est\u00e1 corretamente impedindo um usu\u00e1rio de deletar a conta de outro usu\u00e1rio.

Agora que terminamos de testar a autoriza\u00e7\u00e3o, passaremos para o pr\u00f3ximo desafio: testar tokens expirados. Lembre-se, em um sistema de autentica\u00e7\u00e3o robusto, um token deve expirar ap\u00f3s um certo per\u00edodo de tempo por motivos de seguran\u00e7a. Portanto, \u00e9 importante que testemos o que acontece quando tentamos usar um token expirado. Veremos isso na pr\u00f3xima se\u00e7\u00e3o.

"},{"location":"08/#testando-a-expiracao-do-token","title":"Testando a expira\u00e7\u00e3o do token","text":"

Continuando com nossos testes de autentica\u00e7\u00e3o, a pr\u00f3xima coisa que precisamos testar \u00e9 a expira\u00e7\u00e3o do token. Tokens de autentica\u00e7\u00e3o s\u00e3o normalmente projetados para expirar ap\u00f3s um certo per\u00edodo de tempo por motivos de seguran\u00e7a. Isso evita que algu\u00e9m que tenha obtido um token possa us\u00e1-lo indefinidamente se ele for roubado ou perdido. Portanto, \u00e9 importante que verifiquemos que nosso sistema esteja tratando corretamente a expira\u00e7\u00e3o dos tokens.

Para realizar esse teste, usaremos uma biblioteca chamada freezegun. freezegun\u00e9 uma biblioteca Python que nos permite \"congelar\" o tempo em um ponto espec\u00edfico ou avan\u00e7\u00e1-lo conforme necess\u00e1rio durante os testes. Isso \u00e9 especialmente \u00fatil para testar funcionalidades sens\u00edveis ao tempo, como a expira\u00e7\u00e3o de tokens, sem ter que esperar em tempo real.

Primeiro, precisamos instalar a biblioteca:

poetry add --group dev freezegun\n

Agora criaremos nosso teste. Come\u00e7aremos pegando um token para um usu\u00e1rio, congelando o tempo, esperando pelo tempo de expira\u00e7\u00e3o do token e, em seguida, tentando usar o token para acessar um endpoint que requer autentica\u00e7\u00e3o.

Ao elaborarmos o teste, usaremos a funcionalidade de congelamento de tempo do freezegun. O objetivo \u00e9 simular a cria\u00e7\u00e3o de um token \u00e0s 12:00 e, em seguida, verificar sua expira\u00e7\u00e3o \u00e0s 12:31. Neste cen\u00e1rio, estamos utilizando o conceito de \"viajar no tempo\" para al\u00e9m do per\u00edodo de validade do token, garantindo que a tentativa subsequente de utiliz\u00e1-lo resultar\u00e1 em um erro de autentica\u00e7\u00e3o.

tests/test_auth.py
from freezegun import freeze_time\n\n# ...\n\ndef test_token_expired_after_time(client, user):\n    with freeze_time('2023-07-14 12:00:00'):\n        response = client.post(\n            '/auth/token',\n            data={'username': user.email, 'password': user.clean_password},\n        )\n        assert response.status_code == HTTPStatus.OK\n        token = response.json()['access_token']\n\n    with freeze_time('2023-07-14 12:31:00'):\n        response = client.put(\n            f'/users/{user.id}',\n            headers={'Authorization': f'Bearer {token}'},\n            json={\n                'username': 'wrongwrong',\n                'email': 'wrong@wrong.com',\n                'password': 'wrong',\n            },\n        )\n        assert response.status_code == HTTPStatus.UNAUTHORIZED\n        assert response.json() == {'detail': 'Could not validate credentials'}\n

Lembre-se de que configuramos nosso token para expirar ap\u00f3s 30 minutos. Portanto, n\u00f3s avan\u00e7amos o tempo em 31 minutos para garantir que o token tenha expirado.

Agora, executaremos nosso teste e ver o que acontece:

$ Execu\u00e7\u00e3o no terminal!
task test\n\n# ...\n\ntests/test_auth.py::test_token_expired_after_time FAILED\n

O motivo deste teste falhar \u00e9 que n\u00e3o temos uma condi\u00e7\u00e3o que fa\u00e7a a captura deste cen\u00e1rio. Em fast_zero/security.py, o \u00fanico caso de erro esperado \u00e9 em rela\u00e7\u00e3o \u00e0 dados inv\u00e1lidos durante o decode (DecodeError). Precisamos adicionar uma exception para quando o token estiver no formato correto, mas n\u00e3o estiver no prazo de validade. Quando seu tempo de uso j\u00e1 est\u00e1 expirado.

Para isso, podemos importar do pyjwt o objeto ExpiredSignatureError que \u00e9 a exce\u00e7\u00e3o levantada para a decodifica\u00e7\u00e3o de um c\u00f3digo expirado:

fast_zero/security.py
from datetime import datetime, timedelta\nfrom http import HTTPStatus\n\nfrom fastapi import Depends, HTTPException\nfrom fastapi.security import OAuth2PasswordBearer\nfrom jwt import DecodeError, ExpiredSignatureError, decode, encode\nfrom pwdlib import PasswordHash\nfrom sqlalchemy import select\nfrom sqlalchemy.orm import Session\n\n# ...\n\ndef get_current_user(\n    session: Session = Depends(get_session),\n    token: str = Depends(oauth2_scheme),\n):\n    credentials_exception = HTTPException(\n        status_code=HTTPStatus.UNAUTHORIZED,\n        detail='Could not validate credentials',\n        headers={'WWW-Authenticate': 'Bearer'},\n    )\n\n    try:\n        payload = decode(\n            token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM]\n        )\n        username: str = payload.get('sub')\n        if not username:\n            raise credentials_exception\n        token_data = TokenData(username=username)\n    except DecodeError:\n        raise credentials_exception\n    except ExpiredSignatureError:\n        raise credentials_exception\n\n    user = session.scalar(\n        select(User).where(User.email == token_data.username)\n    )\n\n    if not user:\n        raise credentials_exception\n\n    return user\n

Com essa simples altera\u00e7\u00e3o, temos uma sa\u00edda de erro para quando o token estiver inv\u00e1lido. Por quest\u00f5es de seguran\u00e7a, vamos manter a mesma mensagem usada antes. Dizendo que n\u00e3o conseguimos validar as credenciais.

Assim, vamos executar o teste novamente:

$ Execu\u00e7\u00e3o no terminal!
task test\n\n# ...\n\ntests/test_auth.py::test_token_expired_after_time PASSED\n

Temos a demonstra\u00e7\u00e3o de sucesso.

No entanto, ainda h\u00e1 uma coisa que precisamos implementar: a atualiza\u00e7\u00e3o de tokens. Atualmente, quando um token expira, o usu\u00e1rio teria que fazer login novamente para obter um novo token. Isso n\u00e3o \u00e9 uma \u00f3tima experi\u00eancia para o usu\u00e1rio. Em vez disso, gostar\u00edamos de oferecer a possibilidade de o usu\u00e1rio atualizar seu token quando ele estiver prestes a expirar. Veremos como fazer isso na pr\u00f3xima se\u00e7\u00e3o.

"},{"location":"08/#testando-o-usuario-nao-existente-e-senha-incorreta","title":"Testando o usu\u00e1rio n\u00e3o existente e senha incorreta","text":"

Na constru\u00e7\u00e3o de qualquer sistema de autentica\u00e7\u00e3o, \u00e9 crucial garantir que os casos de erro sejam tratados corretamente. Isso n\u00e3o s\u00f3 previne poss\u00edveis falhas de seguran\u00e7a, mas tamb\u00e9m permite fornecer feedback \u00fatil aos usu\u00e1rios.

Em nossa implementa\u00e7\u00e3o atual, temos duas situa\u00e7\u00f5es espec\u00edficas que devem retornar um erro: quando um usu\u00e1rio inexistente tenta fazer login e quando uma senha incorreta \u00e9 fornecida. Abordaremos esses casos de erro em nossos pr\u00f3ximos testes.

Embora possa parecer redundante testar esses casos j\u00e1 que ambos resultam no mesmo erro, \u00e9 importante verificar que ambos os cen\u00e1rios est\u00e3o corretamente tratados. Isso nos permitir\u00e1 manter a robustez do nosso sistema conforme ele evolui e muda ao longo do tempo.

"},{"location":"08/#testando-a-excecao-para-um-usuario-inexistente","title":"Testando a exce\u00e7\u00e3o para um usu\u00e1rio inexistente","text":"

Para este cen\u00e1rio, precisamos enviar um request para o endpoint de token com um e-mail que n\u00e3o existe no banco de dados. A resposta esperada \u00e9 um HTTP 400 com a mensagem de detalhe 'Incorrect email or password'.

tests/test_auth.py
def test_token_inexistent_user(client):\n    response = client.post(\n        '/auth/token',\n        data={'username': 'no_user@no_domain.com', 'password': 'testtest'},\n    )\n    assert response.status_code == HTTPStatus.BAD_REQUEST\n    assert response.json() == {'detail': 'Incorrect email or password'}\n
"},{"location":"08/#testando-a-excecao-para-uma-senha-incorreta","title":"Testando a exce\u00e7\u00e3o para uma senha incorreta","text":"

Aqui, precisamos enviar um request para o endpoint de token com uma senha incorreta para um usu\u00e1rio existente. A resposta esperada \u00e9 um HTTP 400 com a mensagem de detalhe 'Incorrect email or password'.

tests/test_auth.py
def test_token_wrong_password(client, user):\n    response = client.post(\n        '/auth/token',\n        data={'username': user.email, 'password': 'wrong_password'}\n    )\n    assert response.status_code == HTTPStatus.BAD_REQUEST\n    assert response.json() == {'detail': 'Incorrect email or password'}\n

Com esses testes, garantimos que nossas exce\u00e7\u00f5es est\u00e3o sendo lan\u00e7adas corretamente. Essa \u00e9 uma parte importante da constru\u00e7\u00e3o de um sistema de autentica\u00e7\u00e3o robusto, pois nos permite ter confian\u00e7a de que estamos tratando corretamente os casos de erro.

"},{"location":"08/#implementando-o-refresh-do-token","title":"Implementando o refresh do token","text":"

O processo de renova\u00e7\u00e3o de token \u00e9 uma parte essencial na implementa\u00e7\u00e3o de autentica\u00e7\u00e3o JWT. Em muitos sistemas, por raz\u00f5es de seguran\u00e7a, os tokens de acesso t\u00eam um tempo de vida relativamente curto. Isso significa que eles expiram ap\u00f3s um determinado per\u00edodo de tempo, e quando isso acontece, o cliente precisa obter um novo token para continuar acessando os recursos do servidor. Aqui \u00e9 onde o processo de renova\u00e7\u00e3o de token entra: permite que um cliente obtenha um novo token de acesso sem a necessidade de autentica\u00e7\u00e3o completa (por exemplo, sem ter que fornecer novamente o nome de usu\u00e1rio e senha).

Agora implementaremos a fun\u00e7\u00e3o de renova\u00e7\u00e3o de token em nosso c\u00f3digo.

fast_zero/routes/auth.py
from fast_zero.security import (\n    create_access_token,\n    get_current_user,\n    verify_password,\n)\n\n# ...\n\n@router.post('/refresh_token', response_model=Token)\ndef refresh_access_token(\n    user: User = Depends(get_current_user),\n):\n    new_access_token = create_access_token(data={'sub': user.email})\n\n    return {'access_token': new_access_token, 'token_type': 'bearer'}\n

Implementaremos tamb\u00e9m um teste para verificar se a fun\u00e7\u00e3o de renova\u00e7\u00e3o de token est\u00e1 funcionando corretamente.

tests/test_auth.py
def test_refresh_token(client, user, token):\n    response = client.post(\n        '/auth/refresh_token',\n        headers={'Authorization': f'Bearer {token}'},\n    )\n\n    data = response.json()\n\n    assert response.status_code == HTTPStatus.OK\n    assert 'access_token' in data\n    assert 'token_type' in data\n    assert data['token_type'] == 'bearer'\n

Ainda \u00e9 importante garantir que nosso sistema trate corretamente os tokens expirados. Para isso, adicionaremos um teste que verifica se um token expirado n\u00e3o pode ser usado para renovar um token.

tests/test_auth.py
def test_token_expired_dont_refresh(client, user):\n    with freeze_time('2023-07-14 12:00:00'):\n        response = client.post(\n            '/auth/token',\n            data={'username': user.email, 'password': user.clean_password},\n        )\n        assert response.status_code == HTTPStatus.OK\n        token = response.json()['access_token']\n\n    with freeze_time('2023-07-14 12:31:00'):\n        response = client.post(\n            '/auth/refresh_token',\n            headers={'Authorization': f'Bearer {token}'},\n        )\n        assert response.status_code == HTTPStatus.UNAUTHORIZED\n        assert response.json() == {'detail': 'Could not validate credentials'}\n

Agora, se executarmos nossos testes, todos eles devem passar, incluindo os novos testes que acabamos de adicionar.

$ Execu\u00e7\u00e3o no terminal!
task test\n\n# ...\n\ntests/test_app.py::test_root_deve_retornar_ok_e_ola_mundo PASSED\ntests/test_auth.py::test_get_token PASSED\ntests/test_auth.py::test_token_inexistent_user PASSED\ntests/test_auth.py::test_token_wrong_password PASSED\ntests/test_auth.py::test_refresh_token PASSED\ntests/test_auth.py::test_token_expired_after_time PASSED\ntests/test_db.py::test_create_user PASSED\ntests/test_users.py::test_create_user PASSED\ntests/test_users.py::test_read_users PASSED\ntests/test_users.py::test_read_users_with_users PASSED\ntests/test_users.py::test_update_user PASSED\ntests/test_users.py::test_update_user_with_wrong_user PASSED\ntests/test_users.py::test_delete_user PASSED\ntests/test_users.py::test_delete_user_wrong_user PASSED\ntests/test_users.py::test_token_expired_dont_refresh PASSED\n

Com esses testes, podemos ter certeza de que cobrimos alguns casos importantes relacionados \u00e0 autentica\u00e7\u00e3o de usu\u00e1rios em nossa API.

"},{"location":"08/#commit","title":"Commit","text":"

Agora, faremos um commit com as altera\u00e7\u00f5es que fizemos.

$ Execu\u00e7\u00e3o no terminal!
git add .\ngit commit -m \"Implementando o refresh do token e testes de autoriza\u00e7\u00e3o\"\n
"},{"location":"08/#conclusao","title":"Conclus\u00e3o","text":"

Nesta aula, abordamos uma grande quantidade de t\u00f3picos cruciais para a constru\u00e7\u00e3o de uma aplica\u00e7\u00e3o web segura e robusta. Come\u00e7amos com a implementa\u00e7\u00e3o da funcionalidade de renova\u00e7\u00e3o do token JWT, uma pe\u00e7a fundamental na arquitetura de autentica\u00e7\u00e3o baseada em token. Este processo garante que os usu\u00e1rios possam continuar acessando a aplica\u00e7\u00e3o, mesmo ap\u00f3s o token inicial ter expirado, sem a necessidade de fornecer suas credenciais novamente.

Por\u00e9m, a implementa\u00e7\u00e3o do c\u00f3digo foi apenas a primeira parte do que fizemos. Uma parte significativa da nossa aula foi dedicada a testar de maneira exaustiva a nossa aplica\u00e7\u00e3o. Escrevemos testes para verificar o comportamento b\u00e1sico das nossas rotas de autentica\u00e7\u00e3o, mas n\u00e3o paramos por a\u00ed. Tamb\u00e9m consideramos v\u00e1rios casos de borda que podem surgir durante a autentica\u00e7\u00e3o de um usu\u00e1rio.

Testamos, por exemplo, o que acontece quando se tenta obter um token com credenciais incorretas. Verificamos o comportamento da nossa aplica\u00e7\u00e3o quando um token expirado \u00e9 utilizado. Esses testes nos ajudam a garantir que nossa aplica\u00e7\u00e3o se comporte de maneira adequada n\u00e3o apenas nas situa\u00e7\u00f5es mais comuns, mas tamb\u00e9m quando algo sai do esperado.

Al\u00e9m disso, ao implementar esses testes, n\u00f3s garantimos que futuras altera\u00e7\u00f5es no nosso c\u00f3digo n\u00e3o ir\u00e3o quebrar funcionalidades j\u00e1 existentes. Testes automatizados s\u00e3o uma parte fundamental de qualquer aplica\u00e7\u00e3o de alta qualidade, e o que fizemos nesta aula vai al\u00e9m do b\u00e1sico, mostrando como lidar com cen\u00e1rios complexos e realistas. Nos aproximando do ambiente de produ\u00e7\u00e3o.

Na pr\u00f3xima aula, utilizaremos a infraestrutura de autentica\u00e7\u00e3o que criamos aqui para permitir que os usu\u00e1rios criem, leiam, atualizem e deletem suas pr\u00f3prias listas de tarefas. Isso vai nos permitir explorar ainda mais as funcionalidades do FastAPI e do SQLAlchemy, al\u00e9m de continuar a expandir a nossa su\u00edte de testes.

Agora que a aula acabou, \u00e9 um bom momento para voc\u00ea relembrar alguns conceitos e fixar melhor o conte\u00fado respondendo ao question\u00e1rio referente a ela.

Quiz

"},{"location":"09/","title":"Criando Rotas CRUD para Gerenciamento de Tarefas em FastAPI","text":""},{"location":"09/#criando-rotas-crud-para-gerenciamento-de-tarefas-em-fastapi","title":"Criando Rotas CRUD para Gerenciamento de Tarefas em FastAPI","text":"

Objetivos da Aula:

Caso prefira ver a aula em v\u00eddeo

Esse aula ainda n\u00e3o est\u00e1 dispon\u00edvel em formato de v\u00eddeo, somente em texto ou live!

Aula Slides C\u00f3digo Quiz

Ap\u00f3s termos cumprido todos os passos essenciais para estabelecer um sistema eficiente de gerenciamento de usu\u00e1rios, estamos agora preparados para levar nossa aplica\u00e7\u00e3o a um novo momento, introduzindo um sistema de gerenciamento de tarefas, mais conhecido como todo list. Nesta nova etapa, garantiremos que somente o usu\u00e1rio que criou uma tarefa tenha o direito de acessar e editar tal tarefa, refor\u00e7ando a seguran\u00e7a e a privacidade dos dados. Para isso, desenvolveremos diversos endpoints e implementaremos as medidas de restri\u00e7\u00e3o e autentica\u00e7\u00e3o que aprimoramos na \u00faltima aula.

"},{"location":"09/#estrutura-inicial-do-codigo","title":"Estrutura inicial do c\u00f3digo","text":"

Primeiro, criaremos um novo arquivo chamado todos.py no diret\u00f3rio de routers:

fast_zero/routers/todos.py
from fastapi import APIRouter\n\nrouter = APIRouter(prefix='/todos', tags=['todos'])\n

Neste c\u00f3digo, criamos uma nova inst\u00e2ncia da classe APIRouter do FastAPI. Esta classe \u00e9 usada para definir as rotas de nossa aplica\u00e7\u00e3o. A inst\u00e2ncia router funcionar\u00e1 como um mini aplicativo FastAPI, que poder\u00e1 ter suas pr\u00f3prias rotas, modelos de resposta, etc.

A op\u00e7\u00e3o prefix no construtor do APIRouter \u00e9 usada para definir um prefixo comum para todas as rotas definidas neste roteador. Isso significa que todas as rotas que definirmos neste roteador come\u00e7ar\u00e3o com /todos. Usamos um prefixo aqui porque queremos agrupar todas as rotas relacionadas a tarefas em um lugar. Isso torna nossa aplica\u00e7\u00e3o mais organizada e f\u00e1cil de entender.

A op\u00e7\u00e3o tags \u00e9 usada para agrupar as rotas em se\u00e7\u00f5es no documento interativo de API gerado pelo FastAPI (como Swagger UI e ReDoc). Todas as rotas que definirmos neste roteador aparecer\u00e3o na se\u00e7\u00e3o \"todos\" da documenta\u00e7\u00e3o da API.

Ap\u00f3s definir o roteador, precisamos inclu\u00ed-lo em nossa aplica\u00e7\u00e3o principal. Atualizaremos o arquivo fast_zero/app.py para incluir as rotas de tarefas que criaremos:

fast_zero/app.py
from http import HTTPStatus\n\nfrom fastapi import FastAPI\n\nfrom fast_zero.routers import auth, todos, users\nfrom fast_zero.schemas import Message\n\napp = FastAPI()\n\napp.include_router(users.router)\napp.include_router(auth.router)\napp.include_router(todos.router)\n\n\n@app.get('/', status_code=HTTPStatus.OK, response_model=Message)\ndef read_root():\n    return {'message': 'Ol\u00e1 Mundo!'}\n

Neste c\u00f3digo, chamamos o m\u00e9todo include_router do FastAPI para cada roteador que definimos. Este m\u00e9todo adiciona todas as rotas do roteador \u00e0 nossa aplica\u00e7\u00e3o. Com isso, nossa aplica\u00e7\u00e3o agora ter\u00e1 todas as rotas definidas nos roteadores users, auth e todos.

"},{"location":"09/#implementacao-da-tabela-no-banco-de-dados","title":"Implementa\u00e7\u00e3o da tabela no Banco de dados","text":"

Agora, implementaremos a tabela 'Todos' no nosso banco de dados. Esta tabela estar\u00e1 diretamente relacionada \u00e0 tabela 'User', pois toda tarefa pertence a um usu\u00e1rio. Esta rela\u00e7\u00e3o \u00e9 crucial para garantir que s\u00f3 o usu\u00e1rio dono da tarefa possa acessar e modificar suas tarefas.

fast_zero/models.py
from datetime import datetime\nfrom enum import Enum\n\nfrom sqlalchemy import ForeignKey, func\nfrom sqlalchemy.orm import Mapped, mapped_column, registry, relationship\n\ntable_registry = registry()\n\n\nclass TodoState(str, Enum):\n    draft = 'draft'\n    todo = 'todo'\n    doing = 'doing'\n    done = 'done'\n    trash = 'trash'\n\n\n@table_registry.mapped_as_dataclass\nclass User:\n    __tablename__ = 'users'\n\n    id: Mapped[int] = mapped_column(init=False, primary_key=True)\n    username: Mapped[str] = mapped_column(unique=True)\n    password: Mapped[str]\n    email: Mapped[str] = mapped_column(unique=True)\n    created_at: Mapped[datetime] = mapped_column(\n        init=False, server_default=func.now()\n    )\n\n    todos: Mapped[list['Todo']] = relationship(\n        init=False, back_populates='user', cascade='all, delete-orphan'\n    )\n\n\n@table_registry.mapped_as_dataclass\nclass Todo:\n    __tablename__ = 'todos'\n\n    id: Mapped[int] = mapped_column(init=False, primary_key=True)\n    title: Mapped[str]\n    description: Mapped[str]\n    state: Mapped[TodoState]\n\n    user_id: Mapped[int] = mapped_column(ForeignKey('users.id'))\n\n    user: Mapped[User] = relationship(init=False, back_populates='todos')\n

Neste ponto, \u00e9 importante compreender o conceito de relationship em SQLAlchemy. A fun\u00e7\u00e3o relationship define como as duas tabelas ir\u00e3o interagir. O argumento back_populates permite uma associa\u00e7\u00e3o bidirecional entre as tabelas, ou seja, se tivermos um usu\u00e1rio, podemos acessar suas tarefas atrav\u00e9s do atributo 'todos', e se tivermos uma tarefa, podemos encontrar o usu\u00e1rio a que ela pertence atrav\u00e9s do atributo 'user'. O argumento cascade determina o que ocorre com as tarefas quando o usu\u00e1rio associado a elas \u00e9 deletado. Ao definir 'all, delete-orphan', estamos instruindo o SQLAlchemy a deletar todas as tarefas de um usu\u00e1rio quando este for deletado.

O uso do tipo Enum em state: Mapped[TodoState] \u00e9 outro ponto importante. Enum \u00e9 um tipo de dado especial que permite a cria\u00e7\u00e3o de um conjunto fixo de constantes. Neste caso, estamos utilizando para definir os poss\u00edveis estados de uma tarefa.

Estes conceitos podem parecer um pouco complexos agora, mas ficar\u00e3o mais claros quando come\u00e7armos a implementar os testes.

"},{"location":"09/#testando-as-novas-implementacoes-do-banco-de-dados","title":"Testando as novas implementa\u00e7\u00f5es do banco de dados","text":"

Embora tenhamos 100% de cobertura de c\u00f3digo, isso n\u00e3o garante que tudo esteja funcionando corretamente. S\u00f3 implementamos a estrutura do banco de dados, mas n\u00e3o testamos a l\u00f3gica de como as tabelas e as rela\u00e7\u00f5es funcionam na pr\u00e1tica.

Para isso, criamos um teste para verificar se a rela\u00e7\u00e3o entre tarefas e usu\u00e1rios est\u00e1 funcionando corretamente. Este teste cria uma nova tarefa para um usu\u00e1rio e verifica se essa tarefa aparece na lista de tarefas desse usu\u00e1rio.

tests/test_db.py
from fast_zero.models import Todo, User\n# ...\ndef test_create_todo(session, user: User):\n    todo = Todo(\n        title='Test Todo',\n        description='Test Desc',\n        state='draft',\n        user_id=user.id,\n    )\n\n    session.add(todo)\n    session.commit()\n    session.refresh(todo)\n\n    user = session.scalar(select(User).where(User.id == user.id))\n\n    assert todo in user.todos\n

Com isso, voc\u00ea pode executar os testes:

$ Execu\u00e7\u00e3o no terminal!
task test tests/test_db.py\n# ...\ntests/test_db.py::test_create_user_without_todos PASSED\ntests/test_db.py::test_create_todo PASSED\n

Isso mostra que os testes foram bem-sucedidos. Mesmo sem testes mais extensivos, agora come\u00e7aremos a criar os esquemas para esse modelo e, em seguida, os endpoints.

"},{"location":"09/#schemas-para-todos","title":"Schemas para Todos","text":"

Criaremos dois esquemas para nosso modelo de tarefas (todos): TodoSchema e TodoPublic.

fast_zero/schemas.py
from fast_zero.models import TodoState\n\n#...\n\nclass TodoSchema(BaseModel):\n    title: str\n    description: str\n    state: TodoState\n\n\nclass TodoPublic(TodoSchema):\n    id: int\n\n\nclass TodoList(BaseModel):\n    todos: list[TodoPublic]\n

TodoSchema ser\u00e1 usado para validar os dados de entrada quando uma nova tarefa \u00e9 criada e TodoPublic ser\u00e1 usado para validar os dados de sa\u00edda quando uma tarefa \u00e9 retornada em um endpoint.

"},{"location":"09/#endpoint-de-criacao","title":"Endpoint de cria\u00e7\u00e3o","text":"

Criamos o primeiro endpoint para a cria\u00e7\u00e3o de tarefas. Este \u00e9 um endpoint POST na rota '/todos'. \u00c9 importante destacar que, para criar uma tarefa, um usu\u00e1rio precisa estar autenticado e s\u00f3 esse usu\u00e1rio autenticado ser\u00e1 o propriet\u00e1rio da tarefa.

fast_zero/routers/todos.py
from typing import Annotated\n\nfrom fastapi import APIRouter, Depends\nfrom sqlalchemy.orm import Session\n\nfrom fast_zero.database import get_session\nfrom fast_zero.models import Todo, User\nfrom fast_zero.schemas import TodoPublic, TodoSchema\nfrom fast_zero.security import get_current_user\n\nrouter = APIRouter()\n\nSession = Annotated[Session, Depends(get_session)]\nCurrentUser = Annotated[User, Depends(get_current_user)]\n\nrouter = APIRouter(prefix='/todos', tags=['todos'])\n\n\n@router.post('/', response_model=TodoPublic)\ndef create_todo(\n    todo: TodoSchema,\n    user: CurrentUser,\n    session: Session,\n):\n    db_todo = Todo(\n        title=todo.title,\n        description=todo.description,\n        state=todo.state,\n        user_id=user.id,\n    )\n    session.add(db_todo)\n    session.commit()\n    session.refresh(db_todo)\n\n    return db_todo\n

Neste endpoint, fazemos uso da depend\u00eancia get_current_user que garante que somente usu\u00e1rios autenticados possam criar tarefas, protegendo assim nossa aplica\u00e7\u00e3o.

"},{"location":"09/#testando-o-endpoint-de-criacao","title":"Testando o endpoint de cria\u00e7\u00e3o","text":"

Para garantir que nosso endpoint est\u00e1 funcionando corretamente, criamos um teste para ele. Este teste verifica se o endpoint '/todos' est\u00e1 criando tarefas corretamente.

tests/test_todos.py
def test_create_todo(client, token):\n    response = client.post(\n        '/todos/',\n        headers={'Authorization': f'Bearer {token}'},\n        json={\n            'title': 'Test todo',\n            'description': 'Test todo description',\n            'state': 'draft',\n        },\n    )\n    assert response.json() == {\n        'id': 1,\n        'title': 'Test todo',\n        'description': 'Test todo description',\n        'state': 'draft',\n    }\n

No teste, fazemos uma requisi\u00e7\u00e3o POST para o endpoint '/todos' passando um token de autentica\u00e7\u00e3o v\u00e1lido e um JSON com os dados da tarefa a ser criada. Em seguida, verificamos se a resposta cont\u00e9m os dados corretos da tarefa criada.

Para executar este teste, voc\u00ea deve usar o comando abaixo no terminal:

$ Execu\u00e7\u00e3o no terminal!
task test tests/test_todos.py\n# ...\ntests/test_todos.py::test_create_todo PASSED\n

Com essa implementa\u00e7\u00e3o, os testes devem passar. Por\u00e9m, apesar do sucesso dos testes, nosso c\u00f3digo ainda n\u00e3o est\u00e1 completamente pronto. Ainda \u00e9 necess\u00e1rio criar uma migra\u00e7\u00e3o para a tabela de tarefas no banco de dados.

"},{"location":"09/#criando-a-migracao-da-nova-tabela","title":"Criando a migra\u00e7\u00e3o da nova tabela","text":"

Agora que temos nosso modelo de tarefas definido, precisamos criar uma migra\u00e7\u00e3o para adicionar a tabela de tarefas ao nosso banco de dados. Usamos o Alembic para criar e gerenciar nossas migra\u00e7\u00f5es.

$ Execu\u00e7\u00e3o no terminal!
alembic revision --autogenerate -m \"create todos table\"\n\n# ...\n\nGenerating /<caminho>/fast_zero/migrations/versions/de865434f506_create_todos_table.py\n

Este comando gera um arquivo de migra\u00e7\u00e3o, que se parece com o c\u00f3digo abaixo:

migrations/versions/de865434f506_create_todos_table.py
def upgrade() -> None:\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.create_table('todos',\n    sa.Column('id', sa.Integer(), nullable=False),\n    sa.Column('title', sa.String(), nullable=False),\n    sa.Column('description', sa.String(), nullable=False),\n    sa.Column('state', sa.Enum('draft', 'todo', 'doing', 'done', 'trash', name='todostate'), nullable=False),\n    sa.Column('user_id', sa.Integer(), nullable=False),\n    sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),\n    sa.PrimaryKeyConstraint('id')\n    )\n    # ### end Alembic commands ###\n\n\ndef downgrade() -> None:\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.drop_table('todos')\n    # ### end Alembic commands ###\n

Depois que a migra\u00e7\u00e3o for criada, precisamos aplic\u00e1-la ao nosso banco de dados. Execute o comando alembic upgrade head para aplicar a migra\u00e7\u00e3o.

$ Execu\u00e7\u00e3o no terminal!
alembic upgrade head\nINFO  [alembic.runtime.migration] Context impl SQLiteImpl.\nINFO  [alembic.runtime.migration] Will assume non-transactional DDL.\nINFO  [alembic.runtime.migration] Running upgrade e018397cecf4 -> de865434f506, create todos table\n

Agora que a migra\u00e7\u00e3o foi aplicada, nosso banco de dados deve ter uma nova tabela de tarefas. Para verificar, voc\u00ea pode abrir o banco de dados com o comando sqlite3 database.db e depois executar o comando .schema para ver o esquema do banco de dados.

$ Execu\u00e7\u00e3o no terminal!
sqlite3 database.db\n# ...\nsqlite> .schema\n# ...\nCREATE TABLE todos (\n    id INTEGER NOT NULL,\n    title VARCHAR NOT NULL,\n    description VARCHAR NOT NULL,\n    state VARCHAR(5) NOT NULL,\n    user_id INTEGER NOT NULL,\n    PRIMARY KEY (id),\n    FOREIGN KEY(user_id) REFERENCES users (id)\n);\n

Finalmente, agora que temos a tabela de tarefas em nosso banco de dados, podemos testar nosso endpoint de cria\u00e7\u00e3o de tarefas no Swagger. Para fazer isso, execute nosso servidor FastAPI e abra o Swagger no seu navegador.

"},{"location":"09/#endpoint-de-listagem","title":"Endpoint de listagem","text":"

Agora que criamos a nossa migra\u00e7\u00e3o e temos o endpoint de cria\u00e7\u00e3o de Todos, temos que criar nosso endpoint de listagem de tarefas. Ele deve listar todas as tarefas de acordo com o CurrentUser.

Algumas coisas adicionais e que podem ser importantes na hora de recuperar as tarefas \u00e9 fazer um filtro de busca. Em alguns momentos queremos buscar uma tarefa por t\u00edtulo, em outro momento por descri\u00e7\u00e3o, \u00e0s vezes s\u00f3 pelo estado. Por exemplo, somente tarefas conclu\u00eddas.

Para fazer isso, podemos contar com um recurso do FastAPI chamado Query. A Query permite que definamos par\u00e2metros espec\u00edficos na URL, que podem ser utilizados para filtrar os resultados retornados pelo endpoint. Isso \u00e9 feito atrav\u00e9s da inclus\u00e3o de par\u00e2metros como query strings na URL, interpretados pelo FastAPI para ajustar a consulta ao banco de dados.

Por exemplo, uma query string simples pode ser: todos/?title=\"batatinha\".

Uma caracter\u00edstica importante das queries do FastAPI \u00e9 que podemos juntar mais de um atributo em uma busca. Por exemplo, podemos buscar somente as tarefas a fazer que contenham no t\u00edtulo \"trabalho\". Dessa forma, temos um endpoint mais eficiente, j\u00e1 que podemos realizar buscas complexas e refinadas com uma \u00fanica chamada.

A combina\u00e7\u00e3o poderia ser algo como: todos/?title=\"batatinha\"&status=todo.

A combina\u00e7\u00e3o de diferentes par\u00e2metros de query n\u00e3o s\u00f3 torna o endpoint mais flex\u00edvel, mas tamb\u00e9m permite que os usu\u00e1rios obtenham os dados de que precisam de maneira mais r\u00e1pida e conveniente. Isso contribui para uma melhor experi\u00eancia do usu\u00e1rio e otimiza a intera\u00e7\u00e3o com o banco de dados.

O c\u00f3digo a seguir ilustra como o endpoint de listagem \u00e9 definido utilizando a Query:

fast_zero/routers/todos.py
from fastapi import APIRouter, Depends, Query\nfrom sqlalchemy import select\n# ...\nfrom fast_zero.schemas import TodoList, TodoPublic, TodoSchema\n\n# ...\n\n@router.get('/', response_model=TodoList)\ndef list_todos(  # noqa (1)\n    session: Session,\n    user: CurrentUser,\n    title: str = Query(None),\n    description: str = Query(None),\n    state: str = Query(None),\n    offset: int = Query(None),\n    limit: int = Query(None),\n):\n    query = select(Todo).where(Todo.user_id == user.id)\n\n    if title:\n        query = query.filter(Todo.title.contains(title))\n\n    if description:\n        query = query.filter(Todo.description.contains(description))\n\n    if state:\n        query = query.filter(Todo.state == state)\n\n    todos = session.scalars(query.offset(offset).limit(limit)).all()\n\n    return {'todos': todos}\n
  1. o c\u00f3digo noqa significa que queremos ignorar essa defini\u00e7\u00e3o durante a an\u00e1lise est\u00e1tica. N\u00e3o \u00e9 considerada uma boa pr\u00e1tica de c\u00f3digo ter uma fun\u00e7\u00e3o que recebe mais de 5 argumentos pela regra R0913 do pylint. Nesse caso, por\u00e9m, temos uma particularidade do FastAPI que recebe cada par\u00e2metro de url por um argumento de fun\u00e7\u00e3o. Como queremos ignorar a checagem somente nessa defini\u00e7\u00e3o, usamos o coment\u00e1rio # noqa

Essa abordagem equilibra a flexibilidade e a efici\u00eancia, tornando o endpoint capaz de atender a uma variedade de necessidades de neg\u00f3cio. Utilizando os recursos do FastAPI, conseguimos implementar uma solu\u00e7\u00e3o robusta e f\u00e1cil de manter, que ser\u00e1 testada posteriormente para garantir sua funcionalidade e integridade.

No c\u00f3digo acima, estamos utilizando filtros do SQLAlchemy, uma biblioteca ORM (Object-Relational Mapping) do Python, para adicionar condi\u00e7\u00f5es \u00e0 nossa consulta. Esses filtros correspondem aos par\u00e2metros que o usu\u00e1rio pode passar na URL.

Essas condi\u00e7\u00f5es s\u00e3o traduzidas em cl\u00e1usulas SQL pelo SQLAlchemy, permitindo que o banco de dados filtre os resultados de acordo com os crit\u00e9rios especificados pelo usu\u00e1rio. Essa integra\u00e7\u00e3o entre FastAPI e SQLAlchemy torna o processo de filtragem eficiente e a codifica\u00e7\u00e3o mais expressiva e clara.

"},{"location":"09/#criando-uma-factory-para-simplificar-os-testes","title":"Criando uma factory para simplificar os testes","text":"

Criar uma factory para o endpoint facilitaria os testes por diversas raz\u00f5es, especialmente quando se trata de testar o nosso endpoint de listagem que usa m\u00faltiplas queries. Primeiro, a factory ajuda a encapsular a l\u00f3gica de cria\u00e7\u00e3o dos objetos necess\u00e1rios para o teste, como no caso dos objetos Todo. Isso significa que voc\u00ea pode criar objetos consistentes e bem-formados sem ter que repetir o mesmo c\u00f3digo em v\u00e1rios testes.

Com a complexidade das queries que nosso endpoint permite, precisamos cobrir todos os usos poss\u00edveis dessas queries. A factory vai nos ajudar a criar muitos casos de testes de forma pr\u00e1tica e eficiente, j\u00e1 que podemos gerar diferentes combina\u00e7\u00f5es de t\u00edtulos, descri\u00e7\u00f5es, estados, entre outros atributos, simulando diversas situa\u00e7\u00f5es de uso.

Al\u00e9m disso, ao utilizar bibliotecas como o factory, \u00e9 poss\u00edvel gerar dados aleat\u00f3rios e v\u00e1lidos, o que pode ajudar a garantir que os testes sejam abrangentes e testem o endpoint em uma variedade de condi\u00e7\u00f5es. Ao simplificar o processo de configura\u00e7\u00e3o dos testes, voc\u00ea pode economizar tempo e esfor\u00e7o, permitindo que a equipe se concentre mais na l\u00f3gica do teste.

tests/test_todos.py
import factory.fuzzy\n\nfrom fast_zero.models import Todo, TodoState\n\n# ...\n\nclass TodoFactory(factory.Factory):\n    class Meta:\n        model = Todo\n\n    title = factory.Faker('text')\n    description = factory.Faker('text')\n    state = factory.fuzzy.FuzzyChoice(TodoState)\n    user_id = 1\n

A fixture acima pode ser usada em diversos testes, reduzindo a duplica\u00e7\u00e3o de c\u00f3digo e melhorando a manuten\u00e7\u00e3o. Por exemplo, em um teste que precisa criar v\u00e1rios objetos Todo, voc\u00ea pode simplesmente usar a TodoFactory para criar esses objetos com uma \u00fanica linha de c\u00f3digo. A factory j\u00e1 cont\u00e9m a l\u00f3gica necess\u00e1ria para criar um objeto v\u00e1lido, e voc\u00ea pode facilmente sobrescrever qualquer um dos atributos, se necess\u00e1rio, para o caso de teste espec\u00edfico.

A utiliza\u00e7\u00e3o de f\u00e1bricas tamb\u00e9m promove uma melhor separa\u00e7\u00e3o entre a l\u00f3gica de cria\u00e7\u00e3o do objeto e a l\u00f3gica do teste, tornando os testes mais leg\u00edveis e f\u00e1ceis de seguir. Com a TodoFactory, somos capazes de simular e testar diversos cen\u00e1rios de busca e filtragem, garantindo que nosso endpoint de listagem funcione corretamente em todas as situa\u00e7\u00f5es poss\u00edveis, aumentando assim a robustez e confiabilidade de nosso sistema.

"},{"location":"09/#testes-para-esse-endpoint","title":"Testes para esse endpoint","text":"

Ao trabalhar com o endpoint de listagem de tarefas, temos v\u00e1rias varia\u00e7\u00f5es de query strings que precisam ser testadas. Cada uma dessas varia\u00e7\u00f5es representa um caso de uso diferente, e queremos garantir que o sistema funcione corretamente em todos eles. Separaremos os testes em pequenos blocos e explicar cada um deles.

"},{"location":"09/#testando-a-listagem-de-todos","title":"Testando a Listagem de Todos","text":"

Primeiro, criaremos um teste b\u00e1sico que verifica se o endpoint est\u00e1 listando todos os objetos Todo.

tests/test_todos.py
def test_list_todos_should_return_5_todos(session, client, user, token):\n    expected_todos = 5\n    session.bulk_save_objects(TodoFactory.create_batch(5, user_id=user.id))\n    session.commit()\n\n    response = client.get(\n        '/todos/',\n        headers={'Authorization': f'Bearer {token}'},\n    )\n\n    assert len(response.json()['todos']) == expected_todos\n

Este teste valida que todos os 5 objetos Todo s\u00e3o retornados pelo endpoint.

"},{"location":"09/#testando-a-paginacao","title":"Testando a Pagina\u00e7\u00e3o","text":"

Em seguida, testaremos a pagina\u00e7\u00e3o para garantir que o offset e o limite estejam funcionando corretamente.

tests/test_todos.py
def test_list_todos_pagination_should_return_2_todos(\n    session, user, client, token\n):\n    expected_todos = 2\n    session.bulk_save_objects(TodoFactory.create_batch(5, user_id=user.id))\n    session.commit()\n\n    response = client.get(\n        '/todos/?offset=1&limit=2',\n        headers={'Authorization': f'Bearer {token}'},\n    )\n\n    assert len(response.json()['todos']) == expected_todos\n

Este teste verifica que, quando aplicado o offset de 1 e o limite de 2, apenas 2 objetos Todo s\u00e3o retornados.

"},{"location":"09/#testando-o-filtro-por-titulo","title":"Testando o Filtro por T\u00edtulo","text":"

Tamb\u00e9m queremos verificar se a filtragem por t\u00edtulo est\u00e1 funcionando conforme esperado.

tests/test_todos.py
def test_list_todos_filter_title_should_return_5_todos(\n    session, user, client, token\n):\n    expected_todos = 5\n    session.bulk_save_objects(\n        TodoFactory.create_batch(5, user_id=user.id, title='Test todo 1')\n    )\n    session.commit()\n\n    response = client.get(\n        '/todos/?title=Test todo 1',\n        headers={'Authorization': f'Bearer {token}'},\n    )\n\n    assert len(response.json()['todos']) == expected_todos\n

Este teste garante que quando o filtro de t\u00edtulo \u00e9 aplicado, apenas as tarefas com o t\u00edtulo correspondente s\u00e3o retornadas.

"},{"location":"09/#testando-o-filtro-por-descricao","title":"Testando o Filtro por Descri\u00e7\u00e3o","text":"

Da mesma forma, queremos testar o filtro de descri\u00e7\u00e3o.

tests/test_todos.py
def test_list_todos_filter_description_should_return_5_todos(\n    session, user, client, token\n):\n    expected_todos = 5\n    session.bulk_save_objects(\n        TodoFactory.create_batch(5, user_id=user.id, description='description')\n    )\n    session.commit()\n\n    response = client.get(\n        '/todos/?description=desc',\n        headers={'Authorization': f'Bearer {token}'},\n    )\n\n    assert len(response.json()['todos']) == expected_todos\n

Este teste verifica que, quando filtramos pela descri\u00e7\u00e3o, apenas as tarefas com a descri\u00e7\u00e3o correspondente s\u00e3o retornadas.

"},{"location":"09/#testando-o-filtro-por-estado","title":"Testando o Filtro por Estado","text":"

Finalmente, precisamos testar o filtro de estado.

tests/test_todos.py
def test_list_todos_filter_state_should_return_5_todos(\n    session, user, client, token\n):\n    expected_todos = 5\n    session.bulk_save_objects(\n        TodoFactory.create_batch(5, user_id=user.id, state=TodoState.draft)\n    )\n    session.commit()\n\n    response = client.get(\n        '/todos/?state=draft',\n        headers={'Authorization': f'Bearer {token}'},\n    )\n\n    assert len(response.json()['todos']) == expected_todos\n

Este teste garante que quando filtramos pelo estado, apenas as tarefas com o estado correspondente s\u00e3o retornadas.

"},{"location":"09/#testando-a-combinacao-de-filtros-de-estado-titulo-e-descricao","title":"Testando a Combina\u00e7\u00e3o de Filtros de Estado, T\u00edtulo e Descri\u00e7\u00e3o","text":"

Em nosso conjunto de testes, tamb\u00e9m \u00e9 importante verificar se o endpoint \u00e9 capaz de lidar com m\u00faltiplos par\u00e2metros de consulta simultaneamente. Para isso, criaremos um teste que combine os filtros de estado, t\u00edtulo e descri\u00e7\u00e3o. Isso assegurar\u00e1 que, quando esses par\u00e2metros s\u00e3o usados juntos, o endpoint retornar\u00e1 apenas as tarefas que correspondem a todas essas condi\u00e7\u00f5es.

Este teste \u00e9 vital para garantir que os usu\u00e1rios podem realizar buscas complexas usando v\u00e1rios crit\u00e9rios ao mesmo tempo, e que o endpoint ir\u00e1 retornar os resultados esperados.

A seguir, apresento o c\u00f3digo do teste:

tests/test_todos.py
def test_list_todos_filter_combined_should_return_5_todos(\n    session, user, client, token\n):\n    expected_todos = 5\n    session.bulk_save_objects(\n        TodoFactory.create_batch(\n            5,\n            user_id=user.id,\n            title='Test todo combined',\n            description='combined description',\n            state=TodoState.done,\n        )\n    )\n\n    session.bulk_save_objects(\n        TodoFactory.create_batch(\n            3,\n            user_id=user.id,\n            title='Other title',\n            description='other description',\n            state=TodoState.todo,\n        )\n    )\n    session.commit()\n\n    response = client.get(\n        '/todos/?title=Test todo combined&description=combined&state=done',\n        headers={'Authorization': f'Bearer {token}'},\n    )\n\n    assert len(response.json()['todos']) == expected_todos\n

Com esses testes, cobrimos todas as poss\u00edveis varia\u00e7\u00f5es de query strings para o nosso endpoint, garantindo que ele funciona corretamente em todas essas situa\u00e7\u00f5es. A abordagem modular para escrever esses testes facilita a leitura e a manuten\u00e7\u00e3o, al\u00e9m de permitir uma cobertura de teste abrangente e robusta.

"},{"location":"09/#executando-os-testes","title":"Executando os testes","text":"

\u00c9 importante n\u00e3o esquecermos de executar os testes para ver se tudo corre bem:

task test tests/test_todos.py\n# ...\ntests/test_todos.py::test_create_todo PASSED\ntests/test_todos.py::test_list_todos PASSED\ntests/test_todos.py::test_list_todos_pagination PASSED\ntests/test_todos.py::test_list_todos_filter_title PASSED\ntests/test_todos.py::test_list_todos_filter_description PASSED\ntests/test_todos.py::test_list_todos_filter_state PASSED\ntests/test_todos.py::test_list_todos_filter_combined PASSED\n
"},{"location":"09/#endpoint-de-alteracao","title":"Endpoint de Altera\u00e7\u00e3o","text":"

Para fazer a altera\u00e7\u00e3o de uma tarefa, precisamos de um modelo onde tudo seja opcional, j\u00e1 que poder\u00edamos querer atualizar apenas um ou alguns campos da tarefa. Criaremos o esquema TodoUpdate, no qual todos os campos s\u00e3o opcionais:

fast_zero/schemas.py
class TodoUpdate(BaseModel):\n    title: str | None = None\n    description: str | None = None\n    state: TodoState | None = None\n

Para podermos alterar somente os valores que recebemos no modelo, temos que fazer um dump somente dos valores que recebemos e os atualizar no objeto que pegamos da base de dados:

fast_zero/routers/todos.py
from http import HTTPStatus\nfrom typing import Annotated\n\nfrom fastapi import APIRouter, Depends, HTTPException, Query\n# ...\nfrom fast_zero.schemas import TodoList, TodoPublic, TodoSchema, TodoUpdate\n\n# ...\n\n@router.patch('/{todo_id}', response_model=TodoPublic)\ndef patch_todo(\n    todo_id: int, session: Session, user: CurrentUser, todo: TodoUpdate\n):\n    db_todo = session.scalar(\n        select(Todo).where(Todo.user_id == user.id, Todo.id == todo_id)\n    )\n\n    if not db_todo:\n        raise HTTPException(\n            status_code=HTTPStatus.NOT_FOUND, detail='Task not found.'\n        )\n\n    for key, value in todo.model_dump(exclude_unset=True).items():\n        setattr(db_todo, key, value)\n\n    session.add(db_todo)\n    session.commit()\n    session.refresh(db_todo)\n\n    return db_todo\n

A linha for key, value in todo.model_dump(exclude_unset=True).items(): est\u00e1 iterando atrav\u00e9s de todos os campos definidos na inst\u00e2ncia todo do modelo de atualiza\u00e7\u00e3o. A fun\u00e7\u00e3o model_dump \u00e9 um m\u00e9todo que vem do modelo BaseModel do Pydantic e permite exportar o modelo para um dicion\u00e1rio.

O par\u00e2metro exclude_unset=True \u00e9 importante aqui, pois significa que apenas os campos que foram explicitamente definidos (ou seja, aqueles que foram inclu\u00eddos na solicita\u00e7\u00e3o PATCH) ser\u00e3o inclu\u00eddos no dicion\u00e1rio resultante. Isso permite que voc\u00ea atualize apenas os campos que foram fornecidos na solicita\u00e7\u00e3o, deixando os outros inalterados.

Ap\u00f3s obter a chave e o valor de cada campo definido, a linha setattr(db_todo, key, value) \u00e9 usada para atualizar o objeto db_todo que representa a tarefa no banco de dados. A fun\u00e7\u00e3o setattr \u00e9 uma fun\u00e7\u00e3o embutida do Python que permite definir o valor de um atributo em um objeto. Neste caso, ele est\u00e1 definindo o atributo com o nome igual \u00e0 chave (ou seja, o nome do campo) no objeto db_todo com o valor correspondente.

Dessa forma, garantimos que somente os campos enviados ao schema sejam atualizados no objeto.

"},{"location":"09/#testes-para-o-endpoint-de-alteracao","title":"Testes para o Endpoint de Altera\u00e7\u00e3o","text":"

Os testes aqui incluem o caso de atualiza\u00e7\u00e3o bem-sucedida e o caso de erro quando a tarefa n\u00e3o \u00e9 encontrada:

fast_zero/tests/test_todos.py
from http import HTTPStatus\n\n# ...\n\ndef test_patch_todo_error(client, token):\n    response = client.patch(\n        '/todos/10',\n        json={},\n        headers={'Authorization': f'Bearer {token}'},\n    )\n    assert response.status_code == HTTPStatus.NOT_FOUND\n    assert response.json() == {'detail': 'Task not found.'}\n\n\ndef test_patch_todo(session, client, user, token):\n    todo = TodoFactory(user_id=user.id)\n\n    session.add(todo)\n    session.commit()\n\n    response = client.patch(\n        f'/todos/{todo.id}',\n        json={'title': 'teste!'},\n        headers={'Authorization': f'Bearer {token}'},\n    )\n    assert response.status_code == HTTPStatus.OK\n    assert response.json()['title'] == 'teste!'\n

Agora precisamos executar os testes para ver se est\u00e1 tudo correto:

$ Execu\u00e7\u00e3o no terminal!
task test tests/test_todos.py\n\n# ...\n\ntests/test_todos.py::test_create_todo PASSED\ntests/test_todos.py::test_list_todos PASSED\ntests/test_todos.py::test_list_todos_pagination PASSED\ntests/test_todos.py::test_list_todos_filter_title PASSED\ntests/test_todos.py::test_list_todos_filter_description PASSED\ntests/test_todos.py::test_list_todos_filter_state PASSED\ntests/test_todos.py::test_list_todos_filter_combined PASSED\ntests/test_todos.py::test_patch_todo_error PASSED\ntests/test_todos.py::test_patch_todo PASSED\n

Com tudo funcionando, podemos partir para o nosso endpoint de DELETE.

"},{"location":"09/#endpoint-de-delecao","title":"Endpoint de Dele\u00e7\u00e3o","text":"

A rota para deletar uma tarefa \u00e9 simples e direta. Caso o todo exista, deletaremos ele com a sesion caso n\u00e3o, retornamos 404:

fast_zero/routers/todos.py
from fast_zero.schemas import (\n    Message,\n    TodoList,\n    TodoPublic,\n    TodoSchema,\n    TodoUpdate,\n)\n\n# ...\n\n\n@router.delete('/{todo_id}', response_model=Message)\ndef delete_todo(todo_id: int, session: Session, user: CurrentUser):\n    todo = session.scalar(\n        select(Todo).where(Todo.user_id == user.id, Todo.id == todo_id)\n    )\n\n    if not todo:\n        raise HTTPException(\n            status_code=HTTPStatus.NOT_FOUND, detail='Task not found.'\n        )\n\n    session.delete(todo)\n    session.commit()\n\n    return {'message': 'Task has been deleted successfully.'}\n
"},{"location":"09/#testes-para-o-endpoint-de-delecao","title":"Testes para o Endpoint de Dele\u00e7\u00e3o","text":"

Esses testes verificam tanto a remo\u00e7\u00e3o bem-sucedida quanto o caso de erro quando a tarefa n\u00e3o \u00e9 encontrada:

fast_zero/tests/test_todos.py
def test_delete_todo(session, client, user, token):\n    todo = TodoFactory(user_id=user.id)\n\n    session.add(todo)\n    session.commit()\n\n    response = client.delete(\n        f'/todos/{todo.id}', headers={'Authorization': f'Bearer {token}'}\n    )\n\n    assert response.status_code == HTTPStatus.OK\n    assert response.json() == {\n        'message': 'Task has been deleted successfully.'\n    }\n\n\ndef test_delete_todo_error(client, token):\n    response = client.delete(\n        f'/todos/{10}', headers={'Authorization': f'Bearer {token}'}\n    )\n\n    assert response.status_code == HTTPStatus.NOT_FOUND\n    assert response.json() == {'detail': 'Task not found.'}\n

Por fim, precisamos executar os testes para ver se est\u00e1 tudo correto:

$ Execu\u00e7\u00e3o no terminal!
task test tests/test_todos.py\n\n# ...\n\ntests/test_todos.py::test_create_todo PASSED\ntests/test_todos.py::test_list_todos PASSED\ntests/test_todos.py::test_list_todos_pagination PASSED\ntests/test_todos.py::test_list_todos_filter_title PASSED\ntests/test_todos.py::test_list_todos_filter_description PASSED\ntests/test_todos.py::test_list_todos_filter_state PASSED\ntests/test_todos.py::test_list_todos_filter_combined PASSED\ntests/test_todos.py::test_delete_todo PASSED\ntests/test_todos.py::test_delete_todo_error PASSED\ntests/test_todos.py::test_patch_todo_error PASSED\ntests/test_todos.py::test_patch_todo PASSED\n
"},{"location":"09/#commit","title":"Commit","text":"

Agora que voc\u00ea finalizou a implementa\u00e7\u00e3o desses endpoints, \u00e9 um bom momento para fazer um commit das suas mudan\u00e7as. Para isso, voc\u00ea pode seguir os seguintes passos:

  1. Adicione as mudan\u00e7as para a stage area: git add .
  2. Commit as mudan\u00e7as: git commit -m \"Implementado os endpoints de tarefas\"
"},{"location":"09/#exercicios","title":"Exerc\u00edcios","text":"
  1. Adicione os campos created_at e updated_at na tabela Todo

    • Eles devem ser init=False
    • Deve usar func.now() para cria\u00e7\u00e3o
    • O campo updated_at deve ter onupdate
  2. Criar uma migra\u00e7\u00e3o para que os novos campos sejam versionados e tamb\u00e9m aplicar a migra\u00e7\u00e3o

  3. Adicionar os campos created_at e updated_at no schema de sa\u00edda dos endpoints. Para que esse valores sejam retornados na API.
"},{"location":"09/#conclusao","title":"Conclus\u00e3o","text":"

Nesta aula exploramos os aspectos essenciais para construir uma API completa e funcional para gerenciar tarefas, integrando-se ao sistema de autentica\u00e7\u00e3o que j\u00e1 t\u00ednhamos desenvolvido.

Iniciamos criando a estrutura de banco de dados para as tarefas, incluindo tabelas e migra\u00e7\u00f5es, e em seguida definimos os schemas necess\u00e1rios. A partir da\u00ed, trabalhamos na cria\u00e7\u00e3o dos endpoints para as opera\u00e7\u00f5es CRUD: cria\u00e7\u00e3o, leitura (listagem com filtragem), atualiza\u00e7\u00e3o (edi\u00e7\u00e3o) e exclus\u00e3o (dele\u00e7\u00e3o).

Em cada est\u00e1gio, focamos na qualidade e na robustez, utilizando testes rigorosos para assegurar que os endpoints se comportassem conforme esperado. Exploramos tamb\u00e9m t\u00e9cnicas espec\u00edficas como atualiza\u00e7\u00e3o parcial e filtragem avan\u00e7ada, tornando a API flex\u00edvel e poderosa.

O resultado foi um sistema integrado de gerenciamento de tarefas, ou um \"todo list\", ligado aos usu\u00e1rios e \u00e0 autentica\u00e7\u00e3o que j\u00e1 hav\u00edamos implementado. Esta aula refor\u00e7ou a import\u00e2ncia de um design cuidadoso e uma implementa\u00e7\u00e3o criteriosa, ilustrando como a FastAPI pode ser usada para criar APIs eficientes e profissionais.

Agora que a nossa aplica\u00e7\u00e3o est\u00e1 crescendo e ganhando mais funcionalidades, na pr\u00f3xima aula, mergulharemos no mundo da dockeriza\u00e7\u00e3o. Aprenderemos a colocar a nossa aplica\u00e7\u00e3o dentro de um container Docker, facilitando o deploy e o escalonamento. Este \u00e9 um passo vital no desenvolvimento moderno de aplica\u00e7\u00f5es e estou ansioso para gui\u00e1-lo atrav\u00e9s dele. At\u00e9 l\u00e1!

Agora que a aula acabou, \u00e9 um bom momento para voc\u00ea relembrar alguns conceitos e fixar melhor o conte\u00fado respondendo ao question\u00e1rio referente a ela.

Quiz

"},{"location":"10/","title":"Dockerizando a nossa aplica\u00e7\u00e3o e introduzindo o PostgreSQL","text":""},{"location":"10/#dockerizando-a-nossa-aplicacao-e-introduzindo-o-postgresql","title":"Dockerizando a nossa aplica\u00e7\u00e3o e introduzindo o PostgreSQL","text":"

Objetivos da aula:

Caso prefira ver a aula em v\u00eddeo

Esse aula ainda n\u00e3o est\u00e1 dispon\u00edvel em formato de v\u00eddeo, somente em texto ou live!

Aula Slides C\u00f3digo Quiz

Ap\u00f3s a implementa\u00e7\u00e3o do nosso gerenciador de tarefas na aula anterior, temos uma primeira vers\u00e3o est\u00e1vel da nossa aplica\u00e7\u00e3o. Nesta aula, al\u00e9m de aprendermos a \"dockerizar\" nossa aplica\u00e7\u00e3o FastAPI, tamb\u00e9m abordaremos a migra\u00e7\u00e3o do banco de dados SQLite para o PostgreSQL.

"},{"location":"10/#o-docker-e-a-nossa-aplicacao","title":"O Docker e a nossa aplica\u00e7\u00e3o","text":"

Docker \u00e9 uma plataforma aberta que permite automatizar o processo de implanta\u00e7\u00e3o, escalonamento e opera\u00e7\u00e3o de aplica\u00e7\u00f5es dentro de cont\u00eaineres. Ele serve para \"empacotar\" uma aplica\u00e7\u00e3o e suas depend\u00eancias em um cont\u00eainer virtual que pode ser executado em qualquer sistema operacional que suporte Docker. Isso facilita a implanta\u00e7\u00e3o, o desenvolvimento e o compartilhamento de aplica\u00e7\u00f5es, al\u00e9m de proporcionar um ambiente isolado e consistente.

Caso n\u00e3o tenha o docker instalado na sua m\u00e1quina

A instala\u00e7\u00e3o do Docker varia entre sistemas operacionais. Por esse motivo, acredito que n\u00e3o cabe cobrir a instala\u00e7\u00e3o do docker nesse material.

WindowsLinuxMacOS X

A instala\u00e7\u00e3o no windows varia com a forma em que voc\u00ea administra o seu sistema. Ela pode se basear em WSL2 ou no Hyper-V.

Os passos para ambos os tipos de instala\u00e7\u00e3o podem ser encontrados na documenta\u00e7\u00e3o oficial do docker: link.

A instala\u00e7\u00e3o no linux variar\u00e1 de acordo com a sua distribui\u00e7\u00e3o. As distribui\u00e7\u00f5es mais tradicionais (baseadas em Debian e RHEL podem ser encontradas na documenta\u00e7\u00e3o oficial do docker: link.

Outras distribui\u00e7\u00f5es devem ter o pacote do docker dispon\u00edvel em seus reposit\u00f3rios. Como distro baseadas em Archlinux.

A instala\u00e7\u00e3o no MacOS, depender\u00e1 da arquitetura do seu computador. Se voc\u00ea usa Intel ou Silicon.

Os passos para ambos os tipos de instala\u00e7\u00e3o podem ser encontrados na documenta\u00e7\u00e3o oficial do docker: link.

"},{"location":"10/#criando-nosso-dockerfile","title":"Criando nosso Dockerfile","text":"

Para criar um container Docker, escrevemos uma lista de passos de como construir o ambiente para execu\u00e7\u00e3o da nossa aplica\u00e7\u00e3o em um arquivo chamado Dockerfile. Ele define o ambiente de execu\u00e7\u00e3o, os comandos necess\u00e1rios para preparar o ambiente e o comando a ser executado quando um cont\u00eainer \u00e9 iniciado a partir da imagem.

Uma das coisas interessantes sobre Docker \u00e9 que existe um Hub de containers prontos onde a comunidade hospeda imagens \"prontas\", que podemos usar como ponto de partida. Por exemplo, a comunidade de python mant\u00e9m um grupo de imagens com o ambiente python pronto para uso. Podemos partir dessa imagem com o python j\u00e1 instalado adicionar os passos para que nossa aplica\u00e7\u00e3o seja executada.

Aqui est\u00e1 um exemplo de Dockerfile para executar nossa aplica\u00e7\u00e3o:

Vers\u00e3o 3.11Vers\u00e3o 3.12
FROM python:3.11-slim\nENV POETRY_VIRTUALENVS_CREATE=false\n\nWORKDIR app/\nCOPY . .\n\nRUN pip install poetry\n\nRUN poetry config installer.max-workers 10\nRUN poetry install --no-interaction --no-ansi\n\nEXPOSE 8000\nCMD poetry run uvicorn --host 0.0.0.0 fast_zero.app:app\n

Aqui est\u00e1 o que cada linha faz:

  1. FROM python:3.11-slim: define a imagem base para nosso cont\u00eainer. Estamos usando a vers\u00e3o slim da imagem do Python 3.11, que tem tudo que precisamos para rodar nossa aplica\u00e7\u00e3o.
  2. ENV POETRY_VIRTUALENVS_CREATE=false: define uma vari\u00e1vel de ambiente que diz ao Poetry para n\u00e3o criar um ambiente virtual. (O container j\u00e1 \u00e9 um ambiente isolado)
  3. RUN pip install poetry: instala o Poetry, nosso gerenciador de pacotes.
  4. WORKDIR app/: define o diret\u00f3rio em que executaremos os comandos a seguir.
  5. COPY . .: copia todos os arquivos do diret\u00f3rio atual para o cont\u00eainer.
  6. RUN poetry config installer.max-workers 10: configura o Poetry para usar at\u00e9 10 workers ao instalar pacotes.
  7. RUN poetry install --no-interaction --no-ansi: instala as depend\u00eancias do nosso projeto sem intera\u00e7\u00e3o e sem cores no output.
  8. EXPOSE 8000: informa ao Docker que o cont\u00eainer escutar\u00e1 na porta 8000.
  9. CMD poetry run uvicorn --host 0.0.0.0 fast_zero.app:app: define o comando que ser\u00e1 executado quando o cont\u00eainer for iniciado.
FROM python:3.12-slim\nENV POETRY_VIRTUALENVS_CREATE=false\n\nWORKDIR app/\nCOPY . .\n\nRUN pip install poetry\n\nRUN poetry config installer.max-workers 10\nRUN poetry install --no-interaction --no-ansi\n\nEXPOSE 8000\nCMD poetry run uvicorn --host 0.0.0.0 fast_zero.app:app\n

Aqui est\u00e1 o que cada linha faz:

  1. FROM python:3.12-slim: define a imagem base para nosso cont\u00eainer. Estamos usando a vers\u00e3o slim da imagem do Python 3.12, que tem tudo que precisamos para rodar nossa aplica\u00e7\u00e3o.
  2. ENV POETRY_VIRTUALENVS_CREATE=false: define uma vari\u00e1vel de ambiente que diz ao Poetry para n\u00e3o criar um ambiente virtual. (O container j\u00e1 \u00e9 um ambiente isolado)
  3. RUN pip install poetry: instala o Poetry, nosso gerenciador de pacotes.
  4. WORKDIR app/: define o diret\u00f3rio em que executaremos os comandos a seguir.
  5. COPY . .: copia todos os arquivos do diret\u00f3rio atual para o cont\u00eainer.
  6. RUN poetry config installer.max-workers 10: configura o Poetry para usar at\u00e9 10 workers ao instalar pacotes.
  7. RUN poetry install --no-interaction --no-ansi: instala as depend\u00eancias do nosso projeto sem intera\u00e7\u00e3o e sem cores no output.
  8. EXPOSE 8000: informa ao Docker que o cont\u00eainer escutar\u00e1 na porta 8000.
  9. CMD poetry run uvicorn --host 0.0.0.0 fast_zero.app:app: define o comando que ser\u00e1 executado quando o cont\u00eainer for iniciado.

Vamos entender melhor esse \u00faltimo comando:

"},{"location":"10/#criando-a-imagem","title":"Criando a imagem","text":"

Para criar uma imagem Docker a partir do Dockerfile, usamos o comando docker build. O comando a seguir cria uma imagem chamada \"fast_zero\":

$ Execu\u00e7\u00e3o no terminal!
docker build -t \"fast_zero\" .\n
Voc\u00ea usa Mac com Silicon?

Pode haver alguma incompatibilidade em alguma biblioteca durante o build. Pois nem todos os pacotes est\u00e3o dispon\u00edveis para Silicon no pypi. Arquitetura aarch64.

Caso encontre algum problema, durante o build voc\u00ea pode especificar a plataforma para amd64. Que \u00e9 a arquitetura em que o curso foi escrito:

$ Execu\u00e7\u00e3o no terminal!
docker build --platform linux/amd64 -t \"fast_zero\" .\n

Mais informa\u00e7\u00f5es nessa issue. Obrigado @K-dash por notificar

Este comando l\u00ea o Dockerfile no diret\u00f3rio atual (indicado pelo .) e cria uma imagem com a tag \"fast_zero\", (indicada pelo -t).

Ent\u00e3o verificaremos se a imagem foi criada com sucesso usando o comando:

$ Execu\u00e7\u00e3o no terminal!
docker images\n

Este comando lista todas as imagens Docker dispon\u00edveis no seu sistema.

"},{"location":"10/#executando-o-container","title":"Executando o container","text":"

Para executar o cont\u00eainer, usamos o comando docker run. Especificamos o nome do cont\u00eainer com a flag --name, indicamos a imagem que queremos executar e a tag que queremos usar <nome_da_imagem>:<tag>. A flag -p serve para mapear a porta do host para a porta do cont\u00eainer <porta_do_host>:<porta_do_cont\u00eainer>. Portanto, teremos o seguinte comando:

$ Execu\u00e7\u00e3o no terminal!
docker run -it --name fastzeroapp -p 8000:8000 fast_zero:latest\n

Este comando iniciar\u00e1 nossa aplica\u00e7\u00e3o em um cont\u00eainer Docker, que estar\u00e1 escutando na porta 8000. Para testar se tudo est\u00e1 funcionando corretamente, voc\u00ea pode acessar http://localhost:8000 em um navegador ou usar um comando como:

$ Execu\u00e7\u00e3o no terminal!
curl http://localhost:8000\n
Caso voc\u00ea fique preso no terminal

Caso voc\u00ea tenha a aplica\u00e7\u00e3o travada no terminal e n\u00e3o consiga sair, voc\u00ea pode teclar Ctrl+C para parar a execu\u00e7\u00e3o do container.

"},{"location":"10/#gerenciando-containers-docker","title":"Gerenciando Containers docker","text":"

Quando voc\u00ea trabalha com Docker, \u00e9 importante saber como gerenciar os cont\u00eaineres. Aqui est\u00e3o algumas opera\u00e7\u00f5es b\u00e1sicas para gerenci\u00e1-los:

  1. Rodar um cont\u00eainer em background: se voc\u00ea deseja executar o cont\u00eainer em segundo plano para que n\u00e3o ocupe o terminal, pode usar a op\u00e7\u00e3o -d:

    $ Execu\u00e7\u00e3o no terminal!
    docker run -d --name fastzeroapp -p 8000:8000 fast_zero:latest\n
  2. Parar um cont\u00eainer: quando voc\u00ea \"para\" um cont\u00eainer, est\u00e1 essencialmente interrompendo a execu\u00e7\u00e3o do processo principal do cont\u00eainer. Isso significa que o cont\u00eainer n\u00e3o est\u00e1 mais ativo, mas ainda existe no sistema, com seus dados associados e configura\u00e7\u00e3o. Isso permite que voc\u00ea reinicie o cont\u00eainer posteriormente, se desejar.

    $ Execu\u00e7\u00e3o no terminal!
    docker stop fastzeroapp\n
  3. Remover um cont\u00eainer: ao \"remover\" um cont\u00eainer, voc\u00ea est\u00e1 excluindo o cont\u00eainer do sistema. Isso significa que todos os dados associados ao cont\u00eainer s\u00e3o apagados. Uma vez que um cont\u00eainer \u00e9 removido, voc\u00ea n\u00e3o pode reinici\u00e1-lo; no entanto, voc\u00ea pode sempre criar um novo cont\u00eainer a partir da mesma imagem.

    $ Execu\u00e7\u00e3o no terminal!
    docker rm fastzeroapp\n

Ambos os comandos (stop e rm) usam o nome do cont\u00eainer que definimos anteriormente com a flag --name. \u00c9 uma boa pr\u00e1tica manter a gest\u00e3o dos seus cont\u00eaineres, principalmente durante o desenvolvimento, para evitar um uso excessivo de recursos ou conflitos de nomes e portas.

"},{"location":"10/#introduzindo-o-postgresql","title":"Introduzindo o postgreSQL","text":"

O PostgreSQL \u00e9 um Sistema de Gerenciamento de Banco de Dados Objeto-Relacional (ORDBMS) poderoso e de c\u00f3digo aberto. Ele \u00e9 amplamente utilizado em produ\u00e7\u00e3o em muitos projetos devido \u00e0 sua robustez, escalabilidade e conjunto de recursos extensos.

Mudar para um banco de dados como PostgreSQL tem v\u00e1rios benef\u00edcios:

Al\u00e9m disso, SQLite tem algumas limita\u00e7\u00f5es que podem torn\u00e1-lo inadequado para produ\u00e7\u00e3o em alguns casos. Por exemplo, ele n\u00e3o suporta alta concorr\u00eancia e pode ter problemas de performance com grandes volumes de dados.

Nota

Embora para o escopo da nossa aplica\u00e7\u00e3o e os objetivos de aprendizado o SQLite pudesse ser suficiente, \u00e9 sempre bom nos prepararmos para cen\u00e1rios de produ\u00e7\u00e3o real. A ado\u00e7\u00e3o de PostgreSQL nos d\u00e1 uma pr\u00e9via das pr\u00e1ticas do mundo real e garante que nossa aplica\u00e7\u00e3o possa escalar sem grandes modifica\u00e7\u00f5es de infraestrutura.

"},{"location":"10/#como-executar-o-postgres","title":"Como executar o postgres?","text":"

Embora o PostgreSQL seja poderoso, sua instala\u00e7\u00e3o direta em uma m\u00e1quina real pode ser desafiadora e pode resultar em configura\u00e7\u00f5es diferentes entre os ambientes de desenvolvimento. Felizmente, podemos utilizar o Docker para resolver esse problema. No Docker Hub, est\u00e3o dispon\u00edveis imagens pr\u00e9-constru\u00eddas do PostgreSQL, permitindo-nos executar o PostgreSQL com um \u00fanico comando. Confira a imagem oficial do PostgreSQL.

Para executar um cont\u00eainer do PostgreSQL, use o seguinte comando:

$ Execu\u00e7\u00e3o no terminal!
docker run -d \\\n    --name app_database \\\n    -e POSTGRES_USER=app_user \\\n    -e POSTGRES_DB=app_db \\\n    -e POSTGRES_PASSWORD=app_password \\\n    -p 5432:5432 \\\n    postgres\n
"},{"location":"10/#explicando-as-flags-e-configuracoes","title":"Explicando as Flags e Configura\u00e7\u00f5es","text":"

Esta flag \u00e9 usada para definir vari\u00e1veis de ambiente no cont\u00eainer. No contexto do PostgreSQL, essas vari\u00e1veis s\u00e3o essenciais. Elas configuram o nome de usu\u00e1rio, nome do banco de dados, e senha durante a primeira execu\u00e7\u00e3o do cont\u00eainer. Sem elas, o PostgreSQL pode n\u00e3o iniciar da forma esperada. \u00c9 uma forma pr\u00e1tica de configurar o PostgreSQL sem interagir manualmente ou criar arquivos de configura\u00e7\u00e3o.

O PostgreSQL, por padr\u00e3o, escuta por conex\u00f5es na porta 5432. Mapeando esta porta do cont\u00eainer para a mesma porta no host (usando -p), fazemos com que o PostgreSQL seja acess\u00edvel nesta porta na m\u00e1quina anfitri\u00e3, permitindo que outras aplica\u00e7\u00f5es se conectem a ele.

Sobre as vari\u00e1veis

Os valores acima (app_user, app_db, e app_password) s\u00e3o padr\u00f5es gen\u00e9ricos para facilitar a inicializa\u00e7\u00e3o do PostgreSQL em um ambiente de desenvolvimento. No entanto, \u00e9 altamente recomend\u00e1vel que voc\u00ea altere esses valores, especialmente app_password, para garantir a seguran\u00e7a do seu banco de dados.

"},{"location":"10/#volumes-e-persistencia-de-dados","title":"Volumes e Persist\u00eancia de Dados","text":"

Para garantir a persist\u00eancia dos dados entre execu\u00e7\u00f5es do cont\u00eainer, utilizamos volumes. Um volume mapeia um diret\u00f3rio do sistema host para um diret\u00f3rio no cont\u00eainer. Isso \u00e9 crucial para bancos de dados, pois sem um volume, ao remover o cont\u00eainer, todos os dados armazenados dentro dele se perderiam.

No PostgreSQL, o diret\u00f3rio padr\u00e3o para armazenamento de dados \u00e9 /var/lib/postgresql/data. Mapeamos esse diret\u00f3rio para um volume (neste caso \"pgdata\") em nossa m\u00e1quina host para garantir a persist\u00eancia dos dados:

$ Execu\u00e7\u00e3o no terminal!
docker run -d \\\n    --name app_database \\\n    -e POSTGRES_USER=app_user \\\n    -e POSTGRES_DB=app_db \\\n    -e POSTGRES_PASSWORD=app_password \\\n    -v pgdata:/var/lib/postgresql/data \\\n    -p 5432:5432 \\\n    postgres\n

O par\u00e2metro do volume \u00e9 passado ao cont\u00eainer usando o par\u00e2metro -v Dessa forma, os dados do banco continuar\u00e3o existindo, mesmo que o cont\u00eainer seja reiniciado ou removido.

"},{"location":"10/#adicionando-o-suporte-ao-postgresql-na-nossa-aplicacao","title":"Adicionando o suporte ao PostgreSQL na nossa aplica\u00e7\u00e3o","text":"

Para que o SQLAlchemy suporte o PostgreSQL, precisamos instalar uma depend\u00eancia chamada psycopg. Este \u00e9 o adaptador PostgreSQL para Python e \u00e9 crucial para fazer a comunica\u00e7\u00e3o.

Para instalar essa depend\u00eancia, utilize o seguinte comando:

$ Execu\u00e7\u00e3o no terminal!
poetry add \"psycopg[binary]\"\n

Uma das vantagens do SQLAlchemy enquanto ORM \u00e9 a flexibilidade. Com apenas algumas altera\u00e7\u00f5es m\u00ednimas, como a atualiza\u00e7\u00e3o da string de conex\u00e3o, podemos facilmente transicionar para um banco de dados diferente. Assim, ap\u00f3s ajustar o arquivo .env com a string de conex\u00e3o do PostgreSQL, a aplica\u00e7\u00e3o dever\u00e1 operar normalmente, mas desta vez utilizando o PostgreSQL.

Para ajustar a conex\u00e3o com o PostgreSQL, modifique seu arquivo .env para incluir a seguinte string de conex\u00e3o:

.env
DATABASE_URL=\"postgresql+psycopg://app_user:app_password@localhost:5432/app_db\"\n

Caso tenha alterado as vari\u00e1veis de ambiente do cont\u00eainer

Se voc\u00ea alterou app_user, app_password ou app_db ao inicializar o cont\u00eainer PostgreSQL, garanta que esses valores sejam refletidos na string de conex\u00e3o acima. A palavra localhost indica que o banco de dados PostgreSQL est\u00e1 sendo executado na mesma m\u00e1quina que sua aplica\u00e7\u00e3o. Se o banco de dados estiver em uma m\u00e1quina diferente, substitua localhost pelo endere\u00e7o IP correspondente e, se necess\u00e1rio, ajuste a porta 5432.

Para que a instala\u00e7\u00e3o do psycopg esteja na imagem docker, precisamos fazer um novo build. Para que a nova vers\u00e3o do pyproject.toml seja copiada e os novos pacotes sejam instalados:

$ Execu\u00e7\u00e3o no terminal!
docker rm fastzeroapp #(1)!\ndocker build -t \"fast_zero\" #(2)!\ndocker run -it --name fastzeroapp -p 8000:8000 fast_zero:latest #(3)!\n
  1. Remove a vers\u00e3o antiga
  2. Refaz o build
  3. Executa novamente
"},{"location":"10/#executando-as-migracoes","title":"Executando as migra\u00e7\u00f5es","text":"

Migra\u00e7\u00f5es s\u00e3o como vers\u00f5es para seu banco de dados, permitindo que voc\u00ea atualize sua estrutura de forma ordenada e controlada. Sempre que mudamos de banco de dados, ou at\u00e9 mesmo quando alteramos sua estrutura, as migra\u00e7\u00f5es precisam ser executadas para garantir que a base de dados esteja em sincronia com nosso c\u00f3digo.

No contexto de cont\u00eaineres, rodar as migra\u00e7\u00f5es se torna ainda mais simples. Quando mudamos de banco de dados, como \u00e9 o caso de termos sa\u00eddo de um SQLite (por exemplo) para um PostgreSQL, as migra\u00e7\u00f5es s\u00e3o essenciais. O motivo \u00e9 simples: o novo banco de dados n\u00e3o ter\u00e1 a estrutura e os dados do antigo, a menos que migremos. As migra\u00e7\u00f5es ir\u00e3o garantir que o novo banco de dados tenha a mesma estrutura e rela\u00e7\u00f5es que o anterior.

Antes de executar o pr\u00f3ximo comando

Assegure-se de que ambos os cont\u00eaineres, tanto da aplica\u00e7\u00e3o quanto do banco de dados, estejam ativos. O cont\u00eainer do banco de dados deve estar rodando para que a aplica\u00e7\u00e3o possa se conectar a ele.

Assegure-se de que o cont\u00eainer da aplica\u00e7\u00e3o esteja ativo. Estamos usando a flag --network=host para que o cont\u00eainer use a rede do host. Isso pode ser essencial para evitar problemas de conex\u00e3o, j\u00e1 que n\u00e3o podemos prever como est\u00e1 configurada a rede do computador onde este comando ser\u00e1 executado.

execu\u00e7\u00e3o no terminal
docker run -d --network=host --name fastzeroapp -p 8000:8000 fast_zero:latest\n

Para aplicar migra\u00e7\u00f5es em um ambiente com cont\u00eaineres, frequentemente temos comandos espec\u00edficos associados ao servi\u00e7o. Vejamos como executar migra\u00e7\u00f5es usando o Docker:

$ Execu\u00e7\u00e3o no terminal!
docker exec -it fastzeroapp poetry run alembic upgrade head\n

O comando docker exec \u00e9 usado para invocar um comando espec\u00edfico dentro de um cont\u00eainer em execu\u00e7\u00e3o. A op\u00e7\u00e3o -it \u00e9 uma combina\u00e7\u00e3o de -i (interativo) e -t (pseudo-TTY), que juntas garantem um terminal interativo, permitindo a comunica\u00e7\u00e3o direta com o cont\u00eainer.

Ap\u00f3s executar as migra\u00e7\u00f5es, voc\u00ea pode verificar a cria\u00e7\u00e3o das tabelas utilizando um sistema de gerenciamento de banco de dados. A seguir, apresentamos um exemplo com o Beekeeper Studio:

Lembre-se: Embora as tabelas estejam agora criadas e estruturadas, o banco de dados ainda n\u00e3o cont\u00e9m os dados anteriormente presentes no SQLite ou em qualquer outro banco que voc\u00ea estivesse utilizando antes.

"},{"location":"10/#simplificando-nosso-fluxo-com-docker-compose","title":"Simplificando nosso fluxo com docker-compose","text":"

Docker Compose \u00e9 uma ferramenta que permite definir e gerenciar aplicativos multi-cont\u00eainer com Docker. \u00c9 como se voc\u00ea tivesse um maestro conduzindo uma orquestra: o maestro (ou Docker Compose) garante que todos os m\u00fasicos (ou cont\u00eaineres) toquem em harmonia. Definimos nossa aplica\u00e7\u00e3o e servi\u00e7os relacionados, como o PostgreSQL, em um arquivo compose.yaml e os gerenciamos juntos atrav\u00e9s de comandos simplificados.

Ao adotar o Docker Compose, facilitamos o desenvolvimento e a execu\u00e7\u00e3o da nossa aplica\u00e7\u00e3o com seus servi\u00e7os dependentes utilizando um \u00fanico comando.

"},{"location":"10/#criacao-do-composeyaml","title":"Cria\u00e7\u00e3o do compose.yaml","text":"compose.yaml
services:\n  fastzero_database:\n    image: postgres\n    volumes:\n      - pgdata:/var/lib/postgresql/data\n    environment:\n      POSTGRES_USER: app_user\n      POSTGRES_DB: app_db\n      POSTGRES_PASSWORD: app_password\n    ports:\n      - \"5432:5432\"\n\n  fastzero_app:\n    image: fastzero_app\n    build: .\n    ports:\n      - \"8000:8000\"\n    depends_on:\n      - fastzero_database\n    environment:\n      DATABASE_URL: postgresql+psycopg://app_user:app_password@fastzero_database:5432/app_db\n\nvolumes:\n  pgdata:\n

Explica\u00e7\u00e3o linha a linha:

  1. services:: define os servi\u00e7os (cont\u00eaineres) que ser\u00e3o gerenciados.

  2. fastzero_database:: define nosso servi\u00e7o de banco de dados PostgreSQL.

  3. image: postgres: usa a imagem oficial do PostgreSQL.

  4. volumes:: mapeia volumes para persist\u00eancia de dados.

  5. pgdata:/var/lib/postgresql/data: cria ou usa um volume chamado \"pgdata\" e o mapeia para o diret\u00f3rio /var/lib/postgresql/data no cont\u00eainer.

  6. environment:: define vari\u00e1veis de ambiente para o servi\u00e7o.

  7. fastzero_app:: define o servi\u00e7o para nossa aplica\u00e7\u00e3o.

  8. image: fastzero_app: usa a imagem Docker da nossa aplica\u00e7\u00e3o.

  9. build:: instru\u00e7\u00f5es para construir a imagem se n\u00e3o estiver dispon\u00edvel, procura pelo Dockerfile em ..

  10. ports:: mapeia portas do cont\u00eainer para o host.

  11. \"8000:8000\": mapeia a porta 8000 do cont\u00eainer para a porta 8000 do host.

  12. depends_on:: especifica que fastzero_app depende de fastzero_database. Isto garante que o banco de dados seja iniciado antes da aplica\u00e7\u00e3o.

  13. DATABASE_URL: ...: \u00e9 uma vari\u00e1vel de ambiente que nossa aplica\u00e7\u00e3o usar\u00e1 para se conectar ao banco de dados. Aqui, ele se conecta ao servi\u00e7o fastzero_database que definimos anteriormente.

  14. volumes: (n\u00edvel superior): define volumes que podem ser usados pelos servi\u00e7os.

  15. pgdata:: define um volume chamado \"pgdata\". Este volume \u00e9 usado para persistir os dados do PostgreSQL entre as execu\u00e7\u00f5es do cont\u00eainer.

Sobre o docker-compose

Para usar o Docker Compose, voc\u00ea precisa t\u00ea-lo instalado em seu sistema. Ele n\u00e3o est\u00e1 inclu\u00eddo na instala\u00e7\u00e3o padr\u00e3o do Docker, ent\u00e3o lembre-se de instal\u00e1-lo separadamente!

O guia oficial de instala\u00e7\u00e3o pode ser encontrado aqui

Com este arquivo compose.yaml, voc\u00ea pode iniciar ambos os servi\u00e7os (aplica\u00e7\u00e3o e banco de dados) simultaneamente usando:

docker-compose up\n

Para parar os servi\u00e7os e manter os dados seguros nos volumes definidos, use:

docker-compose down\n

Esses comandos simplificam o fluxo de trabalho e garantem que os servi\u00e7os iniciem corretamente e se comuniquem conforme o esperado.

Execu\u00e7\u00e3o em modo desanexado

Voc\u00ea pode iniciar os servi\u00e7os em segundo plano com a flag -d usando docker-compose up -d. Isso permite que os cont\u00eaineres rodem em segundo plano, liberando o terminal para outras tarefas.

"},{"location":"10/#rodando-as-migracoes-de-forma-automatica","title":"Rodando as migra\u00e7\u00f5es de forma autom\u00e1tica","text":"

Automatizar as migra\u00e7\u00f5es do banco de dados \u00e9 uma pr\u00e1tica recomendada para garantir que sua aplica\u00e7\u00e3o esteja sempre sincronizada com o estado mais atual do seu esquema de banco de dados. \u00c9 como preparar todos os ingredientes antes de come\u00e7ar a cozinhar: voc\u00ea garante que tudo o que \u00e9 necess\u00e1rio est\u00e1 pronto para ser usado.

Para automatizar as migra\u00e7\u00f5es em nossos cont\u00eaineres Docker, utilizamos um entrypoint. O entrypoint define o comando que ser\u00e1 executado quando o cont\u00eainer iniciar. Em outras palavras, \u00e9 o primeiro ponto de entrada de execu\u00e7\u00e3o do cont\u00eainer.

Por que usar o Entrypoint?

No Docker, o entrypoint permite que voc\u00ea configure um ambiente de cont\u00eainer que ser\u00e1 executado como um execut\u00e1vel. \u00c9 \u00fatil para preparar o ambiente, como realizar migra\u00e7\u00f5es de banco de dados, antes de iniciar a aplica\u00e7\u00e3o propriamente dita. Isso significa que qualquer comando definido no CMD do Dockerfile n\u00e3o ser\u00e1 executado automaticamente se um entrypoint estiver definido. Em vez disso, precisamos incluir explicitamente esse comando no script de entrypoint.

Implementando o Entrypoint

Criamos um script chamado entrypoint.sh que ir\u00e1 preparar nosso ambiente antes de a aplica\u00e7\u00e3o iniciar:

entrypoint.sh
#!/bin/sh\n\n# Executa as migra\u00e7\u00f5es do banco de dados\npoetry run alembic upgrade head\n\n# Inicia a aplica\u00e7\u00e3o\npoetry run uvicorn --host 0.0.0.0 --port 8000 fast_zero.app:app\n

Explica\u00e7\u00e3o Detalhada do Script:

Como Funciona na Pr\u00e1tica?

Quando o cont\u00eainer \u00e9 iniciado, o Docker executa o script de entrypoint, que por sua vez executa as migra\u00e7\u00f5es e s\u00f3 ent\u00e3o inicia a aplica\u00e7\u00e3o. Isso garante que o banco de dados esteja atualizado com as \u00faltimas migra\u00e7\u00f5es antes de qualquer intera\u00e7\u00e3o com a aplica\u00e7\u00e3o.

Visualizando o Processo:

Voc\u00ea pode pensar no entrypoint.sh como o ato de aquecer e verificar todos os instrumentos antes de uma apresenta\u00e7\u00e3o musical. Antes de a m\u00fasica come\u00e7ar, cada instrumento \u00e9 afinado e testado. Da mesma forma, nosso script assegura que o banco de dados est\u00e1 em harmonia com a aplica\u00e7\u00e3o antes de ela come\u00e7ar a receber requisi\u00e7\u00f5es.

Adicionando o Entrypoint ao Docker Compose:

Inclu\u00edmos o entrypoint no nosso servi\u00e7o no arquivo compose.yaml, garantindo que esteja apontando para o script correto:

compose.yaml
  fastzero_app:\n    image: fastzero_app\n    entrypoint: ./entrypoint.sh\n    build: .\n

Reconstruindo e Executando com Novas Configura\u00e7\u00f5es:

Para aplicar as altera\u00e7\u00f5es, reconstru\u00edmos e executamos os servi\u00e7os com a op\u00e7\u00e3o --build:

$ Execu\u00e7\u00e3o no terminal!
docker-compose up --build\n

Observando o Comportamento Esperado:

Quando o cont\u00eainer \u00e9 iniciado, voc\u00ea deve ver as migra\u00e7\u00f5es sendo aplicadas, seguidas pela inicializa\u00e7\u00e3o da aplica\u00e7\u00e3o:

$ Exemplo do resultado no terminal!
fastzero_app-1  | INFO  [alembic.runtime.migration] Context impl PostgresqlImpl.\nfastzero_app-1  | INFO  [alembic.runtime.migration] Will assume transactional DDL.\nfastzero_app-1  | INFO:     Started server process [10]\nfastzero_app-1  | INFO:     Waiting for application startup.\nfastzero_app-1  | INFO:     Application startup complete.\nfastzero_app-1  | INFO:     Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)\n

Este processo garante que as migra\u00e7\u00f5es do banco de dados s\u00e3o realizadas automaticamente, mantendo a base de dados alinhada com a aplica\u00e7\u00e3o e pronta para a\u00e7\u00e3o assim que o servidor Uvicorn entra em cena.

Nota de revis\u00e3o sobre vari\u00e1veis de ambiente

Utilizar vari\u00e1veis de ambiente definidas em um arquivo .env \u00e9 uma pr\u00e1tica recomendada para cen\u00e1rios de produ\u00e7\u00e3o devido \u00e0 seguran\u00e7a que oferece. No entanto, para manter a simplicidade e o foco nas funcionalidades do FastAPI neste curso, optamos por explicitar essas vari\u00e1veis no compose.yaml. Isso \u00e9 particularmente relevante, pois o Docker Compose \u00e9 utilizado apenas para o ambiente de desenvolvimento; no deploy para fly.io, o qual \u00e9 o nosso foco, o compose n\u00e3o ser\u00e1 utilizado em produ\u00e7\u00e3o.

Ainda assim, \u00e9 valioso mencionar como essa configura\u00e7\u00e3o mais segura seria realizada, especialmente para aqueles que planejam utilizar o Docker Compose em produ\u00e7\u00e3o.

Em ambientes de produ\u00e7\u00e3o com Docker Compose, \u00e9 uma boa pr\u00e1tica gerenciar vari\u00e1veis de ambiente sens\u00edveis, como credenciais, por meio de um arquivo .env. Isso previne a exposi\u00e7\u00e3o dessas informa\u00e7\u00f5es diretamente no arquivo compose.yaml, contribuindo para a seguran\u00e7a do projeto.

As vari\u00e1veis de ambiente podem ser definidas em nosso arquivo .env localizado na raiz do projeto:

.env
POSTGRES_USER=app_user\nPOSTGRES_DB=app_db\nPOSTGRES_PASSWORD=app_password\nDATABASE_URL=postgresql+psycopg://app_user:app_password@fastzero_database:5432/app_db\n

Para aplicar essas vari\u00e1veis, referencie o arquivo .env no compose.yaml:

compose.yaml
services:\n  fastzero_database:\n    image: postgres\n    env_file:\n      - .env\n    # Restante da configura\u00e7\u00e3o...\n\n  fastzero_app:\n    build: .\n    env_file:\n      - .env\n    # Restante da configura\u00e7\u00e3o...\n

Adotar essa abordagem evita a exposi\u00e7\u00e3o das vari\u00e1veis de ambiente no arquivo de configura\u00e7\u00e3o. Esta n\u00e3o foi a abordagem padr\u00e3o no curso devido \u00e0 complexidade adicional e \u00e0 inten\u00e7\u00e3o de evitar confus\u00f5es. Dependendo do ambiente estabelecido pela equipe de DevOps/SRE em um projeto real, essa gest\u00e3o pode variar entre vari\u00e1veis de ambiente, arquivos .env ou solu\u00e7\u00f5es mais avan\u00e7adas como Vault.

Se optar por utilizar um arquivo .env com as configura\u00e7\u00f5es do PostgreSQL, configure o Pydantic para ignorar vari\u00e1veis de ambiente que n\u00e3o s\u00e3o necess\u00e1rias, adicionando extra='ignore' a chamada de SettingsConfigDic:

fast_zero/settings.py
from pydantic_settings import BaseSettings, SettingsConfigDict\n\n\nclass Settings(BaseSettings):\n    model_config = SettingsConfigDict(\n        env_file='.env', env_file_encoding='utf-8', extra='ignore'\n    )\n\n    DATABASE_URL: str\n    SECRET_KEY: str\n    ALGORITHM: str\n    ACCESS_TOKEN_EXPIRE_MINUTES: int\n

Com essa configura\u00e7\u00e3o, o Pydantic ir\u00e1 ignorar quaisquer vari\u00e1veis no .env que n\u00e3o sejam explicitamente declaradas na classe Settings, evitando assim conflitos e erros inesperados.

Agradecimentos especiais a @vcwild e @williangl pelas revis\u00f5es valiosas nesta aula que me fizeram criar essa nota.

Boas pr\u00e1ticas de inicializa\u00e7\u00e3o do banco de dados

Como esse \u00e9 um caso pensado em estudo, possivelmente n\u00e3o haver\u00e1 problemas relacionados \u00e0 inicializa\u00e7\u00e3o. Em um ambiente de produ\u00e7\u00e3o, por\u00e9m, n\u00e3o existe a garantia de que o postgres est\u00e1 pronto para uso no momento em que o entrypoint for executado. Seria necess\u00e1rio que, antes da execu\u00e7\u00e3o da migra\u00e7\u00e3o, o container do banco de dados tivesse a inicializa\u00e7\u00e3o finalizada.

Isso \u00e9 feito usando o campo healthcheck do compose.yaml:

compose.yaml
services:\n  fastzero_database:\n  image: postgres\n  volumes:\n    - pgdata:/var/lib/postgresql/data\n  environment:\n      POSTGRES_USER: app_user\n      POSTGRES_DB: app_db\n      POSTGRES_PASSWORD: app_password\n  ports:\n      - \"5432:5432\"\n  healthcheck:\n      test: [\"CMD-SHELL\", \"pg_isready\"]\n      interval: 5s\n      timeout: 5s\n      retries: 10\n

Dessa forma, ele ir\u00e1 executar o comando pg_isready a cada 5 segundos por 10 vezes. pg_isready \u00e9 um utilit\u00e1rio do PostgreSQL que verifica se ele j\u00e1 est\u00e1 operando e pronto para receber conex\u00f5es. Desta forma, a inicializa\u00e7\u00e3o do container s\u00f3 termina quando o postgres estiver ouvindo conex\u00f5es.

Dica do @ayharano nessa issue.

"},{"location":"10/#testes-e-docker","title":"Testes e Docker","text":"

Uma das partes importantes dos testes \u00e9 tentar chegar o mais pr\u00f3ximo poss\u00edvel do ambiente de desenvolvimento. Contudo, nessa aula, introduzimos uma depend\u00eancia que vai al\u00e9m do python, o postgres.

Isso pode tornar o nosso c\u00f3digo mais complicado de testar, por existir um DoC. Um \"componente dependente\" para ser executado. Nesse caso, por\u00e9m, \u00e9 interno ao sqlalchemy. Para usar o psycopg, temos uma depend\u00eancia externa ao python, o banco de dados precisa estar sendo executado, caso contr\u00e1rio os testes falhar\u00e3o.

"},{"location":"10/#executando-testes-com-o-banco-de-dados-em-um-container","title":"Executando testes com o banco de dados em um container","text":"

Os testes contemplam um ciclo de feedback positivo, eles t\u00eam que ser executados de forma r\u00e1pida e eficiente. Adicionar o container do Postgres a nossa aplica\u00e7\u00e3o, torna o processo de testes um pouco mais complexo. Pois existe uma depend\u00eancia ao n\u00edvel de sistema para os testes serem executados.

Come\u00e7aremos com o contraexemplo. Vamos alterar o comportamento da fixture do banco de dados para usar o postgres:

tests/conftest.py
from fast_zero.app import app\nfrom fast_zero.database import get_session\nfrom fast_zero.models import table_registry\nfrom fast_zero.settings import Settings\nfrom fast_zero.security import get_password_hash\nfrom tests.factories import UserFactory\n\n\n@pytest.fixture\ndef session():\n    engine = create_engine(Settings().DATABASE_URL)\n    table_registry.metadata.create_all(engine)\n\n    with Session(engine) as session:\n        yield session\n        session.rollback()\n\n    table_registry.metadata.drop_all(engine)\n

Com essa modifica\u00e7\u00e3o, agora estamos apontando para o PostgreSQL, conforme definido nas configura\u00e7\u00f5es da nossa aplica\u00e7\u00e3o (Settings().DATABASE_URL). A transi\u00e7\u00e3o do SQLite para o PostgreSQL \u00e9 facilitada pela abstra\u00e7\u00e3o fornecida pelo SQLAlchemy, que nos permite mudar de um banco para outro sem problemas.

\u00c9 importante notar que essa flexibilidade se deve ao fato de n\u00e3o termos utilizado recursos espec\u00edficos do PostgreSQL que n\u00e3o s\u00e3o suportados pelo SQLite. Caso contr\u00e1rio, a mudan\u00e7a poderia n\u00e3o ser t\u00e3o direta.

Partindo desse exemplo, para os testes serem executados, o banco de dados precisaria estar de p\u00e9. O que nos cobraria um container em execu\u00e7\u00e3o para os testes poderem rodar.

Por exemplo:

$ Execu\u00e7\u00e3o no terminal!
task test\n

Isso originaria esse erro:

============================= test session starts ==============================\nplatform linux -- Python 3.12.3, pytest-8.2.1, pluggy-1.5.0 -- /home/dunossauro/git/fastapi-do-zero/codigo_das_aulas/10/.venv/bin/python\ncachedir: .pytest_cache\nrootdir: /home/dunossauro/git/fastapi-do-zero/codigo_das_aulas/10\nconfigfile: pyproject.toml\nplugins: anyio-4.4.0, cov-5.0.0, Faker-25.4.0\ncollecting ... collected 28 items\n\ntests/test_app.py::test_root_deve_retornar_ok_e_ola_mundo ERROR\n\n==================================== ERRORS ====================================\n___________ ERROR at setup of test_root_deve_retornar_ok_e_ola_mundo ___________\n\nself = <sqlalchemy.engine.base.Connection object at 0x7ad981fb7380>\nengine = Engine(postgresql+psycopg://app_user:***@localhost:5432/app_db)\nconnection = None, _has_events = None, _allow_revalidate = True\n_allow_autobegin = True\n\n# ...\n        if not rv:\n            assert last_ex\n>           raise last_ex.with_traceback(None)\nE           psycopg.OperationalError: connection failed: connection to server at \"127.0.0.1\", port 5432 failed: Connection refused\nE               Is the server running on that host and accepting TCP/IP connections?\n\n.venv/lib/python3.12/site-packages/psycopg/connection.py:748: OperationalError\n

Obtivemos o erro psycopg.OperationalError: connection failed: connection to server at \"127.0.0.1\", port 5432 failed: Connection refused. Ele diz que ouve uma falha na comunica\u00e7\u00e3o com o nosso host na porta 5432. O endere\u00e7o que colocamos no .env. Para que ele fique acess\u00edvel, temos que iniciar o container antes de executar os testes.

Para isso:

$ Execu\u00e7\u00e3o no terminal!
docker-compose up -d fastzero_database #(1)!\n
  1. Inicia somente o container no banco de dados via docker compose

E em seguida executar os testes:

$ Execu\u00e7\u00e3o no terminal!
task test\n

Agora, sucesso. O resultado \u00e9 exatamente o que esper\u00e1vamos:

All checks passed!\n========== test session starts ==========\nplatform linux -- Python 3.12.3, pytest-8.2.1, pluggy-1.5.0 -- /.../10/.venv/bin/python\ncachedir: .pytest_cache\nrootdir: /home/dunossauro/git/fastapi-do-zero/codigo_das_aulas/10\nconfigfile: pyproject.toml\nplugins: anyio-4.4.0, cov-5.0.0, Faker-25.4.0\ncollected 28 items\n\ntests/test_app.py::test_root_deve_retornar_ok_e_ola_mundo PASSED\ntests/test_auth.py::test_get_token PASSED\ntests/test_auth.py::test_token_expired_after_time PASSED\ntests/test_auth.py::test_token_inexistent_user PASSED\ntests/test_auth.py::test_token_wrong_password PASSED\ntests/test_auth.py::test_refresh_token PASSED\ntests/test_auth.py::test_token_expired_dont_refresh PASSED\ntests/test_db.py::test_create_user PASSED\ntests/test_db.py::test_create_todo PASSED\ntests/test_security.py::test_jwt PASSED\ntests/test_todos.py::test_create_todo PASSED\ntests/test_todos.py::test_list_todos_should_return_5_todos PASSED\ntests/test_todos.py::test_list_todos_pagination_should_return_2_todos PASSED\ntests/test_todos.py::test_list_todos_filter_title_should_return_5_todos PASSED\ntests/test_todos.py::test_list_todos_filter_description_should_return_5_todos PASSED\ntests/test_todos.py::test_list_todos_filter_state_should_return_5_todos PASSED\ntests/test_todos.py::test_list_todos_filter_combined_should_return_5_todos PASSED\ntests/test_todos.py::test_patch_todo_error PASSED\ntests/test_todos.py::test_patch_todo PASSED\ntests/test_todos.py::test_delete_todo PASSED\ntests/test_todos.py::test_delete_todo_error PASSED\ntests/test_users.py::test_create_user PASSED\ntests/test_users.py::test_read_users PASSED\ntests/test_users.py::test_read_users_with_users PASSED\ntests/test_users.py::test_update_user PASSED\ntests/test_users.py::test_delete_user PASSED\ntests/test_users.py::test_update_user_with_wrong_user PASSED\ntests/test_users.py::test_delete_user_wrong_user PASSED\n\n---------- coverage: platform linux, python 3.12.3-final-0 -----------\nName                         Stmts   Miss  Cover\n------------------------------------------------\nfast_zero/__init__.py            0      0   100%\nfast_zero/app.py                11      0   100%\nfast_zero/database.py            7      2    71%\nfast_zero/models.py             29      0   100%\nfast_zero/routers/auth.py       26      0   100%\nfast_zero/routers/todos.py      50      0   100%\nfast_zero/routers/users.py      47      4    91%\nfast_zero/schemas.py            35      0   100%\nfast_zero/security.py           42      3    93%\nfast_zero/settings.py            7      0   100%\n------------------------------------------------\nTOTAL                          254      9    96%\n\n\n========== 28 passed in 4.94s ==========\nWrote HTML report to htmlcov/index.html\n

Embora essa seja uma abordagem que funciona, ela \u00e9 trabalhosa e temos que garantir que o container sempre esteja de p\u00e9. E como garantir isso durante a execu\u00e7\u00e3o dos testes?

"},{"location":"10/#containers-de-testes","title":"Containers de testes","text":"

Uma forma de interessante de usar containers em testes, \u00e9 usar containers espec\u00edficos para testes. Em python temos uma biblioteca chamada testcontainers.

TestContainers \u00e9 uma biblioteca que fornece uma interface python para executarmos os containers diretamente no c\u00f3digo dos testes. Voc\u00ea importa o c\u00f3digo referente a um container e ele te retorna todas as configura\u00e7\u00f5es para que voc\u00ea possa usar durante os testes. Desta forma, podemos controlar o fluxo de inicializa\u00e7\u00e3o/finaliza\u00e7\u00e3o dos containers diretamente no c\u00f3digo.

A biblioteca TestContainers tem diversas op\u00e7\u00f5es de containers, principalmente de bancos de dados. Como MariaDB, MongoDB, InfluxDB, etc. Tamb\u00e9m temos a op\u00e7\u00e3o de iniciar o PostgreSQL. Para isso, vamos instalar o testcontainters:

$ Execu\u00e7\u00e3o no terminal!
poetry add --group dev testcontainers\n

Com o testcontainers instalado iremos alterar a fixture de conex\u00e3o com o banco de dados, para usar um container que ser\u00e1 gerenciado pela fixture:

tests/conftest.py
from testcontainers.postgres import PostgresContainer #(1)!\n\n# ...\n\n@pytest.fixture\ndef session():\n    with PostgresContainer('postgres:16', driver='psycopg') as postgres: #(2)!\n        engine = create_engine(postgres.get_connection_url()) #(3)!\n        table_registry.metadata.create_all(engine)\n\n        with Session(engine) as session:\n            yield session\n            session.rollback()\n\n        table_registry.metadata.drop_all(engine)\n
  1. Faz o import do PostgresContainer dos testcontainers. O que quer dizer que ela ser\u00e1 iniciada somente uma vez durante toda a sess\u00e3o de testes.
  2. Cria um container de postgres na vers\u00e3o 16. Usando o psycopg como driver.
  3. get_connection_url() pega a URI do container postgres criado pelo testcontainers.

Agora, todas \u00e0s vezes em que a fixture de session for usada nos testes. Ser\u00e1 iniciado um novo container postgres na vers\u00e3o 16. E as intera\u00e7\u00f5es com o banco ser\u00e3o feitas nesse container.

Tudo pronto para execu\u00e7\u00e3o dos testes:

$ Execu\u00e7\u00e3o no terminal!
task test\n

Os testes devem ser executados com sucesso, mas algumas mensagens estranhas podem come\u00e7ar a aparecer entre o nome dos testes. Algo como:

$ Parte da resposta do comando de testes
tests/test_users.py::test_delete_user_wrong_user Pulling image postgres:16\nContainer started: beff0853dde0\nWaiting for container <Container: beff0853dde0> with image postgres:16 to be ready ...\nWaiting for container <Container: beff0853dde0> with image postgres:16 to be ready ...\nPASSED\n# ...\n========= 28 passed in 80.92s (0:01:20) =========\n

A mensagem Pulling image postgres:16 est\u00e1 dizendo que o container do postgres est\u00e1 sendo baixado do hub. Logo em seguida temos a mensagem Container started: beff0853dde0. Que diz que o container com id beff0853dde0 foi iniciado. Ap\u00f3s essa mensagem vemos o Waiting for container, que diz que est\u00e1 aguardando o container estar pronto para operar durante os testes.

Uma coisa preocupante nessa execu\u00e7\u00e3o \u00e9 a mensagem final: 28 passed in 80.92s (0:01:20). Embora todos os testes tenham sido executados com sucesso, levaram 80 segundos para serem executados (isso na minha m\u00e1quina).

Isso faz com que o tempo de feedback dos testes seja alto. Quando isso acontece, tendemos a executar menos os testes, por conta da demora. Ent\u00e3o, temos que melhorar esse tempo.

"},{"location":"10/#fixtures-de-sessao","title":"Fixtures de sess\u00e3o","text":"

As fixtures do pytest, por padr\u00e3o, s\u00e3o executadas todas \u00e0s vezes em que uma fun\u00e7\u00e3o de teste recebe a fixture como argumento:

c\u00f3digo de exemplo
@pytest.fixture\ndef fixture_de_exemplo():\n    # arrange\n    yield\n    # teardown\n\ndef teste_de_exemplo(fixture_de_exemplo): ...\n

Antes de executar o teste_de_exemplo, ser\u00e1 executado o c\u00f3digo da fixture at\u00e9 a instru\u00e7\u00e3o yield ser executada. A prepara\u00e7\u00e3o para o teste (arrange). Quando a fun\u00e7\u00e3o de teste \u00e9 finalizada, o bloco ap\u00f3s o yield \u00e9 executado. Chamamos ele de \"teardown\", para desfazer o efeito do \"arrage\". A volta do ambiente como era antes do \"arrange\".

Dizemos que uma fixture \"tradicional\" tem o escopo de fun\u00e7\u00e3o. Pois ela \u00e9 iniciada e finalizada em todas as fun\u00e7\u00f5es de teste.

Contudo, existem outros escopos, que precisam ser expl\u00edcitos durante a declara\u00e7\u00e3o da fixture, pelo par\u00e2metro scope. Existem diversos escopos:

Para resolver o problema com a lentid\u00e3o dos testes, iremos criar uma fixture para iniciar o container do banco de dados com o escopo \"session\".

sequenceDiagram\n    PytestRunner-->>Fixture: Executa a fixture at\u00e9 o yield\n    PytestRunner->>Testes: Executa todos os testes\n    Testes-->>Testes: Executa um teste\n    PytestRunner-->>Fixture: Executa a fixture depois do yield

Dessa forma, a fixture \u00e9 inicializada antes de todos os testes, est\u00e1 dispon\u00edvel durante a execu\u00e7\u00e3o das fun\u00e7\u00f5es, sendo finalizada ap\u00f3s a execu\u00e7\u00e3o de todos os testes.

"},{"location":"10/#fixture-para-engine","title":"Fixture para engine","text":"

Para resolver o problema da lentid\u00e3o, vamos criar nova fixture para a engine no escopo session. Ela ficar\u00e1 respons\u00e1vel por iniciar o container (arrange), criar a conex\u00e3o persistente com o postgres (yield) e desfazer o container ap\u00f3s a execu\u00e7\u00e3o de todos os testes (teardown):

tests/conftest.py
@pytest.fixture(scope='session')#(1)!\ndef engine():\n    with PostgresContainer('postgres:16', driver='psycopg') as postgres:\n\n        _engine = create_engine(postgres.get_connection_url())\n\n        with _engine.begin():#(2)!\n            yield _engine\n
  1. fixture sendo definida com o escopo 'session'.
  2. Inicia a conex\u00e3o com o banco de dados. A Session originalmente inicia a conex\u00e3o e a fecha. Contudo, como vamos criar diversas sessions, \u00e9 interessante que o controle da conex\u00e3o seja gerenciado pela engine.

Desta forma, por consequ\u00eancia, n\u00e3o iremos mais definir a engine na fixture de session. Usaremos a fixture de engine, que ser\u00e1 criada somente uma vez durante toda a execu\u00e7\u00e3o dos testes:

tests/conftest.py
@pytest.fixture\ndef session(engine):#(1)!\n    table_registry.metadata.create_all(engine)\n\n    with Session(engine) as session:\n        yield session\n        session.rollback()\n\n    table_registry.metadata.drop_all(engine)\n
  1. engine agora \u00e9 definida pela fixture de engine.

Com isso, podemos executar os testes novamente e devemos ver uma diferen\u00e7a significativa de tempo:

$ Execu\u00e7\u00e3o no terminal!
task test\n\n# ...\n========= 28 passed in 7.96s =========\n

Com o container sendo iniciado somente uma vez, o tempo total de execu\u00e7\u00e3o dos testes caiu para 7.96s, em compara\u00e7\u00e3o com os 80 segundos que t\u00ednhamos antes. Um tempo de feedback aceit\u00e1vel para execu\u00e7\u00e3o de testes.

Desta forma temos uma separa\u00e7\u00e3o do nosso container postgres de desenvolvimento, do container usado pelos testes. Fazendo com que a execu\u00e7\u00e3o dos testes n\u00e3o remova os dados inseridos durante o desenvolvimento da aplica\u00e7\u00e3o.

"},{"location":"10/#commit","title":"Commit","text":"

Para finalizar, ap\u00f3s criar nosso arquivo Dockerfile e compose.yaml, executar os testes e construir nosso ambiente, podemos fazer o commit das altera\u00e7\u00f5es no Git:

  1. Adicionando todos os arquivos modificados nessa aula com git add .
  2. Fa\u00e7a o commit das altera\u00e7\u00f5es com git commit -m \"Dockerizando nossa aplica\u00e7\u00e3o e inserindo o PostgreSQL\"
  3. Envie as altera\u00e7\u00f5es para o reposit\u00f3rio remoto com git push
"},{"location":"10/#conclusao","title":"Conclus\u00e3o","text":"

Dockerizar nossa aplica\u00e7\u00e3o FastAPI, com o PostgreSQL, nos permite garantir consist\u00eancia em diferentes ambientes. A combina\u00e7\u00e3o de Docker e Docker Compose simplifica o processo de desenvolvimento e implanta\u00e7\u00e3o. Na pr\u00f3xima aula, aprenderemos como levar nossa aplica\u00e7\u00e3o para o pr\u00f3ximo n\u00edvel executando os testes de forma remota com a integra\u00e7\u00e3o cont\u00ednua do GitHub Actions.

Agora que a aula acabou, \u00e9 um bom momento para voc\u00ea relembrar alguns conceitos e fixar melhor o conte\u00fado respondendo ao question\u00e1rio referente a ela.

Quiz

"},{"location":"11/","title":"Automatizando os testes com Integra\u00e7\u00e3o Cont\u00ednua","text":""},{"location":"11/#automatizando-os-testes-com-integracao-continua-ci","title":"Automatizando os testes com Integra\u00e7\u00e3o Cont\u00ednua (CI)","text":"

Objetivos da aula:

Caso prefira ver a aula em v\u00eddeo

Esse aula ainda n\u00e3o est\u00e1 dispon\u00edvel em formato de v\u00eddeo, somente em texto ou live!

Aula Slides C\u00f3digo Quiz

Na aula anterior, preparamos nossa aplica\u00e7\u00e3o para execu\u00e7\u00e3o em containers Docker, um passo fundamental para replicar o ambiente de produ\u00e7\u00e3o. Agora, vamos garantir que nossa aplica\u00e7\u00e3o mantenha sua integridade a cada mudan\u00e7a, implementando Integra\u00e7\u00e3o Cont\u00ednua.

"},{"location":"11/#integracao-continua-ci","title":"Integra\u00e7\u00e3o Cont\u00ednua (CI)","text":"

Integra\u00e7\u00e3o Cont\u00ednua (CI) \u00e9 uma pr\u00e1tica de desenvolvimento que envolve a integra\u00e7\u00e3o regular do c\u00f3digo-fonte ao reposit\u00f3rio principal, acompanhada de testes automatizados para garantir a qualidade. O objetivo dessa pr\u00e1tica \u00e9 identificar e corrigir erros de forma precoce, facilitando o desenvolvimento cont\u00ednuo e colaborativo. Pois, caso algu\u00e9m esque\u00e7a de rodar os testes ou exista algum problema na integra\u00e7\u00e3o entre dois commits, ou em algum merge, isso seja detectado no momento em que a integra\u00e7\u00e3o cont\u00ednua \u00e9 executada.

"},{"location":"11/#github-actions","title":"GitHub Actions","text":"

Entre as ferramentas dispon\u00edveis para CI, o GitHub Actions \u00e9 um servi\u00e7o do GitHub que automatiza workflows dentro do seu reposit\u00f3rio. Voc\u00ea pode configurar o GitHub Actions para executar a\u00e7\u00f5es espec\u00edficas \u2014 como testes automatizados \u2014 cada vez que um novo c\u00f3digo \u00e9 commitado no reposit\u00f3rio.

"},{"location":"11/#exemplo-de-workflow","title":"Exemplo de workflow","text":"

Workflows no GitHub Actions come\u00e7am com a constru\u00e7\u00e3o de um ambiente (escolher um sistema operacional e instalar suas depend\u00eancias) e criar diversos passos (steps em ingl\u00eas) para executar todas as etapas que fazemos no nosso computador durante o desenvolvimento. \u00c9 uma forma de garantir que o sistema funciona em um ambiente controlado. Dessa forma, todas \u00e0s vezes que subimos o c\u00f3digo para o reposit\u00f3rio (damos push) esse ambiente e a sequ\u00eancia de passos ser\u00e1 executada.

Por exemplo, como nosso sistema usar\u00e1 um sistema operacional GNU/Linux, podemos selecionar uma distribui\u00e7\u00e3o como Ubuntu para executar todos os passos da execu\u00e7\u00e3o dos nossos testes. Isso inclui diversas etapas como preparar o banco de dados, ler as vari\u00e1veis de ambiente, instalar o python e o poetry, etc.

Antes de mergulharmos na configura\u00e7\u00e3o do YAML, vamos visualizar o processo de constru\u00e7\u00e3o do nosso ambiente de CI com um fluxograma. Este diagrama mostra os passos essenciais, desde a instala\u00e7\u00e3o do Python at\u00e9 a execu\u00e7\u00e3o dos testes, ajudando a entender a sequ\u00eancia de opera\u00e7\u00f5es no GitHub Actions.

flowchart LR\n    Push -- Inicia --> Ubuntu\n    Ubuntu -- Execute os --> Passos\n    Ubuntu --> Z[Configure as vari\u00e1veis de ambiente]\n    subgraph Passos\n      A[Instale a vers\u00e3o 3.11 do Python] --> B[Copie os arquivos do reposit\u00f3rio para o ambiente]\n      B --> C[Instale o Poetry]\n      C --> D[Instale as depend\u00eancia do projeto com Poetry]\n      D --> E[Poetry execute os testes do projeto]\n    end

Com o fluxograma em mente, nosso objetivo de aula \u00e9 traduzir esses passos para a configura\u00e7\u00e3o pr\u00e1tica no GitHub Actions. Agora que temos uma vis\u00e3o clara do que nosso workflow envolve, nos aprofundaremos em como transformar essa teoria em pr\u00e1tica.

"},{"location":"11/#configurando-o-workflow-de-ci","title":"Configurando o workflow de CI","text":"

As configura\u00e7\u00f5es dos workflows no GitHub Actions s\u00e3o definidas em um arquivo YAML localizado em um path especificado pelo github no reposit\u00f3rio .github/workflows/. Dentro desse diret\u00f3rio podemos criar quantos workflows quisermos. Iniciaremos nossa configura\u00e7\u00e3o com um \u00fanico arquivo que chamaremos de pipeline.yaml:

Vers\u00e3o 3.11Vers\u00e3o 3.12 .github/workflows/pipeline.yaml
name: Pipeline\non: [push, pull_request]\n\njobs:\n  test:\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: Instalar o python\n        uses: actions/setup-python@v5\n        with:\n          python-version: '3.11'\n
.github/workflows/pipeline.yaml
name: Pipeline\non: [push, pull_request]\n\njobs:\n  test:\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: Instalar o python\n        uses: actions/setup-python@v5\n        with:\n          python-version: '3.12'\n

Basicamente um arquivo de workflow precisa de tr\u00eas componentes essenciais para serem definidos:

Nesse bloco de c\u00f3digo definimos que toda vez em que um push ou um pull_request ocorrer no nosso reposit\u00f3rio o Pipeline ser\u00e1 executado. Esse workflow tem um job chamado test que roda na \u00faltima vers\u00e3o do Ubuntu runs-on: ubuntu-latest. Nesse job chamado test temos uma lista de passos para serem executados, os steps.

O \u00fanico step que definimos \u00e9 a instala\u00e7\u00e3o do Python na vers\u00e3o \"3.11\":

Vers\u00e3o 3.11Vers\u00e3o 3.12
    steps:\n      - name: Instalar o python\n        uses: actions/setup-python@v5\n        with:\n          python-version: '3.11'\n
    steps:\n      - name: Instalar o python\n        uses: actions/setup-python@v5\n        with:\n          python-version: '3.12'\n

Nesse momento, se executarmos um commit do arquivo .github/workflows/pipeline.yaml e um push em nosso reposit\u00f3rio, um workflow ser\u00e1 iniciado.

$ Execu\u00e7\u00e3o no terminal!
git add .\ngit commit -m \"Instala\u00e7\u00e3o do Python\"\ngit push\n

Nisso, podemos ir at\u00e9 a p\u00e1gina do nosso reposit\u00f3rio no github e clicar na aba Actions, isso exibir\u00e1 todas \u00e0s vezes que um workflow for executado. Se clicarmos no wokflow seremos levados a p\u00e1gina dos jobs executados e se clicarmos nos jobs, temos uma descri\u00e7\u00e3o dos steps executados:

Guia Actions ->Jobs executados ->Steps executados

Isso nos mostra que tudo que configuramos no arquivo pipelines.yaml foi executado pelo actions no momento que em executamos um push no git.

Agora que temos essa vis\u00e3o geral de como o Actions monta e executa workflows, podemos nos concentrar em construir o nosso ambiente.

"},{"location":"11/#construcao-do-nosso-ambiente-de-ci","title":"Constru\u00e7\u00e3o do nosso ambiente de CI","text":"

Para executar nossos testes no workflow, precisamos seguir alguns passos essenciais:

  1. Instalar o Python: fundamental para executar nosso c\u00f3digo e testes, por ser a linguagem base da nossa aplica\u00e7\u00e3o.
  2. Instalar o Poetry: pois toda a nossa aplica\u00e7\u00e3o e depend\u00eancias foram constru\u00eddas com ele.
  3. Instalar as depend\u00eancias do projeto: far\u00e3o com que todas as bibliotecas necess\u00e1rias estejam dispon\u00edveis para a execu\u00e7\u00e3o dos testes.
  4. Executar os testes: passo crucial para validar a integridade e funcionalidade do c\u00f3digo antes de integr\u00e1-lo ao reposit\u00f3rio principal.
flowchart LR\n   Python[\"1: Python instalado\"] --> Poetry[\"2: Poetry instalado\"]\n   Poetry --> Deps[\"3: Instalar as depend\u00eancias via Poetry\"]\n   Deps --> Testes[\"4: Executar os testes via Poetry\"]

Cada um desses passos contribui para estabelecer um ambiente de CI robusto e confi\u00e1vel, assegurando que cada mudan\u00e7a no c\u00f3digo seja validada automaticamente, mantendo a qualidade e a estabilidade da nossa aplica\u00e7\u00e3o.

Para isso, devemos criar um step para cada uma dessas a\u00e7\u00f5es no nosso job test. Desta:

Vers\u00e3o 3.11Vers\u00e3o 3.12 .github/workflows/pipeline.yaml
    steps:\n      - name: Instalar o python\n        uses: actions/setup-python@v5\n        with:\n          python-version: '3.11'\n\n      - name: Instalar o poetry\n        run: pipx install poetry\n\n      - name: Instalar depend\u00eancias\n        run: poetry install\n\n      - name: Executar testes\n        run: poetry run task test\n
.github/workflows/pipeline.yaml
    steps:\n      - name: Instalar o python\n        uses: actions/setup-python@v5\n        with:\n          python-version: '3.12'\n\n      - name: Instalar o poetry\n        run: pipx install poetry\n\n      - name: Instalar depend\u00eancias\n        run: poetry install\n\n      - name: Executar testes\n        run: poetry run task test\n

Para testar essa implementa\u00e7\u00e3o no Actions, temos que fazer um commit1, para executar o trigger do CI:

$ Execu\u00e7\u00e3o no terminal!
git add .\ngit commit -m \"Adicionando passos para executar os testes no CI\"\ngit push\n

Assim, podemos avaliar o impacto desses passos no nosso workflow:

Se analisarmos com calma o resultado, veremos que a execu\u00e7\u00e3o do nosso workflow apresenta um erro de execu\u00e7\u00e3o. O erro est\u00e1 descrito na linha 12: Poetry could not find a pyproject.toml file in <path> or its parents. Se traduzirmos de maneira literal, a linha nos disse Poetry n\u00e3o encontrou o arquivo pyproject.toml no <path> ou em seus parentes.

Para solucionar esse problema, adicionaremos um passo antes da execu\u00e7\u00e3o dos testes para copiar o c\u00f3digo do nosso reposit\u00f3rio para o ambiente do workflow. O GitHub Actions oferece uma a\u00e7\u00e3o espec\u00edfica para isso, chamada actions/checkout. Vamos inclu\u00ed-la como o primeiro passo:

Vers\u00e3o 3.11Vers\u00e3o 3.12 .github/workflows/pipeline.yaml
jobs:\n  test:\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: Copia os arquivos do reposit\u00f3rio\n        uses: actions/checkout@v3\n\n      - name: Instalar o python\n        uses: actions/setup-python@v5\n        with:\n          python-version: '3.11'\n\n      # continua com os passos anteriormente definidos\n
.github/workflows/pipeline.yaml
jobs:\n  test:\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: Copia os arquivos do reposit\u00f3rio\n        uses: actions/checkout@v3\n\n      - name: Instalar o python\n        uses: actions/setup-python@v5\n        with:\n          python-version: '3.12'\n\n      # continua com os passos anteriormente definidos\n

Para testar a execu\u00e7\u00e3o desse passo faremos um novo commit para triggar o Actions:

$ Execu\u00e7\u00e3o no terminal!
git add .\ngit commit -m \"Adicionando o checkout ao pipeline\"\ngit push\n

Com isso, o erro anterior deve ser resolvido e teremos os testes sendo executados no workflow:

Ap\u00f3s resolver este problema, nos deparamos com outro desafio. Evidenciado no bloco a seguir:

Erro do CI!
ImportError while loading conftest '/home/runner/work/<path>/tests/conftest.py'.\ntests/conftest.py:6: in <module>\n    from fast_zero.app import app\nfast_zero/app.py:3: in <module>\n    from fast_zero.routes import auth, todos, users\nfast_zero/routes/auth.py:8: in <module>\n    from fast_zero.database import get_session\nfast_zero/database.py:6: in <module>\n    engine = create_engine(Settings().DATABASE_URL)\n../../../.cache/pypoetry/virtualenvs/fast-zero-IubsqyUK-py3.11/lib/python3.11/site-packages/pydantic_settings/main.py:61: in __init__\n    super().__init__(\nE   pydantic_core._pydantic_core.ValidationError: 4 validation errors for Settings\nE   DATABASE_URL\nE     Field required [type=missing, input_value={}, input_type=dict]\nE       For further information visit https://errors.pydantic.dev/2.1.2/v/missing\n
Erro completo no CI

Ao iniciar a execu\u00e7\u00e3o dos testes, encontramos um erro relacionado \u00e0 nossa classe settings.Settings. Isso ocorreu porque as vari\u00e1veis de ambiente necess\u00e1rias, como DATABASE_URL, n\u00e3o estavam definidas no workflow do CI. Este problema \u00e9 comum quando as vari\u00e1veis do arquivo .env, que utilizamos localmente, n\u00e3o s\u00e3o transferidas para o ambiente de CI.

"},{"location":"11/#configuracao-de-variaveis-de-ambiente-no-actions","title":"Configura\u00e7\u00e3o de vari\u00e1veis de ambiente no Actions","text":"

Como vimos anteriormente, nossa configura\u00e7\u00e3o de CI encontrou um problema devido \u00e0 aus\u00eancia de vari\u00e1veis de ambiente. Para resolver isso, utilizaremos uma funcionalidade dos reposit\u00f3rios do GitHub chamada 'Secrets'. Os 'Secrets' s\u00e3o uma maneira segura de armazenar informa\u00e7\u00f5es confidenciais, como vari\u00e1veis de ambiente, de forma criptografada. Eles s\u00e3o acess\u00edveis dentro do nosso workflow, permitindo que o GitHub Actions utilize esses valores sem exp\u00f4-los publicamente.

"},{"location":"11/#definindo-secrets-no-repositorio","title":"Definindo Secrets no Reposit\u00f3rio","text":"

Para definirmos as vari\u00e1veis de ambiente como 'Secrets', temos duas alternativas. A primeira \u00e9 acessar a aba Settings -> Secrets and variables do nosso reposit\u00f3rio no GitHub. Neste local, podemos inserir manualmente cada 'Secret', como URLs de banco de dados e chaves secretas.

A segunda alternativa \u00e9 utilizar o CLI do GitHub (gh) para adicionar todas as vari\u00e1veis de ambiente que temos no nosso arquivo .env. Isso pode ser feito com o seguinte comando:

$ Execu\u00e7\u00e3o no terminal!
gh secret set -f .env\n

Este comando pega todas as vari\u00e1veis de ambiente do arquivo .env e as configura como 'Secrets' no seu reposit\u00f3rio GitHub.

Configurando Secrets pela Interface Web

Se preferir configurar 'Secrets' pela interface web do GitHub, siga estes passos:

1 - Acesse Settings no seu reposit\u00f3rio2 - Adicione um novo segredo3 - Visualiza\u00e7\u00e3o dos segredos

Acesse Settings no seu reposit\u00f3rio GitHub. Em seguida clique na guia \"Secrets and variables\". Ap\u00f3s isso clique em \"New Repository secret\":

Para adicionar um novo scregredo no campo Name colocamos o nome de um de nossas vari\u00e1veis de ambientes. No campo Secret adicione o valor de uma vari\u00e1vel. Como, por exemplo:

Em seguida clique em Add secret.

Ap\u00f3s adicionar todos os segredos, sua p\u00e1gina de segredos deve se parecer com isso:

"},{"location":"11/#implementacao-no-arquivo-yaml","title":"Implementa\u00e7\u00e3o no Arquivo YAML","text":"

Ap\u00f3s definir as 'Secrets', o pr\u00f3ximo passo \u00e9 integr\u00e1-las ao nosso arquivo de workflow (.github/workflows/pipeline.yaml). Aqui, utilizamos uma sintaxe especial para acessar os valores armazenados como 'Secrets'. Cada 'Secret' \u00e9 mapeado para uma vari\u00e1vel de ambiente no job do nosso workflow, tornando esses valores seguros e acess\u00edveis durante a execu\u00e7\u00e3o do workflow. Vejamos como isso \u00e9 feito:

.github/workflows/pipeline.yaml
jobs:\n  test:\n    runs-on: ubuntu-latest\n\n    env:\n      DATABASE_URL: ${{ secrets.DATABASE_URL }}\n      SECRET_KEY: ${{ secrets.SECRET_KEY }}\n      ALGORITHM: ${{ secrets.ALGORITHM }}\n      ACCESS_TOKEN_EXPIRE_MINUTES: ${{ secrets.ACCESS_TOKEN_EXPIRE_MINUTES }}\n

Neste trecho de c\u00f3digo, a sintaxe ${{ secrets.NOME_DA_VARIAVEL }} \u00e9 usada para referenciar os 'Secrets' que definimos no reposit\u00f3rio. Por exemplo, secrets.DATABASE_URL buscar\u00e1 o valor da 'Secret' chamada DATABASE_URL que definimos. Assim que o workflow \u00e9 acionado, esses valores s\u00e3o injetados no ambiente do job, permitindo que nosso c\u00f3digo os acesse como vari\u00e1veis de ambiente normais.

Essa abordagem n\u00e3o s\u00f3 mant\u00e9m nossos dados confidenciais seguros, mas tamb\u00e9m nos permite gerenciar configura\u00e7\u00f5es sens\u00edveis de forma centralizada, facilitando atualiza\u00e7\u00f5es e manuten\u00e7\u00e3o.

"},{"location":"11/#atualizando-o-workflow","title":"Atualizando o Workflow","text":"

Com as 'Secrets' agora configuradas, precisamos atualizar o nosso workflow para incorporar essas mudan\u00e7as. Isso \u00e9 feito por meio de um novo commit e push para o reposit\u00f3rio, que acionar\u00e1 o workflow com as novas configura\u00e7\u00f5es.

$ Execu\u00e7\u00e3o no terminal!
git add .\ngit commit -m \"Adicionando as vari\u00e1veis de ambiente para o CI\"\ngit push\n

A execu\u00e7\u00e3o do workflow com as novas 'Secrets' nos permitir\u00e1 verificar se os problemas anteriores foram resolvidos.

E SIM, tudo funcionou como esper\u00e1vamos \ud83c\udf89

Agora a cada novo commit ou PR em nossa aplica\u00e7\u00e3o, os testes ser\u00e3o executados para garantir que a integra\u00e7\u00e3o pode acontecer sem problemas.

"},{"location":"11/#conclusao","title":"Conclus\u00e3o","text":"

Atrav\u00e9s deste m\u00f3dulo sobre Integra\u00e7\u00e3o Cont\u00ednua com GitHub Actions, ganhamos uma compreens\u00e3o s\u00f3lida de como a CI \u00e9 vital no desenvolvimento moderno de software. Vimos como o GitHub Actions, uma ferramenta poderosa e vers\u00e1til, pode ser utilizada para automatizar nossos testes e garantir a qualidade e estabilidade do c\u00f3digo a cada commit. Esta pr\u00e1tica n\u00e3o apenas otimiza nosso fluxo de trabalho, mas tamb\u00e9m nos ajuda a identificar e resolver problemas precocemente.

No pr\u00f3ximo m\u00f3dulo, o foco ser\u00e1 na prepara\u00e7\u00e3o da nossa aplica\u00e7\u00e3o FastAPI para o deployment em produ\u00e7\u00e3o. Exploraremos as etapas necess\u00e1rias e as melhores pr\u00e1ticas para tornar nossa aplica\u00e7\u00e3o pronta para o uso no mundo real, abordando desde configura\u00e7\u00f5es at\u00e9 estrat\u00e9gias de deployment eficazes.

Agora que a aula acabou, \u00e9 um bom momento para voc\u00ea relembrar alguns conceitos e fixar melhor o conte\u00fado respondendo ao question\u00e1rio referente a ela.

Quiz

  1. H\u00e1 alternativas para testar o workflow de CI sem fazer um commit, como a ferramenta Act que simula a execu\u00e7\u00e3o do workflow localmente usando Docker.\u00a0\u21a9

"},{"location":"12/","title":"Fazendo deploy no Fly.io","text":""},{"location":"12/#fazendo-deploy-no-flyio","title":"Fazendo deploy no Fly.io","text":"

Objetivos da aula:

Caso prefira ver a aula em v\u00eddeo

Esse aula ainda n\u00e3o est\u00e1 dispon\u00edvel em formato de v\u00eddeo, somente em texto ou live!

Aula Slides C\u00f3digo Quiz

Agora que temos uma API criada com integra\u00e7\u00e3o ao banco de dados e testes sendo executados via integra\u00e7\u00e3o cont\u00ednua. Chegou a t\u00e3o esperada hora de colocar nossa aplica\u00e7\u00e3o em produ\u00e7\u00e3o para que todas as pessoas possam acess\u00e1-la. Colocaremos nossa aplica\u00e7\u00e3o em produ\u00e7\u00e3o usando um servi\u00e7o de PaaS, chamado Fly.io.

"},{"location":"12/#o-flyio","title":"O Fly.io","text":"

O Fly.io \u00e9 uma plataforma de deploy que nos permite lan\u00e7ar nossas aplica\u00e7\u00f5es na nuvem e que oferece servi\u00e7os para diversas linguagens de programa\u00e7\u00e3o e frameworks como Python e Django, PHP e Laravel, Ruby e Rails, Elixir e Phoenix, etc.

Ao mesmo tempo, em que permite que o deploy de aplica\u00e7\u00f5es em containers docker tamb\u00e9m possam ser utilizadas, como \u00e9 o nosso caso. Al\u00e9m disso, o Fly disponibiliza bancos de dados para serem usados em nossas aplica\u00e7\u00f5es, como PostgreSQL e Redis.

O motivo pela escolha do Fly \u00e9 que ele permite que fa\u00e7amos deploys de aplica\u00e7\u00f5es em desenvolvimento / provas de conceito de forma gratuita - o que usaremos para \"colocar nossa aplica\u00e7\u00e3o no mundo\".

Para fazer o uso do fly.io \u00e9 necess\u00e1rio que voc\u00ea crie uma conta no servi\u00e7o.

"},{"location":"12/#flyclt","title":"Flyclt","text":"

Uma das formas de interagir com a plataforma \u00e9 via uma aplica\u00e7\u00e3o de linha de comando disponibilizada pelo Fly, o flyctl.

O flyctl precisa ser instalado em seu computador. Em algumas distribui\u00e7\u00f5es linux o flyctl est\u00e1 dispon\u00edvel nos reposit\u00f3rios de aplica\u00e7\u00f5es. Para Mac/Windows ou distribui\u00e7\u00f5es linux que n\u00e3o contam com o pacote no reposit\u00f3rio, voc\u00ea pode seguir o guia de instala\u00e7\u00e3o oficial.

Ap\u00f3s a instala\u00e7\u00e3o, voc\u00ea pode verificar se o flyctl est\u00e1 instalado em seu sistema operacional digitando o seguinte comando no terminal:

$ Execu\u00e7\u00e3o no terminal!
flyctl version\n\nflyctl v0.1.134 linux/amd64 Commit: ... BuildDate: 2023-12-08T18:58:44Z\n

A vers\u00e3o instalada no meu sistema \u00e9 a 0.1.134. No momento da sua instala\u00e7\u00e3o, voc\u00ea pode se deparar com uma vers\u00e3o mais recente do que a minha no momento, mas os comandos devem funcionar da mesma forma em qualquer vers\u00e3o menor que 0.2.0.

"},{"location":"12/#fazendo-login-via-terminal","title":"Fazendo login via terminal","text":"

Ap\u00f3s a instala\u00e7\u00e3o do flyctl \u00e9 importante que voc\u00ea efetue o login usando suas credenciais, para que o flyctl consiga vincular suas credenciais com a linha de comando. Para isso podemos executar o seguinte comando:

$ Execu\u00e7\u00e3o no terminal!
flyctl auth login\nOpening https://fly.io/app/auth/cli/91283719231023123 ...\n\nWaiting for session...\n

Isso abrir\u00e1 uma janela em seu browser pedindo que voc\u00ea efetue o login:

Ap\u00f3s inserir suas credenciais, voc\u00ea pode fechar o browser e no shell a execu\u00e7\u00e3o do comando terminar\u00e1 mostrando a conta em que voc\u00ea est\u00e1 logado:

$ Continua\u00e7\u00e3o da resposta do terminal
Waiting for session... Done\nsuccessfully logged in as <seu-email@de-login.com>\n

Desta forma, toda a configura\u00e7\u00e3o necess\u00e1ria para o iniciar o deploy est\u00e1 pronta!

"},{"location":"12/#configuracoes-para-o-deploy","title":"Configura\u00e7\u00f5es para o deploy","text":"

Agora com o flyctl devidamente configurado. Podemos iniciar o processo de lan\u00e7amento da nossa aplica\u00e7\u00e3o. O flyctl tem um comando espec\u00edfico para lan\u00e7amento, o launch. Contudo, o comando launch \u00e9 bastante interativo e ao final dele, o deploy da aplica\u00e7\u00e3o \u00e9 executado. Para evitar o deploy no primeiro momento, pois ainda existem coisas para serem configuradas, vamos execut\u00e1-lo da seguinte forma:

$ Execu\u00e7\u00e3o no terminal!
flyctl launch --no-deploy\n

Como resultado desse comando, o flyctl iniciar\u00e1 o modo interativo e exibir\u00e1 uma resposta pr\u00f3xima a essa:

$ Resposta do comando `launch`
Detected a Dockerfile app\nCreating app in /home/dunossauro/ci-example-fastapi\nWe're about to launch your app on Fly.io. Here's what you're getting:\n\nOrganization: <Seu Nome>             (fly launch defaults to the personal org)\nName:         fast-zero              (derived from your directory name)\nRegion:       Sao Paulo, Brazil      (this is the fastest region for you)\nApp Machines: shared-cpu-1x, 1GB RAM (most apps need about 1GB of RAM)\nPostgres:     <none>                 (not requested)\nRedis:        <none>                 (not requested)\n\n? Do you want to tweak these settings before proceeding? (y/N) \n

Nesse texto est\u00e3o destacadas as configura\u00e7\u00f5es padr\u00f5es do Fly. Como a Regi\u00e3o onde seu deploy ser\u00e1 feito (Sao Paulo, Brazil, o mais pr\u00f3ximo a mim nesse momento), a configura\u00e7\u00e3o da m\u00e1quina do deploy App Machines: shared-cpu-1x, 1GB RAM e a op\u00e7\u00e3o padr\u00e3o do Postgres: Postgres: <none>.

Uma nota sobre o Postgres + Fly

Antes de avan\u00e7armos, \u00e9 importante mencionar uma especificidade do Fly.io: para criar uma inst\u00e2ncia do PostgreSQL, a plataforma requer que um cart\u00e3o de cr\u00e9dito seja fornecido. Esta \u00e9 uma medida de seguran\u00e7a adotada para evitar o uso indevido de seus servi\u00e7os, como a execu\u00e7\u00e3o de ferramentas de minera\u00e7\u00e3o. Apesar dessa exig\u00eancia, o servi\u00e7o de PostgreSQL \u00e9 oferecido de forma gratuita. Mais detalhes podem ser encontrados neste artigo.

Caso voc\u00ea n\u00e3o adicione um cart\u00e3o, o erro levatado pelo flyctl est\u00e1 descrito na issue #73

A pergunta feita ao final dessa se\u00e7\u00e3o Do you want to tweak these settings before proceeding? pode ser traduzida como: Voc\u00ea deseja ajustar essas configura\u00e7\u00e3o antes de prosseguir?. Diremos que sim, digitando Y e em seguida Enter.

Assim, a configura\u00e7\u00e3o do lan\u00e7amento deve avan\u00e7ar e travar novamente com um texto parecido com esse:

$ Continua\u00e7\u00e3o do comando `launch`
? Do you want to tweak these settings before proceeding? Yes\nOpening https://fly.io/cli/launch/59f08b31a5efd30bdf5536ac516de5ga ...\n\nWaiting for launch data...\u28fd\n

Nesse momento, ele abrir\u00e1 o browser novamente exibira uma tela de ajustes de configura\u00e7\u00f5es:

Nesse momento faremos alguns ajustes em nossa configura\u00e7\u00e3o:

  1. Se\u00e7\u00e3o Basics: adicionaremos o nome da nossa aplica\u00e7\u00e3o no Fly. (Usarei fastzeroapp)
  2. Se\u00e7\u00e3o Memory & CPU: alteraremos o campo VM Memory para 256MB
  3. Se\u00e7\u00e3o Database:
    • alteraremos o campo Postgres para Fly Postgres
    • criaremos um nome para o nosso servi\u00e7o de banco de dados. (Usarei fastzerodb)
    • no campo Configuration alteraremos para Development - Single node, 1x shared CPU, 256MB RAM, 1GB disk
  4. Por fim, clicamos em Confirm Settings!

Ap\u00f3s esse ajuste, voc\u00ea pode fechar a janela do browser e voltar ao terminal, pois a parte interativa do launch ainda estar\u00e1 em execu\u00e7\u00e3o. Como a resposta a seguir \u00e9 bastante grande, colocarei ... para pular algumas linhas que n\u00e3o nos interessam nesse momento:

$ Continua\u00e7\u00e3o do comando `launch`
Created app 'fastzeroapp' in organization 'personal'\nAdmin URL: https://fly.io/apps/fastzeroapp\nHostname: fastzeroapp.fly.dev\nCreating postgres cluster in organization personal\nCreating app...\n\n...\n\nPostgres cluster fastzerodb created\n  Username:    postgres\n  Password:    t0Vf35P21eDlIVS\n  Hostname:    fastzerodb.internal\n  Flycast:     fdaa:2:77b0:0:1::a\n  Proxy port:  5432\n  Postgres port:  5433\n  Connection string: postgres://postgres:t0Vf35P21eDlIVS@fastzerodb.flycast:5432\n\n...\n\nPostgres cluster fastzerodb is now attached to fastzeroapp\nThe following secret was added to fastzeroapp:\n  DATABASE_URL=postgres://fastzeroapp:zHgBlc6JNaslGtz@fastzerodb.flycast:5432/fastzeroapp?sslmode=disable\nPostgres cluster fastzerodb is now attached to fastzeroapp\n? Create .dockerignore from .gitignore files? (y/N)\n

Nas linhas em destaque, vemos que o Fly se encarregou de criar um dashboard para vermos o status atual da nossa aplica\u00e7\u00e3o (https://fly.io/apps/nome-do-seu-app), inicializou um banco de dados postgres para usarmos em conjunto com nossa aplica\u00e7\u00e3o e tamb\u00e9m adicionou a url do banco de dados a vari\u00e1vel de ambiente DATABASE_URL com a configura\u00e7\u00e3o do postgres referente a nossa aplica\u00e7\u00e3o.

A Connection string do banco de dados deve ser armazenada por voc\u00ea, essa informa\u00e7\u00e3o n\u00e3o ser\u00e1 disponibilizada novamente, nem mesmo na parte web do Fly. Por isso guarde-a com cuidado e n\u00e3o compartilhem de forma alguma.

Assim sendo, para prosseguir com o launch devemos responder a seguinte pergunta: Create .dockerignore from .gitignore files? (y/N), que pode ser traduzida como Crie um .dockerignore partindo do arquivo .gitignore?. Vamos novamente responder que sim. Digitando Y e em seguida Enter.

$ Continua\u00e7\u00e3o do comando `launch`
Created <seu-path>/.dockerignore from 6 .gitignore files.\nWrote config file fly.toml\nValidating <seu-path>/fly.toml\nPlatform: machines\n\u2713 Configuration is valid\nYour app is ready! Deploy with `flyctl deploy`\n

Agora o flyctl criou um arquivo .dockerignore que n\u00e3o copia os arquivos do .gitignore para dentro do container docker e tamb\u00e9m criou um arquivo de configura\u00e7\u00e3o do Fly, o arquivo fly.toml.

Na \u00faltima linha ele nos disse que nossa aplica\u00e7\u00e3o est\u00e1 pronta para o deploy. Mas ainda temos mais configura\u00e7\u00f5es a fazer!

"},{"location":"12/#configuracao-dos-segredos","title":"Configura\u00e7\u00e3o dos segredos","text":"

Para que nossa aplica\u00e7\u00e3o funcione de maneira adequada, todas as vari\u00e1veis de ambiente precisam estar configuradas no ambiente. O flyctl tem um comando para vermos as vari\u00e1veis que j\u00e1 foram definidas no ambiente e tamb\u00e9m para definir novas. O comando secrets.

Para vermos as vari\u00e1veis j\u00e1 configuradas no ambiente, podemos executar o seguinte comando:

$ Execu\u00e7\u00e3o no terminal!
flyctl secrets list\n\nNAME            DIGEST              CREATED AT\nDATABASE_URL    f803df294e7326fa    22m43s ago\n

Uma coisa que podemos notar na resposta do secrets \u00e9 que ele leu nosso arquivo .env e adicionou a vari\u00e1vel de ambiente DATABASE_URL com base no postgres que foi criado durante o comando launch. Um ponto de aten\u00e7\u00e3o que devemos tomar nesse momento, \u00e9 que a vari\u00e1vel criada \u00e9 iniciada com o prefixo postgres://. Para que o sqlalchemy reconhe\u00e7a esse endere\u00e7o como v\u00e1lido, o prefixo deve ser alterado para postgresql+psycopg://. Para isso, usaremos a url fornecida pelo comando launch e alterar o prefixo.

Desta forma, podemos registrar a vari\u00e1vel de ambiente DATABASE_URL novamente. Agora com o valor correto:

$ Execu\u00e7\u00e3o no terminal!
flyctl secrets set DATABASE_URL=postgresql+psycopg://postgres:t0Vf35P21eDlIVS@fastzerodb.flycast:5432\nSecrets are staged for the first deployment\n

Contudo, n\u00e3o \u00e9 somente a vari\u00e1vel de ambiente do postgres que \u00e9 importante para que nossa aplica\u00e7\u00e3o seja executada. Temos que adicionar as outras vari\u00e1veis contidas no nosso .env ao Fly.

Iniciaremos adicionando a vari\u00e1vel ALGORITHM:

$ Execu\u00e7\u00e3o no terminal!
flyctl secrets set ALGORITHM=\"HS256\"\nSecrets are staged for the first deployment\n

Seguida pela vari\u00e1vel SECRET_KEY:

$ Execu\u00e7\u00e3o no terminal!
flyctl secrets set SECRET_KEY=\"your-secret-key\"\nSecrets are staged for the first deployment\n

E por fim a vari\u00e1vel ACCESS_TOKEN_EXPIRE_MINUTES:

$ Execu\u00e7\u00e3o no terminal!
flyctl secrets set ACCESS_TOKEN_EXPIRE_MINUTES=30\nSecrets are staged for the first deployment\n

Com isso, todos os segredos da nossa aplica\u00e7\u00e3o j\u00e1 est\u00e3o configurados no nosso ambiente do Fly. Agora podemos partir para o nosso t\u00e3o aguardado deploy.

"},{"location":"12/#deploy-da-aplicacao","title":"Deploy da aplica\u00e7\u00e3o","text":"

Para efetuarmos o deploy da aplica\u00e7\u00e3o, podemos usar o comando deploy doflyctl. Uma coisa interessante nessa parte do processo \u00e9 que o Fly pode fazer o deploy de duas formas:

  1. Copiar seus arquivos e fazer o build do docker na nuvem;
  2. Voc\u00ea pode fazer o build localmente e subir apenas o container para um reposit\u00f3rio dispon\u00edvel no Fly.

Optaremos por fazer o build localmente para n\u00e3o serem alocadas duas m\u00e1quinas em nossa aplica\u00e7\u00e3o1. Para executar o build localmente usamos a flag --local-only.

O Fly sobre duas inst\u00e2ncias por padr\u00e3o da nossa aplica\u00e7\u00e3o para melhorar a disponibilidade do app. Como vamos nos basear no uso gratuito, para todos poderem executar o deploy, adicionaremos a flag --ha=false ao deploy. Para desativamos a alta escalabilidade:

$ Execu\u00e7\u00e3o no terminal!
fly deploy --local-only --ha=false\n

Como a resposta do comando deploy \u00e9 bastante grande, substituirei o texto por ... para pular algumas linhas que n\u00e3o nos interessam nesse momento:

$ Resultado do comando `deploy`
==> Verifying app config\nValidating /home/dunossauro/ci-example-fastapi/fly.toml\nPlatform: machines\n\u2713 Configuration is valid\n--> Verified app config\n==> Building image\n==> Building image with Docker\n...\n => exporting to image                                                  0.0s\n => => exporting layers                                                 0.0s\n => => writing image sha256:b95a9d9f8abcea085550449a720a0bb9176e195fe4  0.0s\n => => naming to registry.fly.io/fastzeroapp:deployment-01HHKKDMF87FN4  0.0s\n--> Building image done\n==> Pushing image to fly\nThe push refers to repository [registry.fly.io/fastzeroapp]\n...\ndeployment-01HHKKDMF87FN441VA6H0JR4BS: digest: sha256:153a13e2931f923ab60df7e9dd0f18e2cc89fff7833ac18443935c7d0763a329 size: 2419\n--> Pushing image done\nimage: registry.fly.io/fastzeroapp:deployment-01HHKKDMF87FN441VA6H0JR4BS\nimage size: 349 MB\n\nWatch your deployment at https://fly.io/apps/fastzeroapp/monitoring\n\n-------\nUpdating existing machines in 'fastzeroapp' with rolling strategy\n\n-------\n \u2714 Machine 1781551ad22489 [app] update succeeded\n-------\n\nVisit your newly deployed app at https://fastzeroapp.fly.dev/\n

As primeiras linhas da resposta est\u00e3o relacionadas ao build do docker e a publica\u00e7\u00e3o no reposit\u00f3rio de imagens docker do Fly.

Na sequ\u00eancia, temos algumas informa\u00e7\u00f5es importantes a respeito do deploy da nossa aplica\u00e7\u00e3o. Como a URL de monitoramento (https://fly.io/apps/<nome-do-app>/monitoring), o aviso de que o deploy foi efetuado com sucesso (Machine 1781551ad22489 [app] update succeeded) e por fim, a URL de acesso a nossa aplica\u00e7\u00e3o (https://<nome-do-app>.fly.dev/).

Dessa forma podemos acessar a nossa aplica\u00e7\u00e3o acessando a URL fornecida pela \u00faltima linha de resposta em nosso browser, como https://fastzeroapp.fly.dev/:

E pronto, nossa aplica\u00e7\u00e3o est\u00e1 dispon\u00edvel para acesso! Obtivemos o nosso \"Ol\u00e1 mundo\". \ud83d\ude80\ud83d\ude80\ud83d\ude80\ud83d\ude80\ud83d\ude80\ud83d\ude80

Por\u00e9m, contudo, entretanto, ainda existe um problema na nossa aplica\u00e7\u00e3o no ar. Para ficar evidente, tente acessar o swagger da sua aplica\u00e7\u00e3o no ar e registrar um usu\u00e1rio usando o endpoint /user com o m\u00e9todo POST:

Voc\u00ea receber\u00e1 uma mensagem de erro, um erro 500: Internal Server Error, por de n\u00e3o efetuarmos a migra\u00e7\u00e3o no banco de dados de produ\u00e7\u00e3o. Por\u00e9m, para ter certeza disso, podemos usar a URL de monitoramento do Fly para ter certeza do erro ocorrido. Acessando: https://fly.io/apps/<nome-do-app>/monitoring, podemos visualizar os erros exibidos no console da nossa aplica\u00e7\u00e3o:

Podemos ver no console a mensagem: Relation \"users\" does not exist. Que traduzida pode ser lido como A rela\u00e7\u00e3o \"users\" n\u00e3o existe. O significa que a tabela \"users\" n\u00e3o foi criada ou n\u00e3o existe no banco de dados.

Desta forma, para que nossa aplica\u00e7\u00e3o funcione corretamente precisamos executar as migra\u00e7\u00f5es.

"},{"location":"12/#migrations","title":"Migrations","text":"

Agora que nosso container j\u00e1 est\u00e1 em execu\u00e7\u00e3o no fly, podemos executar o comando de migra\u00e7\u00e3o dos dados, pois ele est\u00e1 na mesma rede do postgres configurado pelo Fly2. Essa conex\u00e3o \u00e9 feita via SSH e pode ser efetuada com o comando ssh do flyctl.

Podemos fazer isso de duas formas, acessando efetivamente o container remotamente ou enviando somente um comando para o Fly. Optarei pela segunda op\u00e7\u00e3o, pois ela n\u00e3o \u00e9 interativa e usar\u00e1 somente uma \u00fanica chamada do shell. Desta forma:

$ Execu\u00e7\u00e3o no terminal!
flyctl ssh console -a fastzeroapp -C \"poetry run alembic upgrade head\"\n\nConnecting to fdaa:2:77b0:a7b:1f60:3f74:a755:2... complete\nSkipping virtualenv creation, as specified in config file.\nINFO  [alembic.runtime.migration] Context impl PostgresqlImpl.\nINFO  [alembic.runtime.migration] Will assume transactional DDL.\nINFO  [alembic.runtime.migration] Running upgrade  -> e018397cecf4, create users table\nINFO  [alembic.runtime.migration] Running upgrade e018397cecf4 -> de865434f506, create todos table\n
Poss\u00edvel erro que pode ocorrer!

Uma das formas de funcionamento padr\u00e3o do Fly \u00e9 desativar a m\u00e1quina caso ningu\u00e9m esteja usando. A\u00ed quando uma requisi\u00e7\u00e3o for feita para aplica\u00e7\u00e3o, ele inicia a m\u00e1quina novamente.

Caso voc\u00ea tente fazer um ssh e a aplica\u00e7\u00e3o n\u00e3o estiver de p\u00e9 no momento, voc\u00ea vai receber um erro como esse:

flyctl ssh console -a fastzeroapp -C \"poetry run alembic upgrade head\"\n\nError: app fastzeroapp has no started VMs.\nIt may be unhealthy or not have been deployed yet.\nTry the following command to verify:\n\nfly status\n

Nesse caso, voc\u00ea pode tentar acessar sua aplica\u00e7\u00e3o pelo browser ou via terminal e ela iniciar\u00e1 novamente. Nesse momento, quando a m\u00e1quina estiver rodando, voc\u00ea pode rodar a migra\u00e7\u00e3o novamente.

O comando ssh do flyctl \u00e9 um grupo de subcomandos para executar opera\u00e7\u00f5es espec\u00edficas em um container. Podemos pedir os logs de certificado com ssh log, inserir ou recuperar arquivos via FTP com o ssh ftp.

O subcomando que utilizamos ssh console nos fornece acesso ao shell do container. Por isso tivemos que especificar com a flag -a o nome da nossa aplica\u00e7\u00e3o (poder\u00edamos acessar o console do banco de dados, tamb\u00e9m). E a flag -C \u00e9 o comando que queremos que seja executado no console do container. Nesse caso, o comando completo representa: \"Acesse o console do app fastzeroapp via SSH e execute o comando poetry run alembic upgrade head\".

Dessa forma temos a migra\u00e7\u00e3o executada com sucesso. Voc\u00ea pode usar o comando ssh console sem especificar o comando tamb\u00e9m, dessa forma ele far\u00e1 um login via ssh no container.

Com isso, podemos voltar ao swagger e tentar executar a opera\u00e7\u00e3o de cria\u00e7\u00e3o de um novo user com um POST no endpoit /users. Tudo deve ocorrer perfeitamente dessa vez:

Agora, SIM, nossa aplica\u00e7\u00e3o est\u00e1 em produ\u00e7\u00e3o para qualquer pessoa poder usar e aproveitar da sua aplica\u00e7\u00e3o. Mande o link para geral e veja o que as pessoas acham da sua mais nova aplica\u00e7\u00e3o. \ud83d\ude80

"},{"location":"12/#commit","title":"Commit","text":"

Agora que fizemos todas as altera\u00e7\u00f5es necess\u00e1rias, devemos adicionar ao nosso reposit\u00f3rio os arquivos criados pelo flyctl launch. Os arquivos .dockerignore e fly.toml:

$ Execu\u00e7\u00e3o no terminal!
git add .\ngit commit -m \"Adicionando arquivos gerados pelo Fly\"\ngit push\n

E pronto!

"},{"location":"12/#conclusao","title":"Conclus\u00e3o","text":"

Assim, como prometido, chegamos ao final da jornada! Temos uma aplica\u00e7\u00e3o pequena, mas funcional em produ\u00e7\u00e3o! \ud83d\ude80

Ao longo desta aula, percorremos uma jornada sobre como implantar uma aplica\u00e7\u00e3o FastAPI com Docker no Fly.io, uma plataforma que oferece uma maneira simples e acess\u00edvel de colocar suas aplica\u00e7\u00f5es na nuvem. Exploramos alguns comandos do flyctl e fomos desde a configura\u00e7\u00e3o inicial at\u00e9 o processo de deploy e resolu\u00e7\u00e3o de problemas. Agora, com nossa aplica\u00e7\u00e3o pronta para o mundo, voc\u00ea possui o conhecimento necess\u00e1rio para compartilhar suas cria\u00e7\u00f5es com outras pessoas e continuar sua jornada no desenvolvimento web.

Na pr\u00f3xima aula discutiremos um pouco sobre o que mais voc\u00ea pode aprender para continuar desenvolvendo seus conhecimentos em FastAPI e desenvolvimento web, al\u00e9m de claro, de algumas dicas de materiais. At\u00e9 l\u00e1!

Agora que a aula acabou, \u00e9 um bom momento para voc\u00ea relembrar alguns conceitos e fixar melhor o conte\u00fado respondendo ao question\u00e1rio referente a ela.

Quiz

  1. No plano gratuito existe uma limita\u00e7\u00e3o de m\u00e1quinas dispon\u00edveis por aplica\u00e7\u00e3o. Quando usamos mais de uma m\u00e1quina, temos que ter um plano pago, por esse motivo, faremos o build localmente.\u00a0\u21a9

  2. \u00c9 poss\u00edvel executar a migra\u00e7\u00e3o usando a sua m\u00e1quina como ponto de partida. Para isso \u00e9 necess\u00e1rio usar o proxy do Fly: fly proxy 5432 -a fastzerodb. Dessa forma, a porta 5432 \u00e9 disponibilizada localmente para executar o comando. Acredito, por\u00e9m, que a conex\u00e3o via ssh \u00e9 mais proveitosa, no momento em que podemos explorar mais uma forma de interagir com o Fly.\u00a0\u21a9

"},{"location":"13/","title":"Despedida e pr\u00f3ximos passos","text":""},{"location":"13/#despedida-e-proximos-passos","title":"Despedida e pr\u00f3ximos passos","text":"

Objetivos da aula:

Caso prefira ver a aula em v\u00eddeo

Esse aula ainda n\u00e3o est\u00e1 dispon\u00edvel em formato de v\u00eddeo, somente em texto ou live!

Aula Slides

Estamos chegando ao final de nossa jornada juntos neste curso. Durante esse tempo, tivemos a oportunidade de explorar uma s\u00e9rie de conceitos e tecnologias essenciais para o desenvolvimento de aplica\u00e7\u00f5es web modernas e escal\u00e1veis. \u00c9 importante lembrar que o que vimos aqui \u00e9 apenas a ponta do iceberg. Ainda h\u00e1 muitos aspectos e detalhes que n\u00e3o pudemos cobrir neste curso, como tratamento de logs, observabilidade, seguran\u00e7a avan\u00e7ada, otimiza\u00e7\u00f5es de desempenho, entre outros. Encorajo a todos que continuem explorando e aprendendo.

"},{"location":"13/#revisao","title":"Revis\u00e3o","text":"

Ao longo deste curso, cobrimos uma s\u00e9rie de t\u00f3picos essenciais para o desenvolvimento de aplica\u00e7\u00f5es web modernas e robustas:

"},{"location":"13/#outros-materiais-produzidos-por-mim-sobre-fastapi","title":"Outros materiais produzidos por mim sobre FastAPI","text":"

J\u00e1 cobrimos alguns temas n\u00e3o citados neste curso usando FastAPI em algumas Lives de Python. Voc\u00ea pode assistir para aprender mais tamb\u00e9m.

"},{"location":"13/#templates-e-websockets","title":"Templates e WebSockets","text":"

Na Live de Python #164 conversamos sobre websockets com Python e usamos FastAPI para exemplificar o comportamento. Durante essa live criamos uma aplica\u00e7\u00e3o de chat e usamos os templates com HTML e Jinja2 e Brython para a\u00e7\u00f5es din\u00e2micas como far\u00edamos com JavaScript.

"},{"location":"13/#graphql-strawberry","title":"GraphQL (Strawberry)","text":"

Na Live de Python #185 conversamos sobre GraphQL um padr\u00e3o alternativo a REST APIs. Todos os exemplos foram aplicados usando Strawberry e FastAPI

"},{"location":"13/#sqlmodel","title":"SQLModel","text":"

Na Live de Python #235 conversamos sobre SQLModel um ORM alternativo ao SQLAlchemy que se integra com o Pydantic. O SQLModel tamb\u00e9m foi desenvolvido pelo Sebastian (criador do FastAPI). Caminhando ao final dessa aula, podemos ver a implementa\u00e7\u00e3o do SQLModel em uma aplica\u00e7\u00e3o b\u00e1sica com FastAPI.

"},{"location":"13/#fastui","title":"FastUI","text":"

Na Live de Python #259 conversamos sobre FastUI. Uma forma de usar modelos do Pydantic para retornar componentes React e contruir o front-end da aplica\u00e7\u00e3o coordenado pelo back-end. Um esquema de intera\u00e7\u00e3o de Back/Front conhecido como SDUI (Server-Driver User Interface).

"},{"location":"13/#proximos-passos","title":"Pr\u00f3ximos passos","text":"

Parte importante do aprendizado vem de entender que o que vimos aqui \u00e9 o b\u00e1sico, o m\u00ednimo que devemos saber para conseguir fazer uma aplica\u00e7\u00e3o consistente usando FastAPI. Agora \u00e9 a hora de trilhar novos caminhos e conhecer mais as possibilidades. Tanto na constru\u00e7\u00e3o de APIs, quanto no aprofundamento de recursos do FastAPI.

"},{"location":"13/#observabilidade","title":"Observabilidade","text":"

Embora tenhamos conseguido colocar nossa aplica\u00e7\u00e3o no ar sem grandes problemas. Quando a aplica\u00e7\u00e3o passa da nossa m\u00e1quina, em nosso contexto, para ser utilizada em escala no deploy. Perdemos a visualiza\u00e7\u00e3o do que est\u00e1 acontecendo de fato com a aplica\u00e7\u00e3o. Os erros que est\u00e3o acontecendo, quais partes do sistema est\u00e3o sendo mais utilizadas, o tempo que nossa aplica\u00e7\u00e3o est\u00e1 levando para executar algumas tarefas, etc.

Temos diversas pr\u00e1ticas e ferramentas que nos ajudam a entender como a aplica\u00e7\u00e3o est\u00e1 rodando em produ\u00e7\u00e3o. Como:

Caso queria aprender mais sobre observabilidade com FastAPI

Fizemos uma s\u00e9rie sobre opentelemetry com os exemplos usando FastAPI e diversas integra\u00e7\u00f5es entre servi\u00e7os. Pode ser que voc\u00ea goste e aprenda mais sobre o framework.

Uma introdu\u00e7\u00e3o a observabilidade usando FastAPI:

M\u00e9tricas de observabilidade usando FastAPI como exemplo:

Traces distribu\u00eddos com exemplos com FastAPI:

Logs de observabilidade com exemplos com FastAPI:

Uma pratica geral sobre observabilidade com FastAPI:

Uma forma de unir todos os conceitos de observabilidade \u00e9 utilizando um APM ou construindo sua pr\u00f3pria \"central de observabilidade\" com ferramentas como o Opentelemetry. Ele permite que instalemos diversas formas de instrumenta\u00e7\u00e3o em nossa aplica\u00e7\u00e3o e distribui os dados gerados para diversos backends. Como o Jaeger e o Grafana Tempo para armazenar traces. O Prometheus para ser um backend de m\u00e9tricas. O Grafana Loki para o armazenamento de logs. E por fim, criar um dashboard juntando todas essas informa\u00e7\u00f5es para exibir a sa\u00fade tanto da aplica\u00e7\u00e3o quanto das regras estabelecidas pelo neg\u00f3cio com o Grafana.

"},{"location":"13/#assincronismo","title":"Assincronismo","text":"

Um exemplo b\u00e1sico

Um exemplo b\u00e1sico de uso de Asyncio + FastAPI pode ser encontrado no ap\u00eandice B.

Outro ponto importante, e talvez o carro chefe do FastAPI \u00e9 poder ser usado de forma concorrente. O que significa que ele pode fazer outro trabalho enquanto aguarda por chamadas de input/ouput. Por exemplo, enquanto esperamos o postgres responder, podemos outra requisi\u00e7\u00e3o. Nesse momento, enquanto essa requisi\u00e7\u00e3o faz outra chamada ao banco, podemos responder a que est\u00e1vamos aguardando a resposta no banco de dados. Isso faz com que o tempo da aplica\u00e7\u00e3o seja otimizado durante a execu\u00e7\u00e3o.

Chamadas ass\u00edncronas em python s\u00e3o caracterizadas pelo uso das corrotinas async def e as esperas com await. A pr\u00f3pria documenta\u00e7\u00e3o do fastAPI apresenta um tutorial sobre AsyncIO.

Conversamos sobre AsyncIO diversas vezes na Live de Python. Se pudesse destacar um material que gostei de ter feito sobre esse assunto, seria a live sobre requisi\u00e7\u00f5es ass\u00edncronas:

Se tiver curiosidade de ver um exemplo real de AsyncIO e FastAPI nos mesmos moldes que aprendemos durante esse curso.

Temos o projeto do chat que fica dispon\u00edvel durante as lives. Tanto na Twitch, quanto no YouTube. O livestream-chat.

Aqui temos v\u00e1rios conceitos aplicados ao projeto. Templates com HTML e Jinja, WebSockets com diferentes canais, requisi\u00e7\u00f5es em backgroud, uso de asyncio, eventos do FastAPI, logs com loguru, integra\u00e7\u00e3o com um APM (Sentry), testes ass\u00edncronos, etc.

"},{"location":"13/#anotacao-de-tipos","title":"Anota\u00e7\u00e3o de tipos","text":"

Um dos pontos principais do uso do Pydantic e do FastAPI, que n\u00e3o nos aprofundamos nesse material.

Durante esse material vimos tipos embutidos diferentes como typing.Annotated, tipos customizados pelo Pydantic como email: EmailStr ou at\u00e9 mesmo tipos criados pelo SQLAlchemy como: Mapped[str]. Entender como o sistema de tipos usa essas anota\u00e7\u00f5es em tempo de execu\u00e7\u00e3o pode ser bastante proveitoso para escrever um c\u00f3digo que ser\u00e1 mais seguro em suas rela\u00e7\u00f5es.

O sistema de tipos do python est\u00e1 descrito aqui. Voc\u00ea pode estudar mais por esse material.

Nota do @dunossauro

Meu pr\u00f3ximo material em texto ser\u00e1 um livro online e gratuito sobre tipagem gradual com python. Quando estiver dispon\u00edvel, eu atualizarei essa p\u00e1gina com o link!

"},{"location":"13/#tarefas-em-background","title":"Tarefas em background","text":"

Um exemplo b\u00e1sico

Um exemplo b\u00e1sico de uso de tarefas em segundo plano pode ser encontrado no ap\u00eandice B.

Uma das coisas legais de poder usar AsyncIO \u00e9 poder realizar tarefas em segundo plano. Isso pode ser uma confirma\u00e7\u00e3o de cria\u00e7\u00e3o de conta, como um e-mail. Ou at\u00e9 mesmo a gera\u00e7\u00e3o de um relat\u00f3rio semanal.

Existem v\u00e1rias formas incr\u00edveis de uso, n\u00e3o irei me estender muito nesse t\u00f3pico, pois a documenta\u00e7\u00e3o do fastAPI tem uma \u00f3tima p\u00e1gina em portugu\u00eas sobre Tarefas em segundo plano. Acredito que valha a pena a leitura!

"},{"location":"13/#conclusao","title":"Conclus\u00e3o","text":"

Todos esses conceitos e pr\u00e1ticas s\u00e3o componentes fundamentais no desenvolvimento de aplica\u00e7\u00f5es web modernas e escal\u00e1veis. Eles nos permitem criar aplica\u00e7\u00f5es robustas, confi\u00e1veis e eficientes, que podem ser facilmente mantidas e escaladas.

Gostaria de agradecer a todos que acompanharam essa s\u00e9rie de aulas. Espero que tenham encontrado valor nas informa\u00e7\u00f5es e pr\u00e1ticas que compartilhamos aqui. Lembre-se, a jornada do aprendizado \u00e9 cont\u00ednua e cada passo conta. Continue explorando, aprendendo e crescendo.

At\u00e9 mais!

"},{"location":"14/","title":"Projeto final","text":""},{"location":"14/#projeto-final","title":"Projeto final","text":"

Voc\u00ea chegou ao final, PARABAINS \ud83c\udf89

No aprendizado, nada melhor que praticar! Para isso, vamos fazer nosso \"TCC\" ou como gostam de chamar no mundo \"interprize bizines\": um teste t\u00e9cnico.

A ideia deste projeto final \u00e9 simplesmente extrair tudo que aprendemos no curso para um grande exerc\u00edcio de fixa\u00e7\u00e3o em formato de projeto.

"},{"location":"14/#o-projeto","title":"O projeto","text":"

Neste projeto vamos construir uma API que segue os mesmos moldes da que desenvolvemos durante o curso, por\u00e9m, com outra proposta. Iremos fazer uma vers\u00e3o simplificado de um acervo digital de livros. Chamaremos de MADR (Mader), uma sigla para \"Meu Acervo Digital de Romances\".

O objetivo do projeto \u00e9 criarmos um gerenciador de livros e relacionar com seus autores. Tudo isso em um contexto bastante simplificado. Usando somente as funcionalidades que aprendemos no curso.

A implementa\u00e7\u00e3o ser\u00e1 baseada em 3 pilares:

graph\n    MADR --> A[\"Controle de acesso / Gerenciamento de contas\"]\n    MADR --> B[\"Gerenciamento de Livros\"]\n    MADR --> C[\"Gerenciamento de Romancistas\"]\n    A --> D[\"Gerenciamento de contas\"]\n    D --> Cria\u00e7\u00e3o\n    D --> Atualiza\u00e7\u00e3o\n    A --> G[\"Acesso via JWT\"]\n    D --> Dele\u00e7\u00e3o\n    B --> E[\"CRUD\"]\n    C --> F[\"CRUD\"]
"},{"location":"14/#a-api","title":"A API","text":"

Dividiremos os endpoints em tr\u00eas routers:

  1. contas: Gerenciamento de contas e de acesso \u00e0 API
  2. livros: Gerenciamento de livros
  3. romancistas: Gerenciamento de romancistas
"},{"location":"14/#contas","title":"Contas","text":"

O router de conta deve ser respons\u00e1vel pelas opera\u00e7\u00f5es referentes a cria\u00e7\u00e3o, altera\u00e7\u00e3o e dele\u00e7\u00e3o de contas. Os endpoints:

"},{"location":"14/#as-condicoes-do-token-jwt","title":"As condi\u00e7\u00f5es do token JWT","text":"

O tempo de expira\u00e7\u00e3o do token deve ser de 60 minutos, o algor\u00edtimo usado deve ser HS256 e o subject deve ser o email.

"},{"location":"14/#livros","title":"Livros","text":""},{"location":"14/#romancistas","title":"Romancistas","text":""},{"location":"14/#sanitizacao-de-dados","title":"Sanitiza\u00e7\u00e3o de dados","text":"

Antes de inserir no banco, os nomes de romancistas ou livros devem ser sanitizados.

Exemplos para os nomes:

Entrada Sanitizado \"Machado de Assis\" machado de assis \"Manuel\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Bandeira\" manuel bandeira \"Edgar Alan Poe\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\" edgar alan poe \"Androides Sonham Com Ovelhas El\u00e9tricas?\" androides sonham com ovelhas el\u00e9tricas \"\u00a0\u00a0breve \u00a0hist\u00f3ria \u00a0do tempo\u00a0\" breve hist\u00f3ria do tempo \"O mundo assombrado pelos dem\u00f4nios\" o mundo assombrado pelos dem\u00f4nios"},{"location":"14/#erros","title":"Erros","text":""},{"location":"14/#erros-de-autenticacao","title":"Erros de autentica\u00e7\u00e3o","text":"

Todos os erros relativos \u00e0 autentica\u00e7\u00e3o devem retornar o status code 400 BAD REQUEST com o seguinte schema:

{\n    \"message\": \"Email ou senha incorretos\"\n}\n
"},{"location":"14/#erros-de-permissao","title":"Erros de permiss\u00e3o","text":"

Caso uma pessoa tente fazer uma opera\u00e7\u00e3o sem a permiss\u00e3o necess\u00e1ria, o status code401 Unauthorized dever\u00e1 ser retornado com o json:

{\n    \"message\": \"N\u00e3o autorizado\"\n}\n
"},{"location":"14/#erro-nao-encontrado","title":"Erro n\u00e3o encontrado","text":"

Caso o id n\u00e3o exista no MADR, um erro 404 NOT FOUND deve ser retornado com o json:

{\n    \"message\": \"Romancista n\u00e3o consta no MADR\"\n}\n

ou ent\u00e3o

{\n    \"message\": \"Livro n\u00e3o consta no MADR\"\n}\n
"},{"location":"14/#erro-de-conflito","title":"Erro de conflito","text":"

Caso o recurso j\u00e1 exista, devemos retornar 409 CONFLICT com o json:

{\n    \"message\": \"{recurso} j\u00e1 consta no MADR\"\n}\n

Onde a vari\u00e1vel recurso \u00e9 relativa ao recurso que est\u00e1 duplicado. Exemplos para:

"},{"location":"14/#o-banco-de-dados-orm","title":"O banco de dados / ORM","text":"

A modelagem do banco deve contar com tr\u00eas tabelas: User, Livro e Romancista. Onde Livro e Romancista se relacionam da forma que romancistas podem estar relacionado a diversos livros e diversos livros devem ser associados a uma \u00fanica romancista. Como sugere o DER:

erDiagram\n  Romancista |o -- |{ Livro : livros\n  User {\n      int id PK\n      string email UK\n      string username UK\n      string senha\n  }\n  Livro {\n      int id  PK\n      string ano\n      string titulo UK\n      string id_romancista FK\n  }\n  Romancista {\n      int id PK\n      string nome UK\n      string livros\n  }
"},{"location":"14/#relacionamentos-no-orm","title":"Relacionamentos no ORM","text":"

Alguns problemas podem ser encontrados durante a cria\u00e7\u00e3o dos relacionamentos com SQLAlchemy, ent\u00e3o segue uma cola simples caso sinta que travou.

Em caso de emerg\u00eancia quebre o vidro
class Livro:\n    ...\n\n    autoria: Mapped[Romancista] = relationship(\n        init=False, back_populates='livros'\n    )\n\nclass Romancista:\n    ...\n\n    livros: Mapped[list['Livro']] = relationship(\n        init=False, back_populates='romancista', cascade='all, delete-orphan'\n    )\n
"},{"location":"14/#cenarios-de-teste","title":"Cen\u00e1rios de teste","text":"

O ideal \u00e9 que esse projeto tenha uma cobertura de testes de 100%. Afinal, foi dessa forma que passamos nosso tempo no curso, testando absolutamente tudo e garantindo que o c\u00f3digo funcione da maneira como deveria.

Nesse t\u00f3pico separei alguns cen\u00e1rios de testes usando a linguagem gherkin para te ajudar a pensar em como as requisi\u00e7\u00f5es ser\u00e3o recebidas e devem ser respondidas pela aplica\u00e7\u00e3o.

Esses cen\u00e1rios podem te guiar tanto para escrever a aplica\u00e7\u00e3o, quanto os testes.

"},{"location":"14/#gerenciamento-de-contas","title":"Gerenciamento de contas","text":"Cria\u00e7\u00e3o de contasCasos de erroAutentica\u00e7\u00e3o e autoriza\u00e7\u00e3o
Funcionalidade: Gerenciamento de conta\n\nCen\u00e1rio: Cria\u00e7\u00e3o de conta\n    Quando enviar um \"POST\" em \"/user\"\n    \"\"\"\n    {\n        \"username\": \"dunossauro\",\n        \"email\": \"dudu@dudu.com\",\n        \"password\": \"123456\"\n    }\n    \"\"\"\n    Ent\u00e3o devo receber o status \"201\"\n    E o json contendo\n    \"\"\"\n    {\n        \"email\": \"dudu@dudu.com\",\n        \"username\": \"dunossauro\"\n    }\n    \"\"\"\n\nCen\u00e1rio: Altera\u00e7\u00e3o de conta\n    Quando enviar um \"POST\" em \"/user\"\n    \"\"\"\n    {\n        \"username\": \"dunossauro\",\n        \"email\": \"dudu@dudu.com\",\n        \"password\": \"123456\"\n    }\n    \"\"\"\n    Quando enviar um \"PUT\" em \"/user/1\"\n    \"\"\"\n    {\n        \"username\": \"dunossauro\",\n        \"email\": \"dudu@dudu.com\",\n        \"password\": \"654321\"\n    }\n    \"\"\"\n    Ent\u00e3o devo receber o status \"200\"\n    E o json contendo\n    \"\"\"\n    {\n        \"username\": \"dunossauro\",\n        \"email\": \"dudu@dudu.com\"\n    }\n    \"\"\"\n\nCen\u00e1rio: Dele\u00e7\u00e3o da conta\n    Quando enviar um \"POST\" em \"/user\"\n    \"\"\"\n    {\n        \"username\": \"dunossauro\",\n        \"email\": \"dudu@dudu.com\",\n        \"password\": \"123456\"\n    }\n    \"\"\"\n    Quando enviar um \"DELETE\" em \"/user/1\"\n    Ent\u00e3o devo receber o status \"200\"\n    E o json contendo\n    \"\"\"\n    {\n        \"message\": \"Conta deletada com sucesso\"\n    }\n    \"\"\"\n
Cen\u00e1rio: Cria\u00e7\u00e3o de conta j\u00e1 existente\n    Quando enviar um \"POST\" em \"/user\"\n    \"\"\"\n    {\n        \"username\": \"dunossauro\",\n        \"email\": \"dudu@dudu.com\",\n        \"password\": \"123456\"\n    }\n    \"\"\"\n    Quando enviar um \"POST\" em \"/user\"\n    \"\"\"\n    {\n        \"username\": \"dunossauro\",\n        \"email\": \"dudu@dudu.com\",\n        \"password\": \"123456\"\n    }\n    \"\"\"\n    Ent\u00e3o devo receber o status \"400\"\n    E o json contendo\n    \"\"\"\n    {\n        \"message\": \"Conta j\u00e1 cadastrada\"\n    }\n    \"\"\"\n
TODO\n
"},{"location":"14/#gerenciamento-de-livros","title":"Gerenciamento de livros","text":"
Funcionalidade: Livro\n\nCen\u00e1rio: Registro de livro\n    Quando enviar um \"POST\" em \"/livro/\"\n    \"\"\"\n    {\n        \"ano\": 1973,\n        \"titulo\": \"Caf\u00e9 Da Manh\u00e3 Dos Campe\u00f5es\",\n        \"romancista_id\": 1\n    }\n    \"\"\"\n\n    Ent\u00e3o devo receber o status \"201\"\n    E o json contendo\n    \"\"\"\n    {\n        \"ano\": 1973,\n        \"titulo\": \"caf\u00e9 da manh\u00e3 dos campe\u00f5es\",\n        \"romancista_id\": 1\n    }\n    \"\"\"\n\n\nCen\u00e1rio: Altera\u00e7\u00e3o de livro\n    Quando enviar um \"PATCH\" em \"/livro/1\"\n    \"\"\"\n    {\n        \"ano\": 1974\n    }\n    \"\"\"\n\n    Ent\u00e3o devo receber o status \"200\"\n    E o json contendo\n    \"\"\"\n    {\n        \"ano\": 1974,\n        \"titulo\": \"caf\u00e9 da manh\u00e3 dos campe\u00f5es\",\n        \"romancista_id\": 1\n    }\n    \"\"\"\n\nCen\u00e1rio: Buscar livro por ID\n    Quando enviar um \"GET\" em \"/livro/1\"\n    Ent\u00e3o devo receber o status \"200\"\n    E o json contendo\n    \"\"\"\n    {\n        \"ano\": 1974,\n        \"titulo\": \"caf\u00e9 da manh\u00e3 dos campe\u00f5es\",\n        \"romancista_id\": 1\n    }\n    \"\"\"\n\nCen\u00e1rio: Dele\u00e7\u00e3o de livro\n    Quando enviar um \"DELETE\" em \"/livro/1\"\n\n    Ent\u00e3o devo receber o status \"200\"\n    E o json contendo\n    \"\"\"\n    {\n        \"message\": \"Livro deletado no MADR\"\n    }\n    \"\"\"\n\nCen\u00e1rio: Filtro de livros\n    Quando enviar um \"POST\" em \"/livro/\"\n    \"\"\"\n    {\n        \"ano\": 1900,\n        \"titulo\": \"Caf\u00e9 Da Manh\u00e3 Dos Campe\u00f5es\",\n        \"romancista_id\": 1\n    }\n    \"\"\"\n    E enviar um \"POST\" em \"/livro/\"\n    \"\"\"\n    {\n        \"ano\": 1900,\n        \"titulo\": \"Mem\u00f3rias P\u00f3stumas de Br\u00e1s Cubas\",\n        \"romancista_id\": 2\n    }\n    \"\"\"\n    E enviar um \"POST\" em \"/livro/\"\n    \"\"\"\n    {\n        \"ano\": 1865,\n        \"titulo\": \"Iracema\",\n        \"romancista_id\": 3\n    }\n    \"\"\"\n    E enviar um \"GET\" em \"/livro/?titulo=a&ano=1900\"\n\n    Ent\u00e3o devo receber o status \"200\"\n    E o json contendo\n    \"\"\"\n    {\n        \"livros\": [\n            {\"ano\": 1900, \"titulo\": \"caf\u00e9 da manh\u00e3 dos campe\u00f5es\", \"romancista_id\": 1, \"id\": 1},\n            {\"ano\": 1900, \"titulo\": \"mem\u00f3rias p\u00f3stumas de br\u00e1s cubas\", \"romancista_id\": 2, \"id\": 2}\n        ]\n    }\n    \"\"\"\n
"},{"location":"14/#gerenciamento-de-romancistas","title":"Gerenciamento de romancistas","text":"
Funcionalidade: Romancistas\n\n\nCen\u00e1rio: Cria\u00e7\u00e3o de Romancista\n    Quando enviar um \"POST\" em \"/romancista\"\n    \"\"\"\n    {\n        \"nome\": \"Clarice Lispector\"\n    }\n    \"\"\"\n\n    Ent\u00e3o devo receber o status \"201\"\n    E o json contendo\n    \"\"\"\n    {\n        \"nome\": \"clarice lispector\"\n    }\n    \"\"\"\n\nCen\u00e1rio: Buscar romancista por ID\n    Quando enviar um \"GET\" em \"/romancista/1\"\n    Ent\u00e3o devo receber o status \"200\"\n    E o json contendo\n    \"\"\"\n    {\n        \"nome\": \"clarice lispector\"\n    }\n    \"\"\"\n\nCen\u00e1rio: Altera\u00e7\u00e3o de Romancista\n    Quando enviar um \"PUT\" em \"/romancista/1\"\n    \"\"\"\n    {\n        \"nome\": \"manuel bandeira\"\n    }\n    \"\"\"\n    Ent\u00e3o devo receber o status \"200\"\n    E o json contendo\n    \"\"\"\n    {\n        \"nome\": \"manuel bandeira\"\n    }\n    \"\"\"\n\nCen\u00e1rio: Dele\u00e7\u00e3o de Romancista\n    Quando enviar um \"DELETE\" em \"/romancista/1\"\n    Ent\u00e3o devo receber o status \"200\"\n    E o json contendo\n    \"\"\"\n    {\n        \"message\": \"Romancista deletada no MADR\"\n    }\n    \"\"\"\n\nCen\u00e1rio: Busca de romancistas por filtro\n    Quando enviar um \"POST\" em \"/romancista\"\n    \"\"\"\n    {\n        \"nome\": \"Clarice Lispector\"\n    }\n    \"\"\"\n\n    E enviar um \"POST\" em \"/romancista\"\n    \"\"\"\n    {\n        \"nome\": \"Manuel Bandeira\"\n    }\n    \"\"\"\n\n    E enviar um \"POST\" em \"/romancista\"\n    \"\"\"\n    {\n        \"nome\": \"Paulo Leminski\"\n    }\n    \"\"\"\n\n    Quando enviar um \"GET\" em \"/romancista?nome=a\"\n    Ent\u00e3o devo receber o status \"200\"\n    E o json contendo\n    \"\"\"\n    {\n        \"romancistas\": [\n            {\"nome\": \"clarice lispector\", \"id\": 1},\n            {\"nome\": \"manuel bandeira\", \"id\": 2},\n            {\"nome\": \"paulo leminski\", \"id\": 3}\n        ]\n    }\n    \"\"\"\n
"},{"location":"14/#ferramentas","title":"Ferramentas","text":"

Gostaria que voc\u00ea se sentissem livres para escolher o conjunto de ferramentas que mais gostarem para fazer esse projeto. O formatador preferido, o servidor de aplica\u00e7\u00e3o preferido, projeto de vari\u00e1veis de ambiente preferido, etc.

As \u00fanicas coisas exigidas para a cria\u00e7\u00e3o desse projeto s\u00e3o:

  1. Python 3.11+
  2. FastAPI
  3. SQLAlchemy
  4. Alguma ferramenta para gerenciamento de projeto que suporte pyproject.toml
  5. PostgreSQL
  6. Containers (a ferramenta que preferir. Podman/docker/k8s/...)
  7. Pytest
"},{"location":"14/#entrega-do-projeto-final","title":"Entrega do projeto final","text":"

Criar um projeto utilizando git e hospedado em alguma plataforma (github/gitlab/codeberg/...) e postar nessa issue. Ao final, juntarei todos os projetos finais em uma tabela nesse site para que as pessoas possam aprender com as diferen\u00e7as entre os projetos.

\u00c9 imprescind\u00edvel que seu projeto tenha um README.md explicando quais foram as suas escolhas e como executar o seu projeto. Para podermos rodar e aprender com ele.

"},{"location":"apendices/a_instalacoes/","title":"A - Instalando as ferramentas externas","text":""},{"location":"apendices/a_instalacoes/#a-instalando-as-ferramentas-externas","title":"A - Instalando as ferramentas externas","text":"

Durante as aulas s\u00edncronas, diversas d\u00favidas sobre a configura\u00e7\u00e3o e instala\u00e7\u00e3o das ferramentas fora do python foram levantadas. A ideia dessa p\u00e1gina \u00e9 te auxiliar nas instala\u00e7\u00f5es.

S\u00e3o comandos r\u00e1pidos e simples, n\u00e3o tenho a intens\u00e3o de explicar o que essas ferramentas fazem exatamente, muitas explica\u00e7\u00f5es j\u00e1 foram escritas sobre elas na p\u00e1gina de configura\u00e7\u00e3o do projeto. A ideia \u00e9 agrupar todas as instala\u00e7\u00f5es um \u00fanico lugar.

"},{"location":"apendices/a_instalacoes/#pyenv-no-windows","title":"Pyenv no Windows","text":"

Para instalar o pyenv voc\u00ea precisa abrir seu terminal como administrado e executar o comando:

Invoke-WebRequest -UseBasicParsing -Uri \"https://raw.githubusercontent.com/pyenv-win/pyenv-win/master/pyenv-win/install-pyenv-win.ps1\" -OutFile \"./install-pyenv-win.ps1\"; &\"./install-pyenv-win.ps1\"\n

A mensagem pyenv-win is successfully installed. You may need to close and reopen your terminal before using it. aparecer\u00e1 na tela. Dizendo que precisamos reinicar o shell.

S\u00f3 precisamos fech\u00e1-lo e abrir de novo.

"},{"location":"apendices/a_instalacoes/#pyenv-no-linuxmacos","title":"Pyenv no Linux/MacOS","text":"

Como n\u00e3o tenho como cobrir a instala\u00e7\u00e3o em todos as distros, vou usar uma ferramenta chamada pyenv-installer. \u00c9 bastante simples, somente executar o comando:

$ Execu\u00e7\u00e3o no terminal!
curl https://pyenv.run | bash\n

Ap\u00f3s isso \u00e9 importante que voc\u00ea siga a instru\u00e7\u00e3o de adicionar a configura\u00e7\u00e3o no seu .bashrc:

~/.bashrc
export PATH=\"$HOME/.pyenv/bin:$PATH\"\neval \"$(pyenv init --path)\"\neval \"$(pyenv virtualenv-init -)\"\n

Caso use zsh, xonsh, .... bom... Voc\u00ea deve saber o que est\u00e1 fazendo :)

Ap\u00f3s isso reinicie o shell para que a vari\u00e1vel de ambiente seja carregada.

Caso esteja no ubuntu

\u00c9 importante que voc\u00ea instale o curl e o git antes:

$ Execu\u00e7\u00e3o no terminal!
sudo apt update\nsudo apt install curl git\n
"},{"location":"apendices/a_instalacoes/#instalacao-do-python-via-pyenv","title":"Instala\u00e7\u00e3o do Python via pyenv","text":"

Agora, com o pyenv instalado, voc\u00ea pode instalar a vers\u00e3o do python que usaremos no curso. Como descrito na p\u00e1gina de configura\u00e7\u00e3o do projeto:

$ Execu\u00e7\u00e3o no terminal!
pyenv install 3.12.3\n

A seguinte mensagem deve aparecer na tela:

:: [Info] ::  Mirror: https://www.python.org/ftp/python\n:: [Downloading] ::  3.12.3 ...\n:: [Downloading] ::  From https://www.python.org/ftp/python/3.12.3/python-3.12.3-amd64.exe\n:: [Downloading] ::  To   C:\\Users\\vagrant\\.pyenv\\pyenv-win\\install_cache\\python-3.12.3-amd64.exe\n:: [Installing] ::  3.12.3 ...\n:: [Info] :: completed! 3.12.3\n
"},{"location":"apendices/a_instalacoes/#configurando-a-versao-no-pyenv","title":"Configurando a vers\u00e3o no pyenv","text":"

Agora com vers\u00e3o instalada, devemos dizer ao shim, qual vers\u00e3o ser\u00e1 usada globalmente. Podemos executar esse comando:

$ Execu\u00e7\u00e3o no terminal!
pyenv global 3.12.3\n

Esse comando n\u00e3o costuma exibir nenhuma mensagem em caso de sucesso, se nada foi retornado, significa que tudo ocorreu como esperado.

Para testar se a vers\u00e3o foi definida, podemos chamar o python no terminal:

$ Execu\u00e7\u00e3o no terminal!
python --version\nPython 3.12.3 #(1)!\n
  1. Responde que a vers\u00e3o correta foi setada!
"},{"location":"apendices/a_instalacoes/#pipx","title":"pipx","text":"

O pipx \u00e9 uma ferramenta opcional na configura\u00e7\u00e3o do ambiente, mas \u00e9 extremamente recomendado que voc\u00ea a instale para simplificar a instala\u00e7\u00e3o de pacotes globais.

Para isso, voc\u00ea pode executar:

$ Execu\u00e7\u00e3o no terminal!
pip install pipx\n

A resposta do comando dever\u00e1 ser parecida com essa:

Collecting pipx\n  Downloading pipx-1.6.0-py3-none-any.whl.metadata (18 kB)\nCollecting argcomplete>=1.9.4 (from pipx)\n  Downloading argcomplete-3.3.0-py3-none-any.whl.metadata (16 kB)\nCollecting colorama>=0.4.4 (from pipx)\n  Downloading colorama-0.4.6-py2.py3-none-any.whl.metadata (17 kB)\nCollecting packaging>=20 (from pipx)\n  Downloading packaging-24.1-py3-none-any.whl.metadata (3.2 kB)\nCollecting platformdirs>=2.1 (from pipx)\n  Downloading platformdirs-4.2.2-py3-none-any.whl.metadata (11 kB)\nCollecting userpath!=1.9,>=1.6 (from pipx)\n  Downloading userpath-1.9.2-py3-none-any.whl.metadata (3.0 kB)\nCollecting click (from userpath!=1.9,>=1.6->pipx)\n  Downloading click-8.1.7-py3-none-any.whl.metadata (3.0 kB)\nDownloading pipx-1.6.0-py3-none-any.whl (77 kB)\n   \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501 77.8/77.8 kB 2.2 MB/s eta 0:00:00\nDownloading argcomplete-3.3.0-py3-none-any.whl (42 kB)\n   \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501 42.6/42.6 kB ? eta 0:00:00\nDownloading colorama-0.4.6-py2.py3-none-any.whl (25 kB)\nDownloading packaging-24.1-py3-none-any.whl (53 kB)\n   \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501 54.0/54.0 kB ? eta 0:00:00\nDownloading platformdirs-4.2.2-py3-none-any.whl (18 kB)\nDownloading userpath-1.9.2-py3-none-any.whl (9.1 kB)\nDownloading click-8.1.7-py3-none-any.whl (97 kB)\n   \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501 97.9/97.9 kB 295.3 kB/s eta 0:00:00\nInstalling collected packages: platformdirs, packaging, colorama, argcomplete, click, userpath, pipx\nSuccessfully installed argcomplete-3.3.0 click-8.1.7 colorama-0.4.6 packaging-24.1 pipx-1.6.0 platformdirs-4.2.2 userpath-1.9.2\n

Para testar se o pipx foi instalado com sucesso, podemos executar:

$ Execu\u00e7\u00e3o no terminal!
pipx --version\n

Se a vers\u00e3o for respondida, tudo est\u00e1 certo :)

Uma coisa recomendada de fazer, \u00e9 adicionar os paths do pipx nas vari\u00e1veis de ambiente, para isso podemos executar:

$ Execu\u00e7\u00e3o no terminal!
pipx ensurepath\n

Dessa forma, os pacotes estar\u00e3o no path. Podendo ser chamados pelo terminal sem problemas. A \u00faltima coisa que precisa ser feita \u00e9 abrir o terminal novamente, para que as novas vari\u00e1veis de ambiente sejam lidas.

"},{"location":"apendices/a_instalacoes/#ignr","title":"ignr","text":"

Com o pipx voc\u00ea pode executar:

$ Execu\u00e7\u00e3o no terminal!
pipx install ignr\n
"},{"location":"apendices/a_instalacoes/#poetry","title":"poetry","text":"

Com o pipx voc\u00ea pode executar:

$ Execu\u00e7\u00e3o no terminal!
pipx install poetry\n
"},{"location":"apendices/a_instalacoes/#gh","title":"GH","text":"

Gh \u00e9 um CLI para o github. Facilita em diversos momentos.

A instala\u00e7\u00e3o para diversos sistemas e variantes pode ser encontrada aqui.

"},{"location":"apendices/a_instalacoes/#docker","title":"Docker","text":"

A instala\u00e7\u00e3o do docker \u00e9 bastante diferente para sistemas operacionais diferentes e at\u00e9 mesmo em arquiteturas de processadores diferentes. Por exemplo, MacOS com intel ou arm, ou windows com WSL, ou hyper-V.

Por esse motivo, acredito que seja interessante voc\u00ea seguir os tutoriais oficiais:

"},{"location":"apendices/a_instalacoes/#docker-compose","title":"Docker Compose","text":"

A instala\u00e7\u00e3o varia bastante de sistema para sistema, mas voc\u00ea pode olhar o guia de instala\u00e7\u00e3o oficial.

"},{"location":"apendices/a_instalacoes/#git","title":"Git","text":"

O git pode ser baixado no site oficial para windows e mac. No Linux acredito que todas as distribui\u00e7\u00f5es t\u00eam o git como um pacote dispon\u00edvel para instala\u00e7\u00e3o.

"},{"location":"apendices/b_proximos_passos/","title":"B - Pr\u00f3ximos passos","text":""},{"location":"apendices/b_proximos_passos/#b-proximos-passos","title":"B - Pr\u00f3ximos passos","text":"

Esse ap\u00eandice se destina a mostrar alguns exemplos de c\u00f3digo da p\u00e1gina de despedida/pr\u00f3ximos passos. Alguns exemplos simples de como fazer algumas tarefas que n\u00e3o trabalhamos durante o curso.

"},{"location":"apendices/b_proximos_passos/#templates","title":"Templates","text":"

O FastAPI conta com um recurso de carregamento de arquivos est\u00e1ticos, como CSS e JS. E tamb\u00e9m permite a renderiza\u00e7\u00e3o de templates com jinja.

Os templates s\u00e3o formas de passar informa\u00e7\u00f5es para o HTML diretamente dos endpoints. Mas, comecemos pela estrutura. Criaremos dois diret\u00f3rios. Um para os templates e um para os arquivos est\u00e1ticos:Os templates s\u00e3o formas de passar informa\u00e7\u00f5es para o HTML diretamente dos endpoints. Mas, comecemos pela estrutura. Criaremos dois diret\u00f3rios. Um para os templates e um para os arquivos est\u00e1ticos:

Estrutura dos arquivos
.\n\u251c\u2500\u2500 app.py\n\u251c\u2500\u2500 static #(1)!\n\u2502  \u2514\u2500\u2500 style.css\n\u2514\u2500\u2500 templates #(2)!\n   \u2514\u2500\u2500 index.html\n
  1. Diret\u00f3rio para arquivos est\u00e1ticos.
  2. Diret\u00f3rio para os templates do jinja

Vamos adicionar um arquivo de estilo bastante simples, somente para ver o efeito da configura\u00e7\u00e3o:

static/style.css
h1 {\n    text-align: center;\n}\n

E um arquivo html usando a tag dos templates:

templates/index.html
<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\"/>\n    <title>index.html</title>\n    <link href=\"static/style.css\" rel=\"stylesheet\"/>\n  </head>\n  <body>\n    <h1>Ol\u00e1 {{ nome }}</h1> <!-- (1)! -->\n  </body>\n</html>\n
  1. Nomes dentro de {{ }} s\u00e3o vari\u00e1veis que ser\u00e3o preenchidas pelo contexto

Todas as vari\u00e1veis inclu\u00eddas em {{ vari\u00e1vel }} s\u00e3o passadas pelo endpoint no momento de retornar o template jinja. Com isso, podemos incluir valores da aplica\u00e7\u00e3o no HTML.

Para unir os arquivos est\u00e1ticos e os templates na aplica\u00e7\u00e3o podemos aplicar o seguinte bloco de c\u00f3digo:

app.py
from fastapi import FastAPI, Request\nfrom fastapi.responses import HTMLResponse\nfrom fastapi.staticfiles import StaticFiles\nfrom fastapi.templating import Jinja2Templates\n\napp = FastAPI()\n\n# Diret\u00f3rio contendo arquivos est\u00e1ticos\napp.mount('/static', StaticFiles(directory='static'), name='static')#(1)!\n\n# Diret\u00f3rio contendo os templates Jinja\ntemplates = Jinja2Templates(directory='templates')#(2)!\n\n\n@app.get('/{nome}', response_class=HTMLResponse)\ndef home(request: Request, nome: str):#(3)!\n    return templates.TemplateResponse(#(4)!\n        request=request, name='index.html', context={'nome': nome}\n    )\n
  1. O m\u00e9todo .mount cria um endpoint /static para retornar os arquivos no diret\u00f3rio static.
  2. Jinja2Templates mapeia um diret\u00f3rio em nossa aplica\u00e7\u00e3o onde armazenamos templates jinja para serem lidos pela aplica\u00e7\u00e3o.
  3. O objeto Request do FastAPI \u00e9 o objeto que representa corpo da requisi\u00e7\u00e3o e seu escopo.
  4. O m\u00e9todo TemplateResponse se encarrega de dizer qual o nome (name) do template que ser\u00e1 renderizado no html e context \u00e9 um dicion\u00e1rio que passa as vari\u00e1veis do endpoint para o arquivo html.

Para que os templates sejam renderizados pelo FastAPI precisamos instalar o jinja:

$ Execu\u00e7\u00e3o no terminal!
poetry add jinja2\n

E executar nosso projeto com:

$ Execu\u00e7\u00e3o no terminal!
task run\n

Desta forma, ao acessar o endpoint pela API, temos a jun\u00e7\u00e3o de templates e est\u00e1ticos acontecendo:

"},{"location":"apendices/b_proximos_passos/#asyncio","title":"Asyncio","text":"

O FastAPI tem suporte nativo para programa\u00e7\u00e3o ass\u00edncrona. A \u00fanica coisa que precisa ser feita para isso, a n\u00edvel do framework (n\u00e3o incluindo as depend\u00eancias) \u00e9 adicionar a palavra reservada async no in\u00edcio dos endpoints.

Da seguinte forma:

app.py
from asyncio import sleep\n\nfrom fastapi import FastAPI\n\napp = FastAPI()\n\n\n@app.get('/')\nasync def home():#(1)!\n    await sleep(1)#(2)!\n    return {'message': 'Ol\u00e1 mundo!'}\n
  1. Cria\u00e7\u00e3o de uma fun\u00e7\u00e3o ass\u00edncrona
  2. Contexto de escalonamento usando await

O escalonamento do loop durante as chamadas nos endpoints pode ser feito por meio da palavra reservada await.

Para que os testes sejam executados de forma ass\u00edncrona, precisamos instalar uma extens\u00e3o do pytest:

$ Execu\u00e7\u00e3o no terminal!
poetry add pytest-asyncio\n

Dessa forma podemos executar fun\u00e7\u00f5es de teste que tamb\u00e9m s\u00e3o ass\u00edncronas usando um marcador do pytest:

test_app.py
from fastapi.testclient import TestClient\n\nfrom app import app\n\nimport pytest\n\n\n@pytest.mark.asyncio #(1)!\nasync def test_async():\n    client = TestClient(app)\n    response = client.get('/')\n\n    assert response.json() == {'message': 'Ol\u00e1 mundo!'}\n
  1. Marcador para executar a fun\u00e7\u00e3o de teste dentro de um loop de eventos
"},{"location":"apendices/b_proximos_passos/#tarefas-em-segundo-plano-background","title":"Tarefas em segundo plano (Background)","text":"

TODO: adicionar explica\u00e7\u00e3o a esse t\u00f3pico

app.py
from time import sleep\n\nfrom fastapi import BackgroundTasks, FastAPI\n\n\napp = FastAPI()\n\n\ndef tarefa_em_segundo_plano(tempo=0):#(1)!\n    sleep(tempo)\n\n\n@app.get('/segundo-plano/{tempo}')\ndef segundo_plano(tempo: int, task: BackgroundTasks):#(2)!\n    task.add_task(tarefa_em_segundo_plano, tempo)#(3)!\n    return {'message': 'Sua requisi\u00e7\u00e3o est\u00e1 sendo processada!'}\n
  1. Uma fun\u00e7\u00e3o qualquer em python que ser\u00e1 executada a qualquer momento
  2. O tipo BackgroundTasks deve ser passado ao endpoint para que ele tenha a possibilidade de adicionar uma tarefa ao loop de eventos
  3. O m\u00e9todo .add_task adiciona a tarefa (fun\u00e7\u00e3o) ao loop de eventos.
"},{"location":"apendices/b_proximos_passos/#eventos-de-ciclo-de-vida","title":"Eventos de ciclo de vida","text":"

Os eventos de ciclo de vida s\u00e3o formas de iniciar ou testar alguma condi\u00e7\u00e3o antes da aplica\u00e7\u00e3o ser de fato inicializada. Voc\u00ea pode criar valida\u00e7\u00f5es, como saber se outra aplica\u00e7\u00e3o est\u00e1 de p\u00e9, configurar coisas antes da aplica\u00e7\u00e3o ser iniciada, como iniciar o banco de dados, etc.

Da mesma forma alguns casos para antes da aplica\u00e7\u00e3o ser finalizada tamb\u00e9m podem ser criadas. Como garantir que todas as tarefas em segundo plano estejam de fato finalizadas antes da aplica\u00e7\u00e3o parar de rodar.

app.py
from logging import getLogger\nfrom time import sleep\n\nfrom fastapi import FastAPI\n\n\nlogger = getLogger('uvicorn')\n\n@asynccontextmanager\nasync def lifespan(app: FastAPI):\n    logger.info('Iniciando a aplica\u00e7\u00e3o')#(1)!\n    yield  # Executa a aplica\u00e7\u00e3o\n    logger.info('Finalizando a aplica\u00e7\u00e3o')#(2)!\n\n\napp = FastAPI(lifespan=lifespan)#(3)!\n
  1. Log que ser\u00e1 emitido antes da aplica\u00e7\u00e3o ser iniciada
  2. Log que ser\u00e1 emitido antes da aplica\u00e7\u00e3o ser finalizada
  3. O par\u00e2metro lifespan recebe uma fun\u00e7\u00e3o ass\u00edncrona com yield para uma condi\u00e7\u00e3o de parada. Assim como uma fixture do pytest.

Podemos observar que os logs foram adicionados ao uvicorn antes e depois da execu\u00e7\u00e3o da aplica\u00e7\u00e3o:

$ Execu\u00e7\u00e3o no terminal!
uvicorn app:app\nINFO:     Started server process [254037]\nINFO:     Waiting for application startup.\nINFO:     Iniciando a aplica\u00e7\u00e3o\nINFO:     Application startup complete.\nINFO:     Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)\n# Apertando Ctrl + C\n^C\nINFO:     Shutting down\nINFO:     Waiting for application shutdown.\nINFO:     Finalizando a aplica\u00e7\u00e3o\nINFO:     Application shutdown complete.\nINFO:     Finished server process [254037]\n
"},{"location":"aulas/sincronas/","title":"Aulas s\u00edncronas","text":""},{"location":"aulas/sincronas/#aulas-sincronas","title":"Aulas s\u00edncronas","text":"

Faremos 14 encontros para as aulas s\u00edncronas em formato de live no meu canal do YouTube entre as datas de 11/06 e 25/07.

"},{"location":"aulas/sincronas/#como-vai-funcionar","title":"Como vai funcionar?","text":"

Nossos encontros acontecer\u00e3o as ter\u00e7as e quintas com dura\u00e7\u00e3o de 1h30m. Entre \u00e0s 20:00h e 21:30. Com chat aberto para tirar d\u00favidas enquanto a aula acontece. O material base \u00e9 o que est\u00e1 disposto neste site.

"},{"location":"aulas/sincronas/#agenda","title":"Agenda","text":"N Aula Data Link 00 Abertura e apresenta\u00e7\u00e3o do curso 11/06 Aula 00 01 Configurando o Ambiente de Desenvolvimento 13/06 Aula 01 02 Introdu\u00e7\u00e3o ao desenvolvimento WEB 18/06 Aula 02 03 Estruturando o Projeto e Criando Rotas CRUD 20/06 Aula 03 04 Configurando o Banco de Dados e Gerenciando Migra\u00e7\u00f5es com Alembic 25/06 Aula 04 05 Integrando Banco de Dados a API 27/06 Aula 05 06 Autentica\u00e7\u00e3o e Autoriza\u00e7\u00e3o com JWT 02/07 Aula 06 07 Refatorando a Estrutura do Projeto 04/07 Aula 07 S Aula reservada para tirar d\u00favidas 09/07 Aula d\u00favidas 08 Tornando o sistema de autentica\u00e7\u00e3o robusto 11/07 Aula 08 09 Criando Rotas CRUD para Gerenciamento de Tarefas 16/07 Aula 09 10 Dockerizando a nossa aplica\u00e7\u00e3o e introduzindo o PostgreSQL 18/07 Aula 10 11 Automatizando os testes com Integra\u00e7\u00e3o Cont\u00ednua (CI) 23/07 Aula 11 12 Fazendo deploy no Fly.io 25/07 Aula 12 13 Despedida e pr\u00f3ximos passos 30/07 Aula 13

Agenda com as datas:

Caso queira conversar e tirar d\u00favidas com as pessoas que tamb\u00e9m est\u00e3o/ir\u00e3o fazer o curso, temos um grupo no Telegram.

"},{"location":"aulas/sincronas/#o-que-sera-necessario-para-acompanhar","title":"O que ser\u00e1 necess\u00e1rio para acompanhar?","text":"
  1. Um link com internet para participar das lives
  2. Um computador para acompanhar as aulas
  3. Um sistema operacional que seja capaz de rodar o Docker (algumas vers\u00f5es do Windows tem problemas com isso)
  4. Uma conta no GitHub para hospedar os c\u00f3digos e exerc\u00edcios
  5. Uma conta no Fly.io para fazer o deploy de sua aplica\u00e7\u00e3o
"},{"location":"exercicios_resolvidos/aula_01/","title":"Exerc\u00edcios da aula 01","text":""},{"location":"exercicios_resolvidos/aula_01/#exercicios-da-aula-01","title":"Exerc\u00edcios da aula 01","text":""},{"location":"exercicios_resolvidos/aula_01/#exercicio-01","title":"Exerc\u00edcio 01","text":"

Crie um reposit\u00f3rio para acompanhar o curso e suba em alguma plataforma, como Github, gitlab, codeberg, etc. E compartilhe o link no reposit\u00f3rio do curso para podermos aprender juntos.

"},{"location":"exercicios_resolvidos/aula_02/","title":"Exerc\u00edcios da aula 02","text":""},{"location":"exercicios_resolvidos/aula_02/#exercicios-da-aula-02","title":"Exerc\u00edcios da aula 02","text":""},{"location":"exercicios_resolvidos/aula_02/#exercicio-01","title":"Exerc\u00edcio 01","text":"

Crie um endpoint que retorne \"ol\u00e1 mundo\" usando HTML e escreva seu teste.

Dica: para capturar a resposta do HTML do cliente de testes, voc\u00ea pode usar response.text

"},{"location":"exercicios_resolvidos/aula_02/#solucao","title":"Solu\u00e7\u00e3o","text":"

Para cria\u00e7\u00e3o do endpoint retornando HTML devemos alterar a classe de resposta padr\u00e3o do FastAPI para HTMLResponse:

Implementa\u00e7\u00e3o do endpoint
from fastapi import FastAPI\nfrom fastapi.responses import HTMLResponse\n\napp = FastAPI()\n\n\n@app.get('/', response_class=HTMLResponse)\ndef read_root():\n    return \"\"\"\n    <html>\n      <head>\n        <title> Nosso ol\u00e1 mundo!</title>\n      </head>\n      <body>\n        <h1> Ol\u00e1 Mundo </h1>\n      </body>\n    </html>\"\"\"\n

O teste que faz a valida\u00e7\u00e3o do valor retornado pelo endpoint n\u00e3o precisa ser muito robusto. A ideia principal do exerc\u00edcio \u00e9 somente validar se estamos retornando o \"Ol\u00e1 Mundo\" em formato de HTML:

Implementa\u00e7\u00e3o do teste
from http import HTTPStatus\n\nfrom fastapi.testclient import TestClient\n\nfrom fast_zero.app import app\n\n\ndef test_root_deve_retornar_ola_mundo_em_html():\n    client = TestClient(app)\n\n    response = client.get('/')\n\n    assert response.status_code == HTTPStatus.OK\n    assert '<h1> Ol\u00e1 Mundo </h1>' in response.text\n

O response.text \u00e9 um m\u00e9todo do cliente de testes do FastAPI que converte os bytes de resposta em string.

"},{"location":"exercicios_resolvidos/aula_03/","title":"Exerc\u00edcios da aula 03","text":""},{"location":"exercicios_resolvidos/aula_03/#exercicios-da-aula-03","title":"Exerc\u00edcios da aula 03","text":""},{"location":"exercicios_resolvidos/aula_03/#exercicio-01","title":"Exerc\u00edcio 01","text":"

Escreva um teste para o erro de 404 (NOT FOUND) para o endpoint de PUT.

"},{"location":"exercicios_resolvidos/aula_03/#solucao","title":"Solu\u00e7\u00e3o","text":"

A ideia de um teste de 404 para o PUT \u00e9 tentar fazer a altera\u00e7\u00e3o de um usu\u00e1rio que n\u00e3o existe no banco de dados.

Teste de 404
def test_update_user_should_return_not_found__exercicio(client):\n    response = client.put(\n        '/users/666', #(1)!\n        json={\n            'username': 'bob',\n            'email': 'bob@example.com',\n            'password': 'mynewpassword',\n        },\n    )\n    assert response.status_code == HTTPStatus.NOT_FOUND #(2)!\n    assert response.json() == {'detail': 'User not found'} #(3)!\n
  1. O user com id 666 n\u00e3o existe no nosso sistema.
  2. Como o user n\u00e3o existe, o status code retornado pela fun\u00e7\u00e3o ser\u00e1 404
  3. Por entrar no bloco de valida\u00e7\u00e3o do if o HTTPException foi preenchido com detail='User not found'
"},{"location":"exercicios_resolvidos/aula_03/#exercicio-02","title":"Exerc\u00edcio 02","text":"

Escreva um teste para o erro de 404 (NOT FOUND) para o endpoint de DELETE

"},{"location":"exercicios_resolvidos/aula_03/#solucao_1","title":"Solu\u00e7\u00e3o","text":"

A ideia de um teste de 404 para o DELETE \u00e9 tentar fazer a altera\u00e7\u00e3o de um usu\u00e1rio que n\u00e3o existe no banco de dados.

Teste de 404
def test_delete_user_should_return_not_found__exercicio(client):\n    response = client.delete('/users/666') #(1)!\n\n    assert response.status_code == HTTPStatus.NOT_FOUND #(2)!\n    assert response.json() == {'detail': 'User not found'} #(3)!\n
  1. O user com id 666 n\u00e3o existe no nosso sistema.
  2. Como o user n\u00e3o existe, o status code retornado pela fun\u00e7\u00e3o ser\u00e1 404
  3. Por entrar no bloco de valida\u00e7\u00e3o do if o HTTPException foi preenchido com detail='User not found'
"},{"location":"exercicios_resolvidos/aula_03/#exercicio-03","title":"Exerc\u00edcio 03","text":"

Crie um endpoint de GET para pegar um \u00fanico recurso como users/{id} e fazer seus testes para 200 e 404.

"},{"location":"exercicios_resolvidos/aula_03/#solucao_2","title":"Solu\u00e7\u00e3o","text":"

A implementa\u00e7\u00e3o do endpoint \u00e9 bastante parecida com as que fizemos at\u00e9 agora. Precisamos validar se existe um id compar\u00edvel no nosso banco de dados falso. Nos baseando pela posi\u00e7\u00e3o do elento na lista.

@app.get('/users/{user_id}', response_model=UserPublic)\ndef read_user__exercicio(user_id: int):\n    if user_id > len(database) or user_id < 1:\n        raise HTTPException(\n            status_code=HTTPStatus.NOT_FOUND, detail='User not found'\n        )\n\n    return database[user_id - 1]\n

Um dos testes \u00e9 sobre o retorno 404, que \u00e9 retornado um user que n\u00e3o existe na base de dados e outro \u00e9 o comportamento padr\u00e3o para quando o user \u00e9 retornado com sucesso:

def test_get_user_should_return_not_found__exercicio(client):\n    response = client.get('/users/666')\n\n    assert response.status_code == HTTPStatus.NOT_FOUND\n    assert response.json() == {'detail': 'User not found'}\n\n\ndef test_get_user___exercicio(client):\n    response = client.get('/users/1')\n\n    assert response.status_code == HTTPStatus.OK\n    assert response.json() == {\n        'username': 'bob',\n        'email': 'bob@example.com',\n        'id': 1,\n    }\n
"},{"location":"exercicios_resolvidos/aula_04/","title":"Exerc\u00edcios da aula 04","text":""},{"location":"exercicios_resolvidos/aula_04/#exercicios-da-aula-04","title":"Exerc\u00edcios da aula 04","text":""},{"location":"exercicios_resolvidos/aula_04/#exercicio-01","title":"Exerc\u00edcio 01","text":"

Fazer uma altera\u00e7\u00e3o no modelo (tabela User) e adicionar um campo chamado updated_at:

"},{"location":"exercicios_resolvidos/aula_04/#solucao","title":"Solu\u00e7\u00e3o","text":"
@table_registry.mapped_as_dataclass\nclass User:\n    __tablename__ = 'users'\n\n    id: Mapped[int] = mapped_column(init=False, primary_key=True)\n    username: Mapped[str] = mapped_column(unique=True)\n    password: Mapped[str]\n    email: Mapped[str] = mapped_column(unique=True)\n    created_at: Mapped[datetime] = mapped_column(\n        init=False, server_default=func.now()\n    )\n    updated_at: Mapped[datetime] = mapped_column(  # Exerc\u00edcio\n        init=False, server_default=func.now(), onupdate=func.now()\n    )\n
"},{"location":"exercicios_resolvidos/aula_04/#exercicio-02","title":"Exerc\u00edcio 02","text":"

Criar uma nova migra\u00e7\u00e3o autogerada com alembic.

"},{"location":"exercicios_resolvidos/aula_04/#solucao_1","title":"Solu\u00e7\u00e3o","text":"

Comando explicado na aula para gerar uma migra\u00e7\u00e3o autom\u00e1tica:

$ Execu\u00e7\u00e3o no terminal!
alembic revision --autogenerate -m \"exercicio 02 aula 04\"\n

O Comando deve retornar algo parecido com isso:

Resultado do comando
INFO  [alembic.runtime.migration] Context impl SQLiteImpl.\nINFO  [alembic.runtime.migration] Will assume non-transactional DDL.\nINFO  [alembic.autogenerate.compare] Detected added column 'users.updated_at'\n  Generating /home/dunossauro/git/fastapi-do-\n  zero/codigo_das_aulas/04/migrations/versions/bb77f9679811_exercicio_02_aula_04.py ...  done\n

O arquivo de migra\u00e7\u00f5es deve se parecer com esse:

/migrations/versions/bb77f9679811_exercicio_02_aula_04.py
\"\"\"exercicio 02 aula 04\n\nRevision ID: bb77f9679811\nRevises: 74f39286e2f6\nCreate Date: ...\n\n\"\"\"\nfrom typing import Sequence, Union\n\nfrom alembic import op\nimport sqlalchemy as sa\n\n\n# revision identifiers, used by Alembic.\nrevision: str = 'bb77f9679811'\ndown_revision: Union[str, None] = '74f39286e2f6'\nbranch_labels: Union[str, Sequence[str], None] = None\ndepends_on: Union[str, Sequence[str], None] = None\n\n\ndef upgrade() -> None:\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.add_column('users', sa.Column('updated_at', sa.DateTime(), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False)) #(1)!\n    # ### end Alembic commands ###\n\n\ndef downgrade() -> None:\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.drop_column('users', 'updated_at') #(2)!\n    # ### end Alembic commands ###\n
  1. Adiciona a coluna updated_at na tabela users
  2. Remove a coluna updated_at na tabela users
"},{"location":"exercicios_resolvidos/aula_04/#exercicio-03","title":"Exerc\u00edcio 03","text":"

Aplicar essa migra\u00e7\u00e3o ao banco de dados

"},{"location":"exercicios_resolvidos/aula_04/#solucao_2","title":"Solu\u00e7\u00e3o","text":"

Para aplicar a ultima migra\u00e7\u00e3o devemos nos mover at\u00e9 a head:

$ Execu\u00e7\u00e3o no terminal!
alembic upgrade head\nINFO  [alembic.runtime.migration] Context impl SQLiteImpl.\nINFO  [alembic.runtime.migration] Will assume non-transactional DDL.\nINFO  [alembic.runtime.migration] Running upgrade 74f39286e2f6 -> bb77f9679811, exercicio 02 aula 04\n

Checando o resultado no schema do banco de dados:

$ Execu\u00e7\u00e3o no terminal!
sqlite3 database.db \nSQLite version 3.46.1 2024-08-13 09:16:08\nEnter \".help\" for usage hints.\nsqlite> .schema\nCREATE TABLE alembic_version (\n        version_num VARCHAR(32) NOT NULL, \n        CONSTRAINT alembic_version_pkc PRIMARY KEY (version_num)\n);\nCREATE TABLE users (\n        id INTEGER NOT NULL, \n        username VARCHAR NOT NULL, \n        password VARCHAR NOT NULL, \n        email VARCHAR NOT NULL, \n        created_at DATETIME DEFAULT (CURRENT_TIMESTAMP) NOT NULL,\n        updated_at DATETIME DEFAULT (CURRENT_TIMESTAMP) NOT NULL, \n        PRIMARY KEY (id), \n        UNIQUE (email), \n        UNIQUE (username)\n);\n

Podemos ver que o campo updated_at foi criado com o tipo DATETIME e com o valor padr\u00e3o para CURRENT_TIMESTAMP, assim como no created_at.

"},{"location":"exercicios_resolvidos/aula_05/","title":"Exerc\u00edcios da aula 05","text":""},{"location":"exercicios_resolvidos/aula_05/#exercicios-da-aula-05","title":"Exerc\u00edcios da aula 05","text":""},{"location":"exercicios_resolvidos/aula_05/#exercicio-01","title":"Exerc\u00edcio 01","text":"

Escrever um teste para o endpoint de POST (create_user) que contemple o cen\u00e1rio onde o username j\u00e1 foi registrado. Validando o erro 400.

"},{"location":"exercicios_resolvidos/aula_05/#solucao","title":"Solu\u00e7\u00e3o","text":"

Para testar esse cen\u00e1rio, precisamos de um username que j\u00e1 esteja registrado na base de dados. Para isso, podemos usar a fixture de user que criamos. Ela \u00e9 uma garantia que o valor j\u00e1 est\u00e1 inserido no banco de dados:

/tests/test_app.py
def test_create_user_should_return_400_username_exists__exercicio(client, user):\n    response = client.post(\n        '/users/',\n        json={\n            'username': user.username,\n            'email': 'alice@example.com',\n            'password': 'secret',\n        },\n    )\n    assert response.status_code == HTTPStatus.BAD_REQUEST\n    assert response.json() == {'detail': 'Username already exists'}\n
"},{"location":"exercicios_resolvidos/aula_05/#exercicio-02","title":"Exerc\u00edcio 02","text":"

Escrever um teste para o endpoint de POST (create_user) que contemple o cen\u00e1rio onde o e-mail j\u00e1 foi registrado. Validando o erro 400.

"},{"location":"exercicios_resolvidos/aula_05/#solucao_1","title":"Solu\u00e7\u00e3o","text":"

Para testar esse cen\u00e1rio, precisamos de um e-mail que j\u00e1 esteja registrado na base de dados. Para isso, podemos usar a fixture de user que criamos. Ela \u00e9 uma garantia que o valor j\u00e1 est\u00e1 inserido no banco de dados:

/tests/test_app.py
def test_create_user_should_return_400_email_exists__exercicio(client, user):\n    response = client.post(\n        '/users/',\n        json={\n            'username': 'alice',\n            'email': user.email,\n            'password': 'secret',\n        },\n    )\n    assert response.status_code == HTTPStatus.BAD_REQUEST\n    assert response.json() == {'detail': 'Email already exists'}\n
"},{"location":"exercicios_resolvidos/aula_05/#exercicio-03","title":"Exerc\u00edcio 03","text":"

Atualizar os testes criados nos exerc\u00edcios 1 e 2 da aula 03 para suportarem o banco de dados.

"},{"location":"exercicios_resolvidos/aula_05/#solucao_2","title":"Solu\u00e7\u00e3o","text":"

O objetivo desse exerc\u00edcio n\u00e3o necessariamente uma atualiza\u00e7\u00e3o dos testes, mas o caso de uma execu\u00e7\u00e3o para validar se os testes, como foram feitos ainda funcionariam nessa nova estrutura.

Os meus testes da aula 03:

def test_delete_user_should_return_not_found__exercicio(client):\n    response = client.delete('/users/666')\n\n    assert response.status_code == HTTPStatus.NOT_FOUND\n    assert response.json() == {'detail': 'User not found'}\n\n\ndef test_update_user_should_return_not_found__exercicio(client):\n    response = client.put(\n        '/users/666',\n        json={\n            'username': 'bob',\n            'email': 'bob@example.com',\n            'password': 'mynewpassword',\n        },\n    )\n    assert response.status_code == HTTPStatus.NOT_FOUND\n    assert response.json() == {'detail': 'User not found'}\n

Ao executar eles continuam passando.

"},{"location":"exercicios_resolvidos/aula_05/#exercicio-04","title":"Exerc\u00edcio 04","text":"

Implementar o banco de dados para o endpoint de listagem por id, criado no exerc\u00edcio 3 da aula 03.

"},{"location":"exercicios_resolvidos/aula_05/#solucao_3","title":"Solu\u00e7\u00e3o","text":"

Esse exerc\u00edcio basicamente consiste em duas partes. A primeira \u00e9 alterar o endpoint para usar o banco de dados. Isso pode ser feito de maneira simples injetando a depend\u00eancia da session:

@app.get('/users/{user_id}', response_model=UserPublic)\ndef read_user__exercicio(\n    user_id: int, session: Session = Depends(get_session)\n):\n    db_user = session.scalar(select(User).where(User.id == user_id))\n\n    if not db_user:\n        raise HTTPException(\n            status_code=HTTPStatus.NOT_FOUND, detail='User not found'\n        )\n\n    return db_user\n

A segunda parte \u00e9 entender o que precisa ser feito nos testes para que eles consigam cobrir os dois casos previstos. O de sucesso e o de falha.

O teste de sucesso continua passando, pois ele de fato n\u00e3o depende de nenhuma intera\u00e7\u00e3o com o banco de dados:

def test_get_user_should_return_not_found__exercicio(client):\n    response = client.get('/users/666')\n\n    assert response.status_code == HTTPStatus.NOT_FOUND\n    assert response.json() == {'detail': 'User not found'}\n

J\u00e1 o teste de sucesso, depende que exista um usu\u00e1rio na base dados. Com isso podemos usar a fixture de user tanto na chamada, quanto na valida\u00e7\u00e3o dos dados:

def test_get_user___exercicio(client, user):\n    response = client.get(f'/users/{user.id}')\n\n    assert response.status_code == HTTPStatus.OK\n    assert response.json() == {\n        'username': user.username,\n        'email': user.email,\n        'id': user.id,\n    }\n
"},{"location":"exercicios_resolvidos/aula_06/","title":"Exerc\u00edcios da aula 06","text":""},{"location":"exercicios_resolvidos/aula_06/#exercicios-da-aula-06","title":"Exerc\u00edcios da aula 06","text":""},{"location":"exercicios_resolvidos/aula_06/#exercicio-01","title":"Exerc\u00edcio 01","text":"

Fa\u00e7a um teste para cobrir o cen\u00e1rio que levanta exception credentials_exception na autentica\u00e7\u00e3o caso o User n\u00e3o seja encontrado. Ao olhar a cobertura de security.py voc\u00ea vai notar que esse contexto n\u00e3o est\u00e1 coberto.

"},{"location":"exercicios_resolvidos/aula_06/#solucao","title":"Solu\u00e7\u00e3o","text":"

...

"},{"location":"exercicios_resolvidos/aula_06/#exercicio-02","title":"Exerc\u00edcio 02","text":"

Reveja os testes criados at\u00e9 a aula 5 e veja se eles ainda fazem sentido (testes envolvendo 400)

"},{"location":"exercicios_resolvidos/aula_06/#solucao_1","title":"Solu\u00e7\u00e3o","text":"

...

"},{"location":"projetos/projetos_finais/","title":"Projetos finais","text":""},{"location":"projetos/projetos_finais/#projetos-finais","title":"Projetos finais","text":"

O objetivo dessa p\u00e1gina \u00e9 unir todos os projetos finais de pessoas que fizeram o curso em uma tabela para que voc\u00ea possa consultar, estudar, aprender com as pessoas que tamb\u00e9m fizeram o curso e ver como elas criaram o projeto final.

Caso seu projeto final n\u00e3o esteja aqui, o poste nessa issue

Link do projeto Seu @ no git Coment\u00e1rio (opcional) projeto @dunossauro Ainda n\u00e3o fiz FastZero-MADR @clcosta - madr @alfmorais - bookshelf @Tomas-Tamantini - fast_zero_projeto_final vitorTheDev \u00d3timo curso! madr elyssonmr Sensacional. Consegui fazer tudo async :) madr arturpeixoto Muitos aprendizados! madr_fast @itsGab Projeto baseado no curso, com fastapi_pagination. Obrigado pelo \u00f3timo curso! app_library @thigoap Docker est\u00e1 em uma branch separada (docker). madr @henriquesebastiao 201 CREATED \u2705 fastapi-madr @michelebswm Sensacional madr @taconi API async, com templates usando Jinja2 e httpx e 99.9% em ~portugu\u00eas~ brasileiro. madr @MuriloRohor MADR @gabitrombetta madr @danielbrito91 Baita curso \u2764\ufe0f madr-fastapi @lealre tcc_madr @guilopes15 madr @eduardoklosowski Projeto utilizando Dev Containers integrado ao Visual Studio Code, deploy no Kubernetes via Helm usando um cluster local gerenciado pelo minikube. Mais detalhes confira o hist\u00f3rico do projeto. madr @Romariolima1998 mada_sync LuizPerciliano Projeto fluindo de vento em popa, muito obrigado Edu por t\u00e3o grande aprendizado! \u2764\ufe0f fastapi-acervo-digital @heltonteixeira92 Conte\u00fado supimpa \ud83d\ude80 madr @duca_meneses Excelente curso MADR @hugocs1 Muito foda, obrigado! fastapi_zero_madr_projeto_final @devfabiopedro \ud83d\udcbb Feito! \u270c\ufe0f\ud83d\ude01"},{"location":"projetos/repositorios/","title":"Reposit\u00f3rios do curso","text":""},{"location":"projetos/repositorios/#repositorios-do-curso","title":"Reposit\u00f3rios do curso","text":"

O objetivo dessa p\u00e1gina \u00e9 unir todos os reposit\u00f3rios de pessoas que fizeram o curso em uma tabela para que voc\u00ea possa consultar, estudar, aprender com as pessoas que tamb\u00e9m fizeram o curso e ver como elas resolveram os exerc\u00edcios do curso.

Caso o seu reposit\u00f3rio n\u00e3o esteja aqui, \u00e9 por que voc\u00ea n\u00e3o resolveu os exerc\u00edcios da Primeira aula

Link do projeto Seu @ no git Coment\u00e1rio (opcional) fast_zero @dunossauro Implementa\u00e7\u00e3o do material do curso sem altera\u00e7\u00f5es e sem exerc\u00edcios fast_zero @morgannadev Implementa\u00e7\u00e3o do material do curso sem altera\u00e7\u00f5es fast_zero @rodrigosbarretos Foi bacana enfrentar os problemas instalando as coisas no Ubuntu no WLS fast_zero @azmovi Que projeto bacana dudu, muito obrigado fastapi-do-zero @aguynaldo Estudo a partir do curso de FastAPI do Dunossauro. fastapi-do-zero @gercinei Minha primeira experi\u00eancia com um framework fast_zero @ju14x Implementa\u00e7\u00e3o do material do curso sem altera\u00e7\u00f5es Fast_zero @IsisG13 Estudando com o curso de FastAPI economio @marcos-ag-nolasco Criando um app fullstack, cujo backend vai ser baseado no fast_zero fastapi-do-zero @RWallan Tentando implementar o curso com Async fastapi-do-zero @gylmonteiro Estudos inicias com fastapi crono_task_backend @mau-me App para gerenciamento de tasks, com o backend baseado no fast_zero fast_zero @navegantes Mais uma ferramenta de paito pra caixinha fast_zero @willrockoliv Projeto incr\u00edvel @dunossauro! Muito obrigado!! fastapi-training @Brunoliy Implementa\u00e7\u00e3o do material do curso sem altera\u00e7\u00f5es backend-portfolio @stherzada Implementa\u00e7\u00e3o do curso e aprimorando aprendizado no backend \u2728 fast_zero @lbmendes Usando a VM gratis da OCI para fazer o Curso fast_zero @vilmarspies Implementa\u00e7\u00e3o do material do curso sem altera\u00e7\u00f5es fast_zero @RogerPatriota Implementa\u00e7\u00e3o do material do curso sem altera\u00e7\u00f5es fast-zero @machadoah Aprendendo FastAPI \ud83d\udc0d \u2728 fast_zero @FabricioPython Curtindo FastAPI fast_api @juniohorniche Massa demais esse conte\u00fado \ud83d\ude0d fast_zero @taconi Com hatch, async, podman, Woodpecker CI e hospedado no Codeberg fastapi-do-zero @joceliovieira Repo com material pessoal (notas, c\u00f3digos, etc), sem clone do repo oficial curso-fastapi-webdev @joaobugelli Parab\u00e9ns pelo conte\u00fado e material excelentes! Voc\u00ea \u00e9 demais Duno! notas-musicais-api @rochamatcomp API para o Notas musicais fast_cometa @mpdiasrosa Estudando FastAPI do zero \ud83d\udc0d fastapi_do_zero @arturfarias Projeto de estudos com poetry e fastAPI fastapi-do-zero-dunossauro @leopoldocouto Material de estudo do Curso de FastAPI do Zero do @dunossauro fast_zero @psifabiohenrique Implementa\u00e7\u00e3o do material do curso sem altera\u00e7\u00f5es fast_zero 2005869 Implementa\u00e7\u00e3o do material sem altera\u00e7\u00f5es fast_zero @miguelferreiraZ Estudo a partir do curso: Fast-API do Zero fast_zero @kassoliver Aprendendo um pouco mais de FastAPI com o Dunossauro \ud83d\udc0d fast_zero @jhonatacaiob Aprendendo um pouco de FastAPI com o Dunossauro \ud83d\udc0d fast_zero @arnaldovitor Material de estudo do curso \"FastAPI do Zero\" do @dunossauro fast_zero @vcwild Acompanhando o conte\u00fado do curso s\u00edncrono fast_api @viniciusaito Curtindo as aulas do curso de fast api fast_zero @andreztz Aprendendo FastAPI com @dunossauro \ud83d\udc0d fast_zero @SouzaPatrick Aprendendo um pouco de FastAPI com o Dunossauro \ud83d\udc0d fastzero @AndreGM Aprendendo FastAPI com @dunossauro fastapidozero-dunossaudo @francadev Aprendendo FastAPI com @dunossauro course_fast_api_zero @vmfrois Aprendendo FastAPI com @dunossauro fastapi-do-zero @Everton92 Aprendendo FastAPI com o mais brabo @dunossauro fastapi_zero_duno @guiribeirodev Desenvolvimento Web e FastAPI com o @dunossauro fast_zero @andrefelipemsc Implementa\u00e7\u00e3o do material do curso sem altera\u00e7\u00f5es. Porque este \u00e9 o melhor e mais completo curso da internet. fast_zero @jlplautz Projeto baseado no curso FastAPI com o mestre Dunossauro. fasst_zero @prpires66 Implementa\u00e7\u00e3o do material do curso sem altera\u00e7\u00f5es fastAPI_do_zero @BrunoPinheirofe Primeiros passos com FastAPI fastapidozero @lucaspaimrj21 Configurando o ambiente de desenvolvimento e primeiro commit fast_api_todo @joiltonrsilva Desenvolvendo aplica\u00e7\u00e3o TODO com FastAPI nas aulas do prof. @dunossauro fast_zero @duca-meneses Aprendendo mais sobre fastAPI com o @dunossauro fast_split @thigoap FastAPI com o nome do futuro projeto. fast_sync @edisonmsj First project using fast api FastAPI_do_ZERO @GedeilsonLopes Curso foda demais\u00a0@dunossauro! fast_zero_sync @animarubr Implementa\u00e7\u00e3o do material do curso na plataforma windows dunossauro_fast_api @danielbrito91 Implementa\u00e7\u00e3o do curso fast_zero_sync @marcossa Projeto produzido durante a aula. Aprendendo Python hands-on! fast_zero @FilipeNeiva Muito bom o curso fast_zero @elyssonmr Muito bom aprender ao vivo fast_zero_sync @WilliamCutrim Muito bom fastapi-do-zero @paulinhomacedo Obrigado Edu por sua disposi\u00e7\u00e3o de ensiar. fast_zero_api @peixoto-pc Implementa\u00e7\u00e3o do material do curso sem altera\u00e7\u00f5es fast_do_dunossauro @hebertn88 Projeto desenvolvido durante Curso FastAPI do Zero Toad_list @victorvhs Implementa\u00e7\u00e3o do material do curso sem altera\u00e7\u00f5es fast_zero @josedembo Implementa\u00e7\u00e3o do material do curso sem altera\u00e7\u00f5es curso-fastapi-dunossauro @sigaocaue Implementa\u00e7\u00e3o do material do curso de FastAPI do Dunossauro fast_zero_sync @RRFFTT Meu primeiro projeto, construindo uma API fast_zero @Alan-Gomes1 Implementa\u00e7\u00e3o do material do curso sem altera\u00e7\u00f5es fast_api @PedroP7l Implementa\u00e7\u00e3o do material do curso sem altera\u00e7\u00f5es estudos-fastapi @vizagre Upload da primeira tarefa de aula fastapi-do-zero @gleissonribeiro Projeto desenvolvido durante o curso de fastapi do @dunossauro (Eduardo Mendes) em 2024. fast_zero @cesargodoi muito obrigado pelo empenho e pelo conte\u00fado fastapi_do_zero_dunossauro @CleberNandi fast_zero @alvie40 Excelente did\u00e1tica e estrutura do curso. Obrigado fast_zero @LisandroGuerra Obrigado por este curso excelente! Apredendo tamb\u00e9m a usar o Poetry. fastapi_todo @dubirajara Acompanhando o curso de FastApi do Zero fast_api_zero @IgorStrauss Excelente metodologia, e conceitos muito importantes para o dia a dia na carreira de Dev. fast_zero @divirjo fast_zero @sandrocarval - fast_zero @migueluff - fast_zero @gustaaragao Bem divertido ;) fast_zero @DanielDlc Muito bom, conte\u00fado feito com carinho e intelig\u00eancia. FastAPI_dunossauro @Fernanda-Prado Conte\u00fado excelente, desconhecia o taskipy e quero colocar ele em todo projeto meu to_do_list FrAnKlInSousa - fast_zero @itsGab - Fastapi @kleytonls Muito Obrigado pela dedica\u00e7\u00e3o em fazer um conte\u00fado de t\u00e3o boa qualidade dunossauro FastAPIZero @Leandro-VS Conteudo incr\u00edvel desse curso fast_zero @gabriel19913 Estava a tempos na expectativa por esse curso! T\u00f4 muito ansioso pra aprender coisas novas! fast_zero @marlonato Curso excelente, adorei ver a ideia do ruff e pytest fast_zero @joncasdam Que saudade de lidar com python fast_zero_sync @gabriellcristiann Did\u00e1tica incrivel cara Parab\u00e9ns fast_zero @GuilhermeAndre1 Baita aula boa! full_fast_api @Oseiasdfarias Bom demaizi fast_zero @rbsantosbr Projeto sensacional, aprendizado muito al\u00e9m das tecnologias! fast_zero @CarlosPetrikov Reposit\u00f3rio referente ao curso de FastAPI do Eduardo Mendes fast_zero @Samaelpicoli Aprendendo FastAPI, conte\u00fado sensacional! fast_zero @WesleyPacca Come\u00e7ando FastAPI fastapi_zero @emanoelmendes2 Aprendendo FastAPI fastapi-zero @joaobrc Reposit\u00f3rio do curso de FastAPI fast_api_curso matheuspdf Excelente curso fast_zero @Gui-mp8 Melhor Curso! fast_zero @HulitosCode Fazendo o curso de Fastapi do Zero fast_zero_sync @renatobarramansa Projeto utilizando fastApi fast_zero @renatonaper fast_zero @lidymonteiro Reposit\u00f3rio do curso de FastAPI fast_zero @dgeison Estou utilizando o Windows Subsystem for Linux (WSL) para desenvolver em FastAPI. Valeu pela explica\u00e7\u00e3o, did\u00e1tica, conte\u00fado e material. FastAPI_Lab @tallesemmanuel Por mais que saiba algo, vi que n\u00e3o sei de nada FAST-API Francisco-Libanio Iniciei o projeto estou usando pycharm fast_zero @KrisEgidio Aprendendo FastAPI seguindo o curso FastAPI do Zero! Fast_api_sync @JoaoGBC Aprendendo FastAPI seguindo o curso FastAPI do Zero! fastapi @PedroP7l Implementa\u00e7\u00e3o do material do curso sem altera\u00e7\u00f5es fast_zero_sync @diogogonnelli Implementa\u00e7\u00e3o do material do curso fast_zero_sync @davidrangelrj Implementa\u00e7\u00e3o do material do curso fastapi_project @alsombra Aprendendo fastapi e webdev com a lenda Dunossauro fast_zero @alyssondaniel Implementa\u00e7\u00e3o do material do curso\u00a0COM\u00a0altera\u00e7\u00f5es fast_zero @eduardoalsilva api_master @matheusfly Implementa\u00e7\u00e3o do material do curso sem altera\u00e7\u00f5es fast_zero @MatheusLPolidoro Implementa\u00e7\u00e3o do material do curso sem altera\u00e7\u00f5es running_fast_api @santana98 Repo acompanhando as aulas do curso fastapi-do-zero @thiagosouzalink Excelente curso, parab\u00e9ns! fast_zero_sync @caio-io Minha primeira API fast_zero_sync @giovanezanardo0 Curso top demais fast_zero_sync @Matheus-Novoa Primeiro projeto web com python fast_api_tutorial @Tomas_Tamantini Aprendendo Fast API fast_zero @FariasMi Aprendendo Fast API (dunossauro sou sua f\u00e3) FastOpenDBBrasil @NercinoN21 Uma API Python com FastAPI para descoberta de bases de dados p\u00fablicas do Brasil por tema, ideal para pesquisa e estudos. fast_zero @MuriloRoho fast_zero @flaviacastro - fast_zero @vizagre Tive problemas com o WSL e recriei o projeto do zero no windows. Esse \u00e9 o novo repositorio fast_zero @w1zard Implementa\u00e7\u00e3o do material do curso sem altera\u00e7\u00f5es APIcultura @rmndvngrpslhr Fazendo o curso sozinho foi quando montei uma APIs pela primeira, agora t\u00e1 sendo divertido refazer tudo revendo o conte\u00fado no modo s\u00edncrono o Edu fast_zero_sync @andrescalassara fast_zero Victor Berselli Valeu Eduardo! \ud83e\udd96 festapi Santos Duuu obrigada \ud83e\udd96! my-fastapi @slottwo Implementa\u00e7\u00e3o do material do curso com pequenas altera\u00e7\u00f5es fast_zero @AmeriCodes To perdidinho kk fast_zero @NataGago \u00e9 a tropa do Dunossauro! \ud83e\udd96 fast_zero @Dxm42 Muito obrigado por criar este curso! Estou aprendendo muito. fast_zero_sync @FelipeSantiagoMenezes Estou gostando muito do curso! learning-fastapi @fernandoortolan Implementa\u00e7\u00e3o do material do curso. fast_zero @rafaael1 Aprendendo Fast API, Valeu Eduardo! \ud83e\udd96 fast_zero @felipeCaetano Fazendo o Curso de FastAPI fast_zero @thiagosp Vamos pra cima!!! fast_zero @ssantos89 Aprendendo FastAPI com Dunossauro - First Commit fast_zero_classes @oTerra Projeto criado com base no curso FastAPI do zero do Eduardo Mendes fast_zero @Cmte-Kirk Aspas duplas \u00e9 pra quem tem as duas m\u00e3os! Gostei dessa frase! fast_zero @eduardobrennand Muito bom! \ud83d\ude80\ud83d\ude80\ud83d\ude80 fastapi-dunossauro @gillianoliveira Conte\u00fado nota 100! fast_zero @danweb80 Acompanhamento do Curso FasAPI do Dunossauro fast_api_zero @anselmotaccola fast_zero @epfolletto Curso de FastAPI - Live de Python - Eduardo Mendes fastapi-do-zero-exercicios rg3915 Implementa\u00e7\u00e3o do material do curso sem altera\u00e7\u00f5es. fastapi_zero @devfabiopedro Curso de FastAPI do Dunossauro fastapi_zero @baronetogio Curso de FastAPI fast_zero @thalissonvs Implementa\u00e7\u00e3o do material do curso sem altera\u00e7\u00f5es fast_api_project @guilherme.canfield Obrigado mestre Dunossauro! fastapi_zero @edipolferreira O curso est\u00e1 sensacional! fast_api_zero @brunopmendes Curso t\u00e1 demais (amei a integra\u00e7\u00e3o nativa com o swagger) fast_zero fabiomattes2016 Ta lindo esse curso, continue assim :) fast_zero @tuxanator fastapi, seu lindo. fast_zero @thamibetin Aprendendo mais de Python \ud83d\udc0d com o melhor! \ud83d\udcab fast_zero @washingtonnuness Parab\u00e9ns pelo conte\u00fado e material excelentes! Voc\u00ea \u00e9 demais Duno! FastAPI_Du_Zero @rodten23 Implementa\u00e7\u00e3o do material do curso sem altera\u00e7\u00f5es. Muito Obrigado, Dunossauro! fast_zero @me15degrees Tardei mas n\u00e3o falhei fast_zero @wilsonritt fast_zero @pedronora Curso sensacional fast_zero @Andersonmathema Projeto maravilhoso, espero melhorar muito com esse aprendizado e compartilhar o pouco que sei com a galera fast_zero @BrunoRimbanoJunior Muito Aprendizado, s\u00f3 tenho a agradecer ao professor Eduardo. curso-fastapi-do-zero @mferreiracosta Implementa\u00e7\u00e3o do material do curso sem altera\u00e7\u00f5es fast_zero @ViniNardelli Come\u00e7ando um pouco atrasado, mas aprendendo bastante fastapi_curso @juacy fast_zero @paullosergio Implementa\u00e7\u00e3o do material do curso sem altera\u00e7\u00f5es fast_zero @JordyAraujo Implementa\u00e7\u00e3o do material do curso sem altera\u00e7\u00f5es fast_zero @marfebr implementa\u00e7\u00e3o do material do curso sem altera\u00e7\u00f5es fast_zero @guilopes15 Implementa\u00e7\u00e3o do material do curso estudo_kaoz allandarus Estudos com Fast Api fast_zero @arturpeixoto Expandindo os conhecimentos de back-end com o FastAPI fast_zero @CarlosHenriquePSouza fastapi_duno_curso @LeandroDeJesus-S antes tarde do que mais tarde fastapi_zero_sync @Rafael-Inacio fast_zero @ArthurTZ Inicio do projeto fast_zero_sync @LuizPerciliano Come\u00e7ar \u00e9 importante, terminar \u00e9 melhor ainda! fastapi_learn @viniciusmilk Conhecimento sempre \u00e9 bem-vindo fastapi_curso @alves05 Curso excelente de FastAPI, o melhor que ta tendo! Vlw Eduardo! Fast_Zero_Hero @Lyarkh API desenvolvida com base no Curso de FastAPI trilha_fastAPI @vitoriarntrindade Obrigada por ser t\u00e3o bom pra comunidade Python! fast_zero @Tiago-Verde Obrigado Dunossauro fastapi_do_zero SantosTavares Este reposit\u00f3rio ser\u00e1 utilizado como base para novos projetos fast_zero_july_sync @marceloc4rdoso Esse reposit\u00f3rio \u00e9 destinado a estudos de FastAPI com @dunossauro Fast Notebook Matheus Um ambiente para anota\u00e7\u00f5es do curso. Fast_Api_Sync Braian N Ribeiro Fui descobri o curso quase no final das aulas online mas ainda vai da pra participar de umas 2 aulas valeu Duduzito Curso_FastApi regianemr Aprendendo a usar o Fast Api fast_zer0 xjhfsz Aulas s\u00edncronas fastapi_zero @sandenbergmelo Aprendendo como construir APIs em python com o FastAPI FastAPI @frbelotto Coment\u00e1rio fast_zero @Viniscorza Implementa\u00e7\u00e3o do material do curso - FastApi - Dunossauro FastAPI do Zero Hiroowtf Iniciando o Curso fast_zero @Pedro-hen Aprendendo FastAPI fast_api_zero andre-alves77 Obrigado Eduu fast_zero @williamslews Aulas sincronas fast_zero vitorTheDev Obrigado duno! curso-fastapi Tchez Parab\u00e9ns pelo curso! project_fastapi amandapolari Construindo API em Python utilizando FastAPI fast_zero vgrcontreras Implementa\u00e7\u00e3o do curso FastAPI do Zero! fast_zero @HigorTadeu Reposit\u00f3rio utilizado para os c\u00f3digos em Python do curso com FastAPI fastapi-do-zero @rodfersou Usando Nix no lugar de Pyenv; Scripts to rule then all no lugar do taskipy fast_zero @alexrodriguesdasilva Implementa\u00e7\u00e3o do material do curso sem altera\u00e7\u00f5es fastapi_ai_project @lesampaio Implementando o material do curso + aplica\u00e7\u00e3o de intelig\u00eancia artificial :shipit: fast_zero @rdgr1 Aprendendo FastAPI para Fins Educacionais :) fast_zero hsdanield Acompanhamento do curso fastapi-do-zero fast_zero_sync @amanmdest Bel\u00edssimo curso de FastAPI, me divertindo e aprendendo bastante curso_fast_zero @QuintelaCarvalho Aprendendo Sempre Mais com voc\u00ea Eduardo, Obrigado! fast_zero_sync seu @ Coment\u00e1rio fast_zero @huhero Colombiano \ud83c\udde8\ud83c\uddf4 aprendiendo FastApi fast_zero_classes @luismineo Setup inicial da aula de fastAPI fast_zero @Brugarassis :D fast_api_task @daniloheraclio \ud83c\udf89 fast_zero @DevSchoof Iniciando o curso fastapi-do-zero @heltonteixeira92 To infinity and beyond fast_zero seu @barscka Aprendo python com o melhor fastAPI_do_zero @viniciusCalcantara UaU! fast_zero @balaios Implementa\u00e7\u00e3o do material do curso sem altera\u00e7\u00f5es fastapizero @levyvix Aprendendo FastAPI do Zero python-curso-fastapi @fabiocasadossites Aprendendo FastAPI do Zero fast_zero @henriquesebastiao Muito grato por todo o curso e pela dedica\u00e7\u00e3o Edu. Sensacional! FastAPI_Zero JuniorD-Isael Primeira aula e eu j\u00e1 aprendi um zilh\u00e3o de coisas novas fast_zero_sync_v2 LuizPerciliano Opi! Refazendo aulas em novo projeto, vamos que vamos de muito aprendizado! do_know_fastapi @LucasDatilioCarderelli Correndo para assistir as aulas e fazer os desafios fast_zero @perylemke Aprendendo FastAPI fast_zero_sync vallejoguilherme fast_zero otonielnn Explorando FastApi fast-zero icaroHenrique Aprendendo FastAPI"},{"location":"quizes/aula_01/","title":"01 - Configurando o Ambiente de Desenvolvimento","text":"01 - Configurando o Ambiente de Desenvolvimento 01 - Qual a fun\u00e7\u00e3o do pyenv?Criar e gerenciar ambientes virtuais para bibliotecasInstalar vers\u00f5es diferentes do python no seu ambienteUma alternativa ao pipUma alternativa ao venvEnviar 02 - Qual a fun\u00e7\u00e3o do Poetry?Uma alternativa ao pipGerenciar um projeto pythonTem o mesmo prop\u00f3sito do pyenvEnviar 03 - O que faz o comando \"fastapi dev\"?Cria um ambiente de desenvolvimento para o FastAPIInicia o servidor de produ\u00e7\u00e3o do FastAPIInicia o Uvicorn em modo de desenvolvimentoInstala o FastAPIEnviar 04 - Ao que se refere o endere\u00e7o \"127.0.0.1\"?Ao endere\u00e7o de rede localCaminho de loopbackEndere\u00e7o do FastAPIEnviar 05 - A flag do poetry \"--group dev\" instala os pacotesde produ\u00e7\u00e3ode desenvolvimentode testesEnviar 06 - Qual a fun\u00e7\u00e3o do taskipy?Criar \"atalhos\" para comandos mais simplesFacilitar o manuseio das opera\u00e7\u00f5es de terminalInstalar ferramentas de desenvolvimentoGerenciar o ambiente virtualEnviar 07 - O pytest \u00e9:um linterum formatador de c\u00f3digoframework de testesEnviar 08 - Qual a ordem esperada de execu\u00e7\u00e3o de um teste?arrange, act, assertact, assert, arrangearrange, assert, actEnviar 09 - Dentro do nosso teste, qual a fun\u00e7\u00e3o da chamada na linha em destaque?
def test_root_deve_retornar_ok_e_ola_mundo():\n    client = TestClient(app)\n\n    response = client.get('/')\n\n    assert response.status_code == HTTPStatus.OK\n    assert response.json() == {'message': 'Ol\u00e1 Mundo!'}\n
assert arrange act Enviar 10 - Na cobertura de testes, o que quer dizer \"Stmts\"?Linha cobertas pelo testeLinhas n\u00e3o cobertas pelo testeLinhas de c\u00f3digoEnviar"},{"location":"quizes/aula_02/","title":"02 - Introdu\u00e7\u00e3o ao desenvolvimento WEB","text":""},{"location":"quizes/aula_02/#02-introducao-ao-desenvolvimento-web","title":"02 - Introdu\u00e7\u00e3o ao desenvolvimento WEB","text":"01 - Sobre o modelo cliente servidor, podemos afirmar que:O servidor \u00e9 respons\u00e1vel por servir dados aos clientesO cliente \u00e9 quem consome recursos do servidorA comunica\u00e7\u00e3o \u00e9 iniciada pelo servidorAs respostas s\u00e3o originadas pelo clienteEnviar 02 - Um servidor de aplica\u00e7\u00e3o como uvicorn:pode servir a aplica\u00e7\u00e3o em loopbackpode servir a aplica\u00e7\u00e3o em rede localdeve servir a aplica\u00e7\u00e3o somente em produ\u00e7\u00e3oEnviar 03 - O que significa URL?Uma Rota LocalRotas Locais UnidasLocalizador de Recursos LocaisLocalizador Uniforme de RecursosEnviar 04 - Qual dessas op\u00e7\u00f5es faz parte da URL:ProtocoloEndere\u00e7oHTMLCaminhoPortaVerboEnviar 05 - Qual desses campos n\u00e3o faz parte do cabe\u00e7alho HTTP?ServerContent-TypeCorpoAcceptEnviar 06 - O verbo PUT tem a fun\u00e7\u00e3o de:Solicitar um recursoDeletar um recursoAtualizar um recurso existenteTodas as anterioresEnviar 07 - Respostas com c\u00f3digos da classe 500, significamSucessoErro no servidorInformacionaisErro no clienteEnviar 08 - O c\u00f3digo 200 de resposta significa:FoundAcceptedCreatedOKEnviar 09 - O c\u00f3digo 422 de resposta significa:Not FoundOKUnprocessable EntityBad RequestForbiddenEnviar 10 - Qual a fun\u00e7\u00e3o do pydantic?Validar os dados que saem da APIValidar os dados que entram na APIDocumenta\u00e7\u00e3o autom\u00e1ticaEnviar"},{"location":"quizes/aula_03/","title":"03 - Estruturando o Projeto e Criando Rotas CRUD","text":"03 - Estruturando o Projeto e Criando Rotas CRUD 01 - O m\u00e9todo POST pode ser associado a qual letra da sigla CRUD?UDCREnviar 02 - Quando um recurso \u00e9 criado via POST, qual o Status deve ser retornado para sucesso?200201202301Enviar 03 - Quando um schema n\u00e3o \u00e9 respeitado pelo cliente, qual o status retornado?500404401422Enviar 04 - O FastAPI retorna qual status para quando o servidor n\u00e3o respeita o contrato? UNPROCESSABLE ENTITYI'M A TEAPOTINTERNAL SERVER ERRORNOT IMPLEMENTEDEnviar 05 - O que faz a seguinte fixture
@pytest.fixture\ndef client():\n    return TestClient(app)\n
Faz uma requisi\u00e7\u00e3o a aplica\u00e7\u00e3o Cria um cliente de teste reutiliz\u00e1vel Faz o teste automaticamente Enviar 06 - Qual c\u00f3digo de resposta deve ser enviado quando o recurso requerido n\u00e3o for encontrado?201404401500Enviar 07 - Sobre o relacionamento dos schemas, qual seria a resposta esperada pelo cliente em UserList?
class UserPublic(BaseModel):\n    username: str\n    email: str\n\n\nclass UserList(BaseModel):\n    users: list[UserPublic]\n
{\"users\": {\"username\": \"string\", \"email\": \"e@mail.com\"}} {\"users\": [{\"username\": \"string\", \"email\": \"e@mail.com\"}]} As duas est\u00e3o corretas Enviar 08 - HTTPException tem a fun\u00e7\u00e3o de:Criar um erro de servidorRetornar um erro ao clienteFazer uma valida\u00e7\u00e3o HTTPEnviar 09 - 'users/{user_id}' permite:Parametrizar a URLPedir por um recurso com id espec\u00edficoAumentar a flexibilidade dos endpointsEnviar 10 - Qual a fun\u00e7\u00e3o desse bloco de c\u00f3digo nos endpoints de PUT E DELETE?
if user_id > len(database) or user_id < 1:\n    raise HTTPException(\n        status_code=HTTPStatus.NOT_FOUND, detail='User not found'\n    )\n
Garantir que s\u00f3 sejam chamados id v\u00e1lidos Montar um novo schema do pydantic Criar um erro de servidor Enviar"},{"location":"quizes/aula_04/","title":"04 - Configurando o Banco de Dados e Gerenciando Migra\u00e7\u00f5es com Alembic","text":"04 - Configurando o Banco de Dados e Gerenciando Migra\u00e7\u00f5es com Alembic 01 - Qual a fun\u00e7\u00e3o do sqlalchemy em nosso projeto?Gerenciar a conex\u00e3o com o banco de dadosRepresentar o modelo dos dados como objetosFazer busca de dados no bancoTodas as alternativasEnviar 02 - O Registry do sqlalchemy tem a fun\u00e7\u00e3o de:Criar um schema de valida\u00e7\u00e3o da APICriar um objeto que representa a tabela no banco de dadosCriar um registro no banco de dadosEnviar 03 - Qual a fun\u00e7\u00e3o do objeto Mapperexecutar a fun\u00e7\u00e3o map do python no banco de dadosCriar uma rela\u00e7\u00e3o entre o tipo de dados do python e o da tabela do bancoDizer qual o tipo de dado que ter\u00e1 no banco de dadosFazer uma convers\u00e3o para tipos do pythonEnviar 04 - O que faz o a fun\u00e7\u00e3o mapped_column?Indicar valores padr\u00f5es para as colunasCriar indicadores de SQL no objetoAdiciona restri\u00e7\u00f5es referentes a coluna no banco de dadosTodas as anterioresEnviar 05 - Qual a fun\u00e7\u00e3o do mapped_column no seguinte c\u00f3digo:
quiz: Mapped[str] = mapped_column(unique=True)\n
O valor de quiz deve ser \u00fanico na coluna Este campo \u00e9 o \u00fanico da tabela A tabela s\u00f3 tem um campo S\u00f3 \u00e9 poss\u00edvel inserir um valor \u00fanico nesse campo Enviar 06 - O que significa init=False no mapeamento?Diz que a coluna n\u00e3o deve ser iniciada no bancoToma a responsabilidade do preenchimento do campo para o SQLAlchemyDiz que existe um valor padr\u00e3o na colunaEnviar 07 - O m\u00e9todo \"scalar\" da session tem o objetivo de:
session.scalar(select(User).where(User.username == 'Quiz'))\n
Executar uma query no banco de dados Retornar somente um resultado do banco Converter o resultado da query em um objeto do modelo Todas as alternativas est\u00e3o corretas Enviar 08 - A fun\u00e7\u00e3o \"select\" tem a objetivo de:
session.scalar(select(User).where(User.username == 'Quiz'))\n
Executar uma busca no banco de dados Selecionar objetos 'User' no projeto Montar uma query de SQL Criar um filtro de busca Enviar 09 - Qual o objetivo do arquivo .env?Isolar vari\u00e1veis do ambiente do c\u00f3digo fonteCriar vari\u00e1veis no ambiente virtualCriar vari\u00e1veis globais no projetoEnviar 10 - As migra\u00e7\u00f5es t\u00eam a fun\u00e7\u00e3o de:Refletir as tabelas do banco de dados no ORMCriar tabelas no banco de dadosRefletir as classes do ORM no banco de dadosCriar um banco de dadosEnviar"},{"location":"quizes/aula_05/","title":"05 - Integrando Banco de Dados a API","text":"05 - Integrando Banco de Dados a API 01 - Qual a fun\u00e7\u00e3o de adicionarmos a fun\u00e7\u00e3o \"Depends\" no seguinte c\u00f3digo:
@app.post('/users/', status_code=HTTPStatus.CREATED, response_model=UserPublic)\ndef create_user(\n    user: UserSchema,\n    session: Session = Depends(get_session)\n):\n
Indicar que a fun\u00e7\u00e3o depende de algo Documentar no schema os dados que s\u00e3o requeridos para chamar o endpoint Executar a fun\u00e7\u00e3o 'get_session' e passar seu resultado para fun\u00e7\u00e3o Indicar que a fun\u00e7\u00e3o 'get_session' tem que ser executada antes da 'create_user' Enviar 02 - Sobre a inje\u00e7\u00e3o na fixture podemos afirmar que:
with TestClient(app) as client:\n    app.dependency_overrides[get_session] = get_session_override\n    yield client\n\napp.dependency_overrides.clear()\n
Ela remover\u00e1 depend\u00eancia do c\u00f3digo durante a execu\u00e7\u00e3o do teste Ser\u00e1 feita a sobreescrita de uma dependencia por outra durante o teste A depend\u00eancia 'get_session' ser\u00e1 for\u00e7ada durante o teste Enviar 03 - Essa fixture no banco de dados garante que:
@pytest.fixture\ndef session():\n    engine = create_engine(\n        'sqlite:///:memory:',\n        connect_args={'check_same_thread': False},\n        poolclass=StaticPool,\n    )\n
O banco de dados estar\u00e1 em mem\u00f3ria N\u00e3o ser\u00e1 executada a verifica\u00e7\u00e3o entre a thread do banco e do teste Ser\u00e1 usado um pool de tamanho fixo Criar\u00e1 uma conex\u00e3o com o banco de dados para usar nos testes todas as alternativas anteriores Enviar 04 - Para que o cliente requisite o campo \"limit\" ele deve usar a url:
@app.get('/users/', response_model=UserList)\ndef read_users(\n    skip: int = 0, limit: int = 100, session: Session = Depends(get_session)\n):\n
/users/?limit=10 /users/limit/10 /users/limit=10? /users/&limit=10 Enviar 05 - Quais os padr\u00f5es de projeto implementados pela Session?Reposit\u00f3rioUnidade de trabalhoCompositeProxyEnviar 06 - O que faz o m\u00e9todo session.commit()?Faz um commit no gitPersiste os dados no banco de dadosExecuta as transa\u00e7\u00f5es na sess\u00e3oAbre uma conex\u00e3o com o banco de dadosEnviar 07 - O que faz o m\u00e9todo session.refresh(obj)?Atualiza a conex\u00e3o com o banco de dadosAtualiza dos dados da sess\u00e3oSincroniza o objeto do ORM com o banco de dadosSincroniza a sess\u00e3o com o banco de dadosEnviar 08 - O que o \"|\" siginifica na query?
session.scalar(\n    select(User).where(\n        (User.username == user.username) | (User.email == user.email)\n    )\n)\n
user.username 'E' user.email user.username 'SEM' user.email user.username 'OU' user.email user.username 'COM' user.email Enviar 09 - Quando usamos o m\u00e9todo 'model_validate' de um schema do Pydantic estamos:Validando um JSON com o modeloValidando um request com o modeloConverte um objeto em um schemaCoverte um objeto em JSONEnviar 10 - Quando usamos 'model_config' em um schema do Pydantic estamos:Alterando o comportamento de 'model_validate'Adicionando mais um campo de valida\u00e7\u00e3oAlterando a estrutura do modeloEnviar"},{"location":"quizes/aula_06/","title":"06 - Autentica\u00e7\u00e3o e Autoriza\u00e7\u00e3o com JWT","text":"06 - Autentica\u00e7\u00e3o e Autoriza\u00e7\u00e3o com JWT 01 - Qual a fun\u00e7\u00e3o da 'pwdlib' em nosso projeto?Criar um hash da senhaVerificar se o texto limpo bate com o texto sujoSalvar as senhas em texto limpoFazer valida\u00e7\u00e3o das senhasEnviar 02 - Qual a necessidade de adicionar a linha em destaque no endpoint de PUT?
@app.put('/users/{user_id}', response_model=UserPublic)\n# ...\n    db_user.username = user.username\n    db_user.password = get_password_hash(user.password)\n    db_user.email = user.email\n
Validar a senha no momento do update Criar o hash da senha durante a atualiza\u00e7\u00e3o Pegar a senha antiga durante o update Salvar a senha de forma limpa no banco de dados Enviar 03 - Qual o prop\u00f3sito da autentica\u00e7\u00e3o?Fornecer um mecanismo de tokensValidar que o cliente \u00e9 quem diz serGarantir que s\u00f3 pessoas autorizadas possam executar a\u00e7\u00f5esEnviar 04 - Qual a fun\u00e7\u00e3o do endpoint '/token'?Gerenciar a autoriza\u00e7\u00e3o do clienteFazer a autentica\u00e7\u00e3o do clienteRenovar o token JWTTodas as respostas est\u00e3o corretasEnviar 05 - O 'OAuth2PasswordRequestForm' fornece:Um formul\u00e1rio de cadastroUm formul\u00e1rio de autentica\u00e7\u00e3oUm formul\u00e1rio de autoriza\u00e7\u00e3oUm formul\u00e1rio de altera\u00e7\u00e3o de registroEnviar 06 - Qual a fun\u00e7\u00e3o do token JWT?Fornecer informa\u00e7\u00f5es sobre o cliente para o servidorGerenciar o tempo de validade do tokenCarregar dados sobre autoriza\u00e7\u00e3oTodas as respostas est\u00e3o corretasEnviar 07 - Qual o objetivo da claim 'sub'?Guardar o tempo de validade do tokenIdentificar o servidor que gerou o tokenIdentificar qual cliente gerou o tokenIdentificar o email do clienteEnviar 08 - Qual a fun\u00e7\u00e3o da 'secret_key'?Usar como base para criptografar a senha do cliente com Argon2Usar como base para gera\u00e7\u00e3o do HTTPSUsar como base para assinar o Token com HS256Enviar 09 - Qual o objetivo da fun\u00e7\u00e3o 'current_user'?Gerenciar a autentica\u00e7\u00e3o dos clientesValidar o token JWTGerenciar a autoriza\u00e7\u00e3o dos endpointsSaber que \u00e9 o usu\u00e1rio logadoEnviar"},{"location":"quizes/aula_07/","title":"07 - Refatorando a Estrutura do Projeto","text":"07 - Refatorando a Estrutura do Projeto 01 - Quais s\u00e3o as fun\u00e7\u00f5es do \"Router\" do FastAPICriar uma \"sub-aplica\u00e7\u00e3o\"Isolar endpoints por dom\u00ednioFacilitar a manuten\u00e7\u00e3o do c\u00f3digoMelhorando o desempenho da aplica\u00e7\u00e3oEnviar 02 - Sobre o par\u00e2metro \"prefix\" do router, podemos afirmar que:Adicionar os endpoints no roteadorFazer as chamadas unificadas do enpoint Padronizar um prefixo para N endpointsEnviar 03 - Qual a fun\u00e7\u00e3o do par\u00e2metro 'tag' nos routers?Dizer quais par\u00e2metros devem ser passados ao endpointsColocar um prefixo nos endpointsAgrupar os endpoins do mesmo dom\u00ednio na documenta\u00e7\u00e3oAdicionar cores diferentes no swaggerEnviar 04 - Qual a fun\u00e7\u00e3o do tipo \"Annotated\" no FastAPI!Reduzir o tamanho das fun\u00e7\u00f5esReutilizar anota\u00e7\u00f5es em N endpointsAtribuir metadados aos tiposTodas as alternativasEnviar 05 - O que o \"Annotated\" faz nesse c\u00f3digo?
@app.put('/users/{user_id}', response_model=UserPublic)\ndef endpoint(session: Annotated[Session, Depends(get_session)])\n
Diz que o par\u00e2metro 'session' \u00e9 do tipo 'Session' e depende de 'get_session' Diz que o par\u00e2metro 'session' \u00e9 do tipo 'Annotated' Faz a troca de 'session' por 'Session' Enviar"},{"location":"quizes/aula_08/","title":"08 - Tornando o sistema de autentica\u00e7\u00e3o robusto","text":"08 - Tornando o sistema de autentica\u00e7\u00e3o robusto 01 - Sobre o Factory-boy. O que siginifica a classe Meta?
class UserFactory(factory.Factory):\n    class Meta:\n        model = User\n
Diz que ser\u00e1 usada uma metaclasse Explica ao Factory qual objeto ele deve se basear Extente a classe Factory com os par\u00e2metros de Meta Enviar 02 - Ainda sobre o Factory-boy. O que siginifica \"factory.Sequence\"?Criar\u00e1 uma sequ\u00eancia de MetasAdicionar\u00e1 +1 em cada objeto criadoMonta uma sequ\u00eancia de objetosCria um novo objeto do factoryEnviar 03 - Ainda sobre o Factory-boy. O que siginifica \"factory.LazyAttribute\"?Diz que o atributo ser\u00e1 criado em tempo de execu\u00e7\u00e3oDiz que o atributo usar\u00e1 outros atributos para ser inicializadoUsa outros campos para ser compostoCria um atributo independ\u00eanteEnviar 04 - O que faz o gerenciador de contexto \"freeze_time\"?
with freeze_time('2023-07-14 12:00:00'):\n    response = client.post(\n        '/auth/token',\n        data={'username': user.email, 'password': user.clean_password},\n    )\n
Pausa o tempo nas instru\u00e7\u00f5es dentro do 'with' Congela o tempo da fun\u00e7\u00e3o toda Muda a hora do computador para um bloco Enviar 05 - O que levanta o erro \"ExpiredSignatureError\"?Quando deu erro no valor de 'exp'Quando n\u00e3o deu certo avaliar a claim de 'exp'Quando a claim de 'exp' tem um tempo expiradoEnviar"},{"location":"quizes/aula_09/","title":"09 - Criando Rotas CRUD para Gerenciamento de Tarefas em FastAPI","text":"09 - Criando Rotas CRUD para Gerenciamento de Tarefas em FastAPI 01 - Qual o papel da classe 'TodoState' em nosso c\u00f3digo?Fornece m\u00e9todos para gerar IDs sequenciais para tarefas.Armazena o hist\u00f3rico de altera\u00e7\u00f5es das tarefas ao longo do tempo.Permite a cria\u00e7\u00e3o de tarefas com diferentes n\u00edveis de prioridade.Tipo com valores nomeados e constantes para representar estados de tarefas.Enviar A classe `TodoState` define estados de tarefas (rascunho, pendente, etc.) com nomes claros, facilitando o c\u00f3digo e garantindo seguran\u00e7a e agilidade na manuten\u00e7\u00e3o. 02 - Qual o significado da rela\u00e7\u00e3o `user: Mapped[User] = relationship(...)` em nosso modelo?Define conex\u00e3o entre usu\u00e1rios e tarefas (N:N) com lista inicializada e acesso bidirecional.Estabelece rela\u00e7\u00e3o entre usu\u00e1rios e tarefas (1:N) com lista n\u00e3o inicializada e acesso bidirecional.Cria v\u00ednculo entre usu\u00e1rios e tarefas (1:1) com lista n\u00e3o inicializada e acesso unidirecional.Estabelece rela\u00e7\u00e3o entre usu\u00e1rios e tarefas (1:N) com lista inicializada e acesso unidirecional.Enviar A rela\u00e7\u00e3o `user: Mapped[User] = relationship(...)` define uma conex\u00e3o 1:N entre usu\u00e1rios e tarefas, permitindo que um usu\u00e1rio tenha v\u00e1rias tarefas e cada tarefa esteja ligada a um \u00fanico usu\u00e1rio. A lista de tarefas n\u00e3o \u00e9 inicializada automaticamente e as entidades podem se acessar mutuamente. 03 - Qual o significado do par\u00e2metro de consulta `state: str | None = None` no endpoint de busca?Permite filtrar resultados por valor de string obrigat\u00f3rio ('state'), n\u00e3o aceitando None.Filtra resultados por estado ('state'), com valor padr\u00e3o 'pendente' se n\u00e3o especificado.Habilita filtro por 'state' (string ou None), com valor padr\u00e3o None se n\u00e3o especificado.Cria um par\u00e2metro opcional 'state' que recebe floats para filtrar resultados.Enviar O par\u00e2metro `state: str | None = None` no FastAPI permite filtrar resultados por um valor de string opcional ('state'), que pode ser None por padr\u00e3o. 04 - Qual a fun\u00e7\u00e3o do `FuzzyChoice` no Factory Boy?Gera dados de teste aleat\u00f3rios e realistas, facilitando a cria\u00e7\u00e3o de testes de unidade robustos.Gera valores aleat\u00f3rios para cada atributo de um objeto de teste, facilitando a cria\u00e7\u00e3o de testes.Cria objetos de teste com valores predefinidos a partir de um conjunto de op\u00e7\u00f5es.Permite a gera\u00e7\u00e3o de dados de teste aleat\u00f3rios e realistas para diferentes tipos de dados.Enviar O `FuzzyChoice` do Factory Boy gera valores aleat\u00f3rios a partir de um conjunto predefinido, criando objetos de teste com dados realistas e facilitando testes de unidade robustos. 05 - Por qual raz\u00e3o usamos `# noqa` no endpoint `list_todos`:Para dizer aos QAs que esse c\u00f3digo n\u00e3o \u00e9 pra eles.Para dizer que esse c\u00f3digo n\u00e3o ser\u00e1 coberto por testes.Para remover a checagem no linter na express\u00e3o.Enviar 06 - Qual a fun\u00e7\u00e3o do `session.bulk_save_objects` nos testes de todo?Inserir uma lista de objetos na sessionSalvar diversos objetos de uma vez no banco de dadosEnviar 07 - Qual a fun\u00e7\u00e3o do `exclude_unset=True` no c\u00f3digo abaixo?
@router.patch('/{todo_id}', response_model=TodoPublic)\ndef patch_todo(\n    todo_id: int, session: Session, user: CurrentUser, todo: TodoUpdate\n):\n    # ...\n    for key, value in todo.model_dump(exclude_unset=True).items():\n        setattr(db_todo, key, value)\n
Exclui os valores que n\u00e3o fazem parte do schema Exclui os valores que n\u00e3o foram passados para o schema Exclui os valores que s\u00e3o None no schema Enviar"},{"location":"quizes/aula_10/","title":"10 - Dockerizando a nossa aplica\u00e7\u00e3o e introduzindo o PostgreSQL","text":""},{"location":"quizes/aula_10/#10-dockerizando-a-nossa-aplicacao-e-introduzindo-o-postgresql","title":"10 - Dockerizando a nossa aplica\u00e7\u00e3o e introduzindo o PostgreSQL","text":"01 - Qual a fun\u00e7\u00e3o do arquivo `compose.yaml`?Subir a aplica\u00e7\u00e3o de forma simplesEspecificar os servi\u00e7os e como eles se relacionamSubstituir o DockerfileCriar uma container dockerEnviar 02 - Qual instru\u00e7\u00e3o do Dockerfile o `entrypoint` substitui?O comando de execu\u00e7\u00e3o (CMD)A defini\u00e7\u00e3o da imagem base (FROM)A exposi\u00e7\u00e3o das portas (EXPOSE)Enviar 03 - O que quer dizer escopo nas fixtures?Em quais testes elas v\u00e3o atuarSe um m\u00f3dulo pode usar aquela fixtureQual a dura\u00e7\u00e3o da fixtureCapturar as vari\u00e1veis de ambienteEnviar 04 - Por que usamos o escopo de \"session\" na fixture?Pra dizer que ela vai substituir a fixture de sessionCriar uma sess\u00e3o do cliente com o banco de dadosDizer que a fixture tem a dura\u00e7\u00e3o de um testeDizer que a fixture ser\u00e1 executada uma \u00fanica vez durante os testesEnviar 05 - Para que serve o volume no docker?Para armazenar as imagens geradasPara adicionar um banco de dadosPara armazenar o cache do dockerPara persistir arquivos na m\u00e1quina hostEnviar 06 - O que faz a flag `-it` no CLI do docker?Conecta o container na internetRoda o container no modo interativoConfigura a rede do dockerPassa as vari\u00e1veis de ambienteEnviar 07 - Por que precisamos usar o TestContainers no projeto?Para executar os testes dentro de containersPara testar os containers da aplica\u00e7\u00e3oPara criar imagens durante o testePara iniciar containers durante o testeEnviar"},{"location":"quizes/aula_11/","title":"11 - Automatizando os testes com Integra\u00e7\u00e3o Cont\u00ednua (CI)","text":""},{"location":"quizes/aula_11/#11-automatizando-os-testes-com-integracao-continua-ci","title":"11 - Automatizando os testes com Integra\u00e7\u00e3o Cont\u00ednua (CI)","text":"01 - Qual a fun\u00e7\u00e3o da integra\u00e7\u00e3o cont\u00ednua?Proibir que c\u00f3digo que n\u00e3o funciona seja commitadoVerificar se a integra\u00e7\u00e3o das altera\u00e7\u00f5es foi bem sucedidaImpedir que pessoas de fora integrem c\u00f3digo em nosso reposit\u00f3rioIntegrar novos commits ao reposit\u00f3rioEnviar 02 - O que \u00e9 o Github Actions?Uma aplica\u00e7\u00e3o que executa os testes localmenteUm test runner como o pytestForma de integrar o github com outras aplica\u00e7\u00f5esUm servi\u00e7o do github para CIEnviar 03 - O que \u00e9 um workflow de CI?Uma lista de passos que o CI deve executarUma automa\u00e7\u00e3o executada sempre c\u00f3digo \u00e9 adicionado ao resposit\u00f3rioUma forma de versionar software como o gitPassos que ser\u00e3o executados antes do commitEnviar 04 - Quando o nosso trigger de CI \u00e9 ativado?Sempre que fazemos um pushSempre que criamos um pull requestSempre que um commit \u00e9 feitoSempre que uma issue \u00e9 abertaEnviar 05 - Nos steps, o que quer dizer \"uses\"?Diz que vamos usar uma action prontaDiz que vamos executar uma instru\u00e7\u00e3o de shellQue vamos fazer a instala\u00e7\u00e3o de um componente no workflowFazer checkout do c\u00f3digo do reposit\u00f3rioEnviar 06 - Nos steps, o que quer dizer \"run\"?Que vamos usar uma action pronta do githubServe para dizer que vamos usar um passoDefinir uma vari\u00e1vel de ambienteDiz que vamos executar uma instru\u00e7\u00e3o de shellEnviar 07 - Qual a fun\u00e7\u00e3o das \"secrets\" no arquivo yaml?Criar vari\u00e1veis de ambienteN\u00e3o expor dados sens\u00edveis no arquivo de ciSubstituir vari\u00e1veis \u200b\u200bcom valores din\u00e2micosOrganizar o c\u00f3digo YAMLEnviar"},{"location":"quizes/aula_12/","title":"12 - Fazendo deploy no Fly.io","text":""},{"location":"quizes/aula_12/#12-fazendo-deploy-no-flyio","title":"12 - Fazendo deploy no Fly.io","text":"01 - O que \u00e9 fazer \"deploy\"?Colocar a aplica\u00e7\u00e3o em produ\u00e7\u00e3oExecutar os testes da aplica\u00e7\u00e3oExecutar a aplica\u00e7\u00e3o localmenteFazer o processo de integra\u00e7\u00e3o cont\u00ednuaEnviar 02 - O quer dizer \"PaaS\"?Software como servi\u00e7oUm local para subir a aplica\u00e7\u00e3oSoftwares como o githubPlataforma como servi\u00e7oEnviar 03 - O que \u00e9 o Fly.io?Uma plataforma de c\u00f3digoUma plataforma de versionamentoUma plataforma de CloudUma plataforma de integra\u00e7\u00e3o cont\u00ednuaEnviar 04 - Para que usamos o \"flyctl\"?Para fazer o login no flyPara nos comunicarmos com o fly via terminalPara fazer deploy da aplica\u00e7\u00e3oPara fazer o build do containerEnviar"}]} \ No newline at end of file +{"config":{"lang":["pt"],"separator":"[\\s\\-]+","pipeline":["stopWordFilter"]},"docs":[{"location":"","title":"FastAPI do Zero!","text":""},{"location":"#fastapi-do-zero","title":"FastAPI do ZERO","text":"Caso prefira ver a apresenta\u00e7\u00e3o do curso em v\u00eddeo

Esse aula ainda n\u00e3o est\u00e1 dispon\u00edvel em formato de v\u00eddeo, somente em texto ou live!

Aula Slides

Ol\u00e1, boas vindas ao curso de FastAPI!

A nossa inten\u00e7\u00e3o neste curso \u00e9 facilitar o aprendizado no desenvolvimento de APIs usando o FastAPI. Vamos explorar como integrar bancos de dados, criar testes e um sistema b\u00e1sico de autentica\u00e7\u00e3o com JWT. Tudo isso para oferecer uma boa base para quem quer trabalhar com desenvolvimento web com Python. A ideia desse curso \u00e9 apresentar os conceitos de forma pr\u00e1tica, construindo um projeto do zero e indo at\u00e9 a sua fase de produ\u00e7\u00e3o.

"},{"location":"#o-que-e-fastapi","title":"O que \u00e9 FastAPI?","text":"

FastAPI \u00e9 um framework Python moderno, projetado para simplicidade, velocidade e efici\u00eancia. A combina\u00e7\u00e3o de diversas funcionalidades modernas do Python como anota\u00e7\u00f5es de tipo e suporte a concorr\u00eancia, facilitando o desenvolvimento de APIs.

"},{"location":"#sobre-o-curso","title":"Sobre o curso","text":"

Este curso foi desenvolvido para oferecer uma experi\u00eancia pr\u00e1tica no uso do FastAPI, uma das ferramentas mais modernas para constru\u00e7\u00e3o de APIs. Ao longo do curso, o objetivo \u00e9 que voc\u00ea obtenha uma compreens\u00e3o das funcionalidades do FastAPI e de boas pr\u00e1ticas associadas a ele.

O projeto central do curso ser\u00e1 a constru\u00e7\u00e3o de um gerenciador de tarefas (uma lista de tarefas), come\u00e7ando do zero. Esse projeto incluir\u00e1 a implementa\u00e7\u00e3o da autentica\u00e7\u00e3o do usu\u00e1rio e das opera\u00e7\u00f5es CRUD completas.

Para a constru\u00e7\u00e3o do projeto, ser\u00e3o utilizadas as vers\u00f5es mais recentes das ferramentas, dispon\u00edveis em 2024, como a vers\u00e3o 0.114 do FastAPI, a vers\u00e3o 2.0 do Pydantic, a vers\u00e3o 2.0 do SQLAlchemy ORM, al\u00e9m do Python 3.11/3.12 e do Alembic para gerenciamento de migra\u00e7\u00f5es.

Al\u00e9m da constru\u00e7\u00e3o do projeto, o curso tamb\u00e9m incluir\u00e1 a pr\u00e1tica de testes, utilizando o pytest. Essa abordagem planeja garantir que as APIs desenvolvidas sejam n\u00e3o apenas funcionais, mas tamb\u00e9m robustas e confi\u00e1veis.

"},{"location":"#o-que-voce-vai-aprender","title":"O que voc\u00ea vai aprender?","text":"

Aqui est\u00e1 uma vis\u00e3o geral dos t\u00f3picos que abordaremos neste curso:

  1. Configura\u00e7\u00e3o do ambiente de desenvolvimento para FastAPI: come\u00e7aremos do absoluto zero, criando e configurando nosso ambiente de desenvolvimento.

  2. Primeiros Passos com FastAPI e Testes: ap\u00f3s configurar o ambiente, mergulharemos na estrutura b\u00e1sica de um projeto FastAPI e faremos uma introdu\u00e7\u00e3o detalhada ao Test Driven Development (TDD).

  3. Modelagem de Dados com Pydantic e SQLAlchemy: aprenderemos a criar e manipular modelos de dados utilizando Pydantic e SQLAlchemy, dois recursos que levam a efici\u00eancia do FastAPI a outro n\u00edvel.

  4. Autentica\u00e7\u00e3o e Autoriza\u00e7\u00e3o em FastAPI: construiremos um sistema de autentica\u00e7\u00e3o completo, para proteger nossas rotas e garantir que apenas usu\u00e1rios autenticados tenham acesso a certos dados.

  5. Testando sua Aplica\u00e7\u00e3o FastAPI: faremos uma introdu\u00e7\u00e3o detalhada aos testes de aplica\u00e7\u00e3o FastAPI, utilizando as bibliotecas pytest e coverage. Al\u00e9m de execut\u00e1-los em um pipeline de integra\u00e7\u00e3o cont\u00ednua com github actions.

  6. Dockerizando e Fazendo Deploy de sua Aplica\u00e7\u00e3o FastAPI: por fim, aprenderemos como \"dockerizar\" nossa aplica\u00e7\u00e3o FastAPI e fazer seu deploy utilizando Fly.io.

"},{"location":"#esse-curso-e-gratuito","title":"\ud83d\udcb0 Esse curso \u00e9 gratuito?","text":"

SIM! Esse curso foi todo desenvolvido de forma aberta e com a ajuda financeira de pessoas incr\u00edveis. Caso voc\u00ea sinta vontade de contribuir, voc\u00ea pode me pagar um caf\u00e9 por pix (pix.dunossauro@gmail.com) ou apoiar a campanha recorrente de financiamento coletivo da live de python que \u00e9 o que paga as contas aqui de casa.

"},{"location":"#onde-o-curso-sera-disponibilizado","title":"Onde o curso ser\u00e1 disponibilizado?","text":"

Esse material ser\u00e1 disponibilizado de tr\u00eas formas diferentes:

  1. Em livro texto: todo o material est\u00e1 dispon\u00edvel nessa p\u00e1gina;

  2. Em aulas s\u00edncronas ao vivo: para quem prefere o compromisso de acompanhar em grupo. Datas j\u00e1 dispon\u00edveis;

    Playlist das Aulas s\u00edncronas (Ao vivo):

  3. Em formato de v\u00eddeo (ass\u00edncronas): todas as aulas ser\u00e3o disponibilizadas em formato de v\u00eddeo em meu canal do YouTube para quem prefere assistir ao ler. (V\u00eddeos ainda n\u00e3o dispon\u00edveis)

"},{"location":"#pre-requisitos","title":"Pr\u00e9-requisitos","text":"

Para aproveitar ao m\u00e1ximo este curso, \u00e9 recomendado que voc\u00ea j\u00e1 tenha algum conhecimento pr\u00e9vio em python, se pudesse listar o que considero importante para n\u00e3o se perder, os t\u00f3picos em python importantes s\u00e3o:

As refer\u00eancias servem como base caso voc\u00ea ainda n\u00e3o tenha estudado esses assuntos

Alguns outros t\u00f3picos n\u00e3o relativos a python tamb\u00e9m ser\u00e3o abordados. Ent\u00e3o \u00e9 interessante que voc\u00ea tenha algum entendimento b\u00e1sico sobre:

Caso esteja iniciando seus estudos em Python!

Caso voc\u00ea ainda n\u00e3o se sinta uma pessoa preparada, ou caiu aqui sem saber exatamente o que esperar. Temos um pequeno curso introdut\u00f3rio. Destinado aos primeiros passos com python.

Link direto

Tamb\u00e9m temos uma live focada em dicas para iniciar os estudos em python

Link direto

Ou ent\u00e3o a leitura do livro Pense em python

"},{"location":"#aulas","title":"Aulas","text":"
  1. Configurando o Ambiente de Desenvolvimento
  2. Introdu\u00e7\u00e3o ao desenvolvimento WEB
  3. Estruturando seu Projeto e Criando Rotas CRUD
  4. Configurando Banco de Dados e Gerenciando Migra\u00e7\u00f5es com Alembic
  5. Integrando Banco de Dados a API
  6. Autentica\u00e7\u00e3o e Autoriza\u00e7\u00e3o
  7. Refatorando a Estrutura do Projeto
  8. Tornando o sistema de autentica\u00e7\u00e3o robusto
  9. Criando Rotas CRUD para Tarefas
  10. Dockerizando a nossa aplica\u00e7\u00e3o e introduzindo o PostgreSQL
  11. Automatizando os testes com integra\u00e7\u00e3o cont\u00ednua
  12. Fazendo o deploy no Fly.io
  13. Despedida e pr\u00f3ximos passos

Ap\u00f3s todas as aulas, se voc\u00ea sentir que ainda quer evoluir mais e testar seus conhecimentos, temos um projeto final para avaliar o quanto voc\u00ea aprendeu.

"},{"location":"#quem-vai-ministrar-essas-aulas","title":"\ud83e\udd96 Quem vai ministrar essas aulas?","text":"

Prazer! Eu me chamo Eduardo. Mas as pessoas me conhecem na internet como @dunossauro.

Sou um programador Python muito empolgado e curioso. Toco um projeto pessoal chamado Live de Python h\u00e1 quase 7 anos. Onde conversamos sobre tudo e mais um pouco quando o assunto \u00e9 Python.

Esse projeto que estamos desenvolvendo \u00e9 um peda\u00e7o, um projeto, de um grande curso de FastAPI que estou montando. Espero que voc\u00ea se divirta ao m\u00e1ximo com a parte pr\u00e1tica enquanto escrevo em mais detalhes todo o potencial te\u00f3rico que lan\u00e7arei no futuro!

Caso queira saber mais sobre esse projeto completo.

"},{"location":"#revisao-e-contribuicoes","title":"Revis\u00e3o e contribui\u00e7\u00f5es","text":"

Esse material contou com a revis\u00e3o e contribui\u00e7\u00f5es inestim\u00e1veis de pessoas incr\u00edveis:

@adorilson, @aguynaldo, @alphabraga, @andrespp, @azmovi, @bugelseif, @gabrielhardcore, @gbpagano, @henriqueccda, @henriquesebastiao, @ig0r-ferreira, @itsGab, @ivansantiagojr, @jlplautz, @jonathanscheibel, @julioformiga, @lbmendes, @lucasmpavelski, @lucianoratamero, @matheusalmeida28, @me15degrees, @mmaachado, @rennerocha, @ricardo-emanuel01, @rodbv, @rodrigosbarretos, @taconi, @vcwild, @williangl, @vdionysio

Muito obrigado!

"},{"location":"#licenca","title":"\ud83d\udcd6 Licen\u00e7a","text":"

Todo esse curso foi escrito e produzido por Eduardo Mendes (@dunossauro).

Todo esse material \u00e9 gratuito e est\u00e1 sob licen\u00e7a Creative Commons BY-NC-SA. O que significa que:

Pontos de aten\u00e7\u00e3o:

"},{"location":"#ferramentas-necessarias-para-acompanhar-o-curso","title":"\ud83e\uddf0 Ferramentas necess\u00e1rias para acompanhar o curso","text":"
  1. Um editor de texto ou IDE de sua escolha. Estou usando o GNU/Emacs enquanto escrevo as aulas;
  2. Um terminal. Todos os exemplos do curso s\u00e3o executados e explicados no terminal. Voc\u00ea pode usar o que se sentir mais a vontade e for compat\u00edvel com seu sistema operacional;
  3. Ter o interpretador Python instalado em uma vers\u00e3o igual ou superior a 3.11
  4. Uma conta no Github: para podermos testar com Github Actions;
  5. Uma conta no Fly.io: ferramenta que usaremos para fazer deploy.
"},{"location":"#ferramentas-de-apoio","title":"\ud83d\udd27 Ferramentas de apoio","text":"

Toda essa p\u00e1gina foi feita usando as seguintes bibliotecas:

Para os slides:

"},{"location":"#repositorio","title":"\ud83d\udcc1 Reposit\u00f3rio","text":"

O versionamento de tudo est\u00e1 sendo feito no reposit\u00f3rio do curso Github

"},{"location":"#deploy","title":"\ud83d\ude80 Deploy","text":"

Os deploys das p\u00e1ginas est\u00e1ticas geradas pelo MkDocs est\u00e3o sendo feitos no Netlify

"},{"location":"#conclusao","title":"Conclus\u00e3o","text":"

Neste curso, a inten\u00e7\u00e3o \u00e9 fornecer uma compreens\u00e3o completa do framework FastAPI, utilizando-o para construir uma aplica\u00e7\u00e3o de gerenciamento de tarefas. O aprendizado ser\u00e1 focado na pr\u00e1tica, e cada conceito ser\u00e1 acompanhado por exemplos e exerc\u00edcios relevantes.

A jornada come\u00e7ar\u00e1 com a configura\u00e7\u00e3o do ambiente de desenvolvimento e introdu\u00e7\u00e3o ao FastAPI. Ao longo das aulas, abordaremos t\u00f3picos como autentica\u00e7\u00e3o, opera\u00e7\u00f5es CRUD, testes com pytest e deploy. A \u00eanfase ser\u00e1 colocada na aplica\u00e7\u00e3o de boas pr\u00e1ticas e no entendimento das ferramentas e tecnologias atualizadas, incluindo as vers\u00f5es mais recentes do FastAPI, Pydantic, SQLAlchemy ORM, Python e Alembic.

Este conte\u00fado foi pensado para auxiliar na compreens\u00e3o de como criar uma API eficiente e confi\u00e1vel, dando aten\u00e7\u00e3o a aspectos importantes como testes e integra\u00e7\u00e3o com banco de dados.

Nos vemos na primeira aula. \u2764

"},{"location":"#faq","title":"F.A.Q.","text":"

Perguntas frequentes que me fizeram durante os v\u00eddeos:

"},{"location":"01/","title":"Configurando o ambiente de desenvolvimento","text":""},{"location":"01/#configurando-o-ambiente-de-desenvolvimento","title":"Configurando o Ambiente de Desenvolvimento","text":"

Objetivos dessa aula:

Caso prefira ver a aula em v\u00eddeo

Esse aula ainda n\u00e3o est\u00e1 dispon\u00edvel em formato de v\u00eddeo, somente em texto ou live!

Aula Slides C\u00f3digo Quiz Exerc\u00edcios

Nesta aula, iniciaremos nossa jornada na constru\u00e7\u00e3o de uma API com FastAPI. Partiremos do b\u00e1sico, configurando nosso ambiente de desenvolvimento. Discutiremos desde a escolha e instala\u00e7\u00e3o da vers\u00e3o correta do Python at\u00e9 a instala\u00e7\u00e3o e configura\u00e7\u00e3o do Poetry, um gerenciador de pacotes e depend\u00eancias para Python. Al\u00e9m disso, instalaremos e configuraremos uma s\u00e9rie de ferramentas de desenvolvimento \u00fateis, como Ruff, pytest e Taskipy.

Ap\u00f3s configurado o nosso ambiente, criaremos nosso primeiro programa \"Hello, World!\" com FastAPI. Isso nos permitir\u00e1 confirmar que tudo est\u00e1 funcionando corretamente. E, finalmente, exploraremos uma parte crucial do Desenvolvimento Orientado por Testes (TDD), escrevendo nosso primeiro teste com Pytest.

"},{"location":"01/#ambiente-de-desenvolvimento","title":"Ambiente de Desenvolvimento","text":"

Para iniciar esse curso voc\u00ea precisa de algumas ferramentas instaladas:

  1. Um editor de texto a sua escolha (Usarei o GNU/Emacs)
  2. Um terminal a sua escolha (Usarei o Terminator)
  3. Uma vers\u00e3o 3.11+ do Python instalada.
    • Caso n\u00e3o tenha essa vers\u00e3o voc\u00ea pode baixar do site oficial
    • Ou instalar via pyenv
    • Embora a vers\u00e3o 3.7 seja suficiente para executar o FastAPI em si, neste curso usaremos recursos de Python que est\u00e3o presentes apenas a partir da vers\u00e3o 3.11. Portanto, essa \u00e9 a vers\u00e3o m\u00ednima obrigat\u00f3ria para o curso.
  4. O Poetry para gerenciar os pacotes e seu ambiente virtual (caso n\u00e3o conhe\u00e7a o poetry temos uma live de python sobre ele)
  5. Git: Para gerenciar vers\u00f5es do nosso projeto. (Caso precise de um tutorial legal sobre git, o TeoMeWhy fez uma playlist muito legal, totalmente de gra\u00e7a)
  6. Docker: Para criar um container da nossa aplica\u00e7\u00e3o (caso n\u00e3o tenha nenhuma experi\u00eancia com docker a Linuxtips tem uma playlist completa e gr\u00e1tis sobre docker no canal deles no YouTube)1
  7. OPCIONAL: O pipx pode te ajudar bastante nesses momentos de instala\u00e7\u00f5es
  8. OPCIONAL: O ignr para criar nosso gitignore
  9. OPCIONAL (extremamente recomendado): O gh para criar o reposit\u00f3rio e fazer altera\u00e7\u00f5es sem precisar acessar a p\u00e1gina do Github

\ud83d\udea8\ud83d\udea8 Caso voc\u00ea precise de ajuda com a instala\u00e7\u00e3o dessas ferramentas, temos um ap\u00eandice especial para te ajudar com isso!. Basta clicar em achar a ferramenta que deseja instalar! \ud83d\udea8\ud83d\udea8

"},{"location":"01/#instalacao-do-python","title":"Instala\u00e7\u00e3o do Python","text":"

Se voc\u00ea precisar (re)construir o ambiente usado nesse curso, \u00e9 extremamente recomendado que voc\u00ea use o pyenv.

Pyenv \u00e9 uma aplica\u00e7\u00e3o externa ao python que permite que voc\u00ea instale diferentes vers\u00f5es do python no sistema e as isola. Podendo isolar vers\u00f5es espec\u00edficas, para projetos espec\u00edficos. Na computa\u00e7\u00e3o, chamamos esse conceito de shim. Uma camada, onde toda vez que o python for chamado, ele redirecionar\u00e1 ao python na vers\u00e3o especificada no pyenv globalmente ou em uma vers\u00e3o fixada em projeto especifico. Uma esp\u00e9cie de \"proxy\".

graph LR;\n  A[\"Executando o python no terminal\"] --> pyenv\n  pyenv[\"Pyenv shim\"] --> questao{\"existe '.python-version'?\"}\n  questao -->|sim| B[\"Execute esta vers\u00e3o instalada no pyenv\"]\n  questao -->|n\u00e3o| C[\"Use a vers\u00e3o global do pyenv\"]

A instala\u00e7\u00e3o do pyenv varia entre sistemas operacionais. Caso voc\u00ea esteja usando Windows, recomendo que voc\u00ea use o pyenv-windows para fazer a instala\u00e7\u00e3o, os passos est\u00e3o descritos na p\u00e1gina. Para GNU/Linux e MacOS, use o pyenv-installer, os passos para instala\u00e7\u00e3o tamb\u00e9m est\u00e3o descritos.

Navegue at\u00e9 o diret\u00f3rio onde far\u00e1 os c\u00f3digos e exerc\u00edcios do curso e digite os seguintes comandos do pyenv:

Vers\u00e3o 3.11Vers\u00e3o 3.12 $ Execu\u00e7\u00e3o no terminal!
pyenv update\npyenv install 3.11:latest\n
Para quem usa Windows

O pyenv-win tem um bug intermitente em rela\u00e7\u00e3o ao uso de :latest:

PS C:\\Users\\vagrant> pyenv install 3.11:latest\n:: [Info] ::  Mirror: https://www.python.org/ftp/python\npyenv-install: definition not found: 3.11:latest\n\nSee all available versions with `pyenv install --list`.\nDoes the list seem out of date? Update it using `pyenv update`.\n

Caso voc\u00ea se depare com esse erro, pode rodar o comando pyenv install --list e ver a maior vers\u00e3o dispon\u00edvel do python no momento da sua instala\u00e7\u00e3o. Em seguida executar pyenv install 3.11.<a maior vers\u00e3o dispon\u00edvel>. Nesse momento em que escrevo \u00e9 a vers\u00e3o 3.11.9 :

PS C:\\Users\\vagrant> pyenv install 3.11.9\n:: [Info] ::  Mirror: https://www.python.org/ftp/python\n:: [Downloading] ::  3.11.9 ...\n:: [Downloading] ::  From https://www.python.org/ftp/python/3.11.9/3.11.9-amd64.exe\n:: [Downloading] ::  To   C:\\Users\\vagrant\\.pyenv\\pyenv-win\\install_cache\\python-3.11.9-amd64.exe\n:: [Installing] ::  3.11.9 ...\n:: [Info] :: completed! 3.11.9\n

Desta forma os pr\u00f3ximos comandos podem ser executados normalmente.

Certifique que a vers\u00e3o do python 3.11 esteja instalada:

$ Execu\u00e7\u00e3o no terminal!
pyenv versions\n* system (set by /home/dunossauro/.pyenv/version)\n3.10.12\n3.11.1\n3.12.0\n3.11.9\n3.12.0b1\n

A resposta esperada \u00e9 que o Python 3.11.9 (a maior vers\u00e3o do python 3.11 enquanto escrevia esse material) esteja nessa lista.

$ Execu\u00e7\u00e3o no terminal!
pyenv update\npyenv install 3.12:latest\n
Para quem usa Windows

O pyenv-win tem um bug intermitente em rela\u00e7\u00e3o ao uso de :latest:

PS C:\\Users\\vagrant> pyenv install 3.12:latest\n:: [Info] ::  Mirror: https://www.python.org/ftp/python\npyenv-install: definition not found: 3.12:latest\n\nSee all available versions with `pyenv install --list`.\nDoes the list seem out of date? Update it using `pyenv update`.\n

Caso voc\u00ea se depare com esse erro, pode rodar o comando pyenv install --list e ver a maior vers\u00e3o dispon\u00edvel do python no momento da sua instala\u00e7\u00e3o. Em seguida executar pyenv install 3.12.<a maior vers\u00e3o dispon\u00edvel>. Nesse momento em que escrevo \u00e9 a vers\u00e3o 3.12.3 :

PS C:\\Users\\vagrant> pyenv install 3.12.3\n:: [Info] ::  Mirror: https://www.python.org/ftp/python\n:: [Downloading] ::  3.12.3 ...\n:: [Downloading] ::  From https://www.python.org/ftp/python/3.12.3/3.12.3-amd64.exe\n:: [Downloading] ::  To   C:\\Users\\vagrant\\.pyenv\\pyenv-win\\install_cache\\python-3.12.3-amd64.exe\n:: [Installing] ::  3.12.3 ...\n:: [Info] :: completed! 3.12.3\n

Desta forma os pr\u00f3ximos comandos podem ser executados normalmente.

Certifique que a vers\u00e3o do python 3.12 esteja instalada:

$ Execu\u00e7\u00e3o no terminal!
pyenv versions\n* system (set by /home/dunossauro/.pyenv/version)\n3.10.12\n3.11.1\n3.12.0\n3.12.3\n3.12.0b1\n

A resposta esperada \u00e9 que o Python 3.12.3 (a maior vers\u00e3o do python 3.12 enquanto escrevia esse material) esteja nessa lista.

Toda a implementa\u00e7\u00e3o do curso foi feita com o python 3.11 e testada para ser compat\u00edvel com a vers\u00e3o 3.12. Nesse momento de configura\u00e7\u00e3o est\u00e3o dispon\u00edveis as duas vers\u00f5es. Ao decorrer do curso, voc\u00ea pode se deparar com a vers\u00e3o 3.11 fixada no texto. Mas, sinta-se a vontade para alterar para vers\u00e3o 3.12.

"},{"location":"01/#gerenciamento-de-dependencias-com-poetry","title":"Gerenciamento de Depend\u00eancias com Poetry","text":"

Ap\u00f3s instalar o Python, o pr\u00f3ximo passo \u00e9 instalar o Poetry, um gerenciador de pacotes e depend\u00eancias para Python. O Poetry facilita a cria\u00e7\u00e3o, o gerenciamento e a distribui\u00e7\u00e3o de pacotes Python.

Caso esse seja seu primeiro contato com o Poetry

Temos uma live de python explicando somente ele:

Link direto

Para instalar o Poetry, voc\u00ea pode seguir as instru\u00e7\u00f5es presentes na documenta\u00e7\u00e3o oficial do Poetry para o seu sistema operacional. Alternativamente, se voc\u00ea optou por usar o pipx, pode instalar o Poetry com o seguinte comando:

$ Execu\u00e7\u00e3o no terminal!
pipx install poetry\n
Caso queira usar o pipx e n\u00e3o o tenha instalado no seu ambiente

O pipx \u00e9 uma forma de instalar pacotes de forma global no seu sistema sem que eles interfiram no seu ambiente global do python. Ele cria um ambiente virtual isolado para cada ferramenta.

O guia de instala\u00e7\u00e3o do pipx contempla diversos sistemas operacionais: guia

"},{"location":"01/#criacao-do-projeto-fastapi-e-instalacao-das-dependencias","title":"Cria\u00e7\u00e3o do Projeto FastAPI e Instala\u00e7\u00e3o das Depend\u00eancias","text":"

Agora que temos o Python e o Poetry prontos, podemos come\u00e7ar a criar nosso projeto FastAPI.

Inicialmente criaremos um novo projeto python usando o Poetry, com o comando poetry new e em seguida navegaremos at\u00e9 o diret\u00f3rio criado:

$ Execu\u00e7\u00e3o no terminal!
poetry new fast_zero\ncd fast_zero\n

Ele criar\u00e1 uma estrutura de arquivos e pastas como essa:

.\n\u251c\u2500\u2500 fast_zero\n\u2502  \u2514\u2500\u2500 __init__.py\n\u251c\u2500\u2500 pyproject.toml\n\u251c\u2500\u2500 README.md\n\u2514\u2500\u2500 tests\n   \u2514\u2500\u2500 __init__.py\n
Vers\u00e3o 3.11Vers\u00e3o 3.12

Para que a vers\u00e3o do python que instalamos via pyenv seja usada em nosso projeto criado com poetry, devemos dizer ao pyenv qual vers\u00e3o do python ser\u00e1 usada nesse diret\u00f3rio:

$ Execu\u00e7\u00e3o no terminal!
pyenv local 3.11.9  # (1)!\n
  1. Essa era a maior vers\u00e3o do 3.11 quando escrevi

Esse comando criar\u00e1 um arquivo oculto chamado .python-version na raiz do nosso projeto:

.python-version
3.11.9\n

Esse arquivo far\u00e1 com que toda vez que o terminal for aberto nesse diret\u00f3rio, o pyenv use a vers\u00e3o descrita no arquivo quando o python interpretador for chamado.

Em conjunto com essa instru\u00e7\u00e3o, devemos dizer ao poetry que usaremos exatamente a vers\u00e3o 3.11 em nosso projeto. Para isso alteraremos o arquivo de configura\u00e7\u00e3o do projeto o pyproject.toml na raiz do projeto:

pyproject.toml
[tool.poetry.dependencies]\npython = \"3.11.*\"  # (1)!\n
  1. .* quer dizer qualquer vers\u00e3o da 3.11

Para que a vers\u00e3o do python que instalamos via pyenv seja usada em nosso projeto criado com poetry, devemos dizer ao pyenv qual vers\u00e3o do python ser\u00e1 usada nesse diret\u00f3rio:

$ Execu\u00e7\u00e3o no terminal!
pyenv local 3.12.3  # (1)!\n
  1. Essa era a maior vers\u00e3o do 3.12 quando escrevi

Esse comando criar\u00e1 um arquivo oculto chamado .python-version na raiz do nosso projeto:

.python-version
3.12.3\n

Esse arquivo far\u00e1 com que toda vez que o terminal for aberto nesse diret\u00f3rio, o pyenv use a vers\u00e3o descrita no arquivo quando o python interpretador for chamado.

Em conjunto com essa instru\u00e7\u00e3o, devemos dizer ao poetry que usaremos exatamente a vers\u00e3o 3.12 em nosso projeto. Para isso alteraremos o arquivo de configura\u00e7\u00e3o do projeto o pyproject.toml na raiz do projeto:

pyproject.toml
[tool.poetry.dependencies]\npython = \"3.12.*\"  # (1)!\n
  1. .* quer dizer qualquer vers\u00e3o da 3.12

Desta forma, temos uma vers\u00e3o do python selecionada para esse projeto e uma garantia que o poetry usar\u00e1 essa vers\u00e3o para a cria\u00e7\u00e3o do nosso ambiente virtual.

Em seguida, inicializaremos nosso ambiente virtual com Poetry e instalaremos o FastAPI:

$ Execu\u00e7\u00e3o no terminal!
poetry install # (1)!\npoetry add 'fastapi[standard]' # (2)!\n
  1. Cria o ambiente virtual (venv)
  2. Adiciona o FastAPI no nosso ambiente virtual
"},{"location":"01/#primeira-execucao-de-um-hello-world","title":"Primeira Execu\u00e7\u00e3o de um \"Hello, World!\"","text":"

Uma coisa bastante interessante sobre o FastAPI \u00e9 que ele \u00e9 um framework web baseado em fun\u00e7\u00f5es. Da mesma forma em que criamos fun\u00e7\u00f5es tradicionalmente em python, podemos estender essas fun\u00e7\u00f5es para que elas sejam servidas pelo servidor. Por exemplo:

fast_zero/app.py
def read_root():\n    return {'message': 'Ol\u00e1 Mundo!'}\n

Essa fun\u00e7\u00e3o em python basicamente retorna um dicion\u00e1rio com uma chave chamada 'message' e uma mensagem 'Ol\u00e1 Mundo!'. Se adicionarmos essa fun\u00e7\u00e3o em novo arquivo chamado app.py no diret\u00f3rio fast_zero. Podemos fazer a chamada dela pelo terminal interativo (REPL):

>>> Terminal interativo!
>>> read_root()\n{'message': 'Ol\u00e1 Mundo!'}\n

De forma tradicional, como todas as fun\u00e7\u00f5es em python.

Dica: Como abrir o terminal interativo (REPL)

Para abrir o terminal interativo com o seu c\u00f3digo carregado, voc\u00ea deve chamar o Python no terminal usando -i:

$ Execu\u00e7\u00e3o no terminal!
python -i <seu_arquivo.py>\n

O interpretador do Python executa o c\u00f3digo do arquivo e retorna o shell ap\u00f3s executar tudo que est\u00e1 escrito no arquivo.

Para o nosso caso espec\u00edfico, como o nome do arquivo \u00e9 fast_zero/app.py, devemos executar esse comando no terminal:

$ Execu\u00e7\u00e3o no terminal!
python -i fast_zero/app.py\n

Desta forma, usando somente um decorador do FastAPI, podemos fazer com que uma determinada fun\u00e7\u00e3o seja acess\u00edvel pela rede:

fast_zero/app.py
from fastapi import FastAPI # (1)!\n\napp = FastAPI()  # (2)!\n\n@app.get('/')  # (3)!\ndef read_root():  # (4)!\n    return {'message': 'Ol\u00e1 Mundo!'} # (5)!\n
  1. Importando da biblioteca fastapi o objeto FastAPI
  2. Iniciando uma aplica\u00e7\u00e3o FastAPI
  3. Definindo um endpoint com o endere\u00e7o / acess\u00edvel pelo m\u00e9todo HTTP GET
  4. Fun\u00e7\u00e3o que ser\u00e1 executada quando o endere\u00e7o / for acessado por um cliente
  5. Os dados que ser\u00e3o retornados pelo endere\u00e7o quando for chamado

A linha em destaque @app.get('/') exp\u00f5em a nossa fun\u00e7\u00e3o para ser servida pelo FastAPI. Dizendo que quando um cliente acessar o nosso endere\u00e7o de rede no caminho /, usando o m\u00e9todo HTTP GET2, a fun\u00e7\u00e3o ser\u00e1 executada. Desta maneira, temos todo o c\u00f3digo necess\u00e1rio para criar nossa primeira aplica\u00e7\u00e3o web com FastAPI.

Antes de iniciarmos nossa aplica\u00e7\u00e3o, temos que fazer um passo importante, habilitar o ambiente virtual, para que o python consiga enxergar nossas depend\u00eancias instaladas. O poetry tem um comando espec\u00edfico para isso:

$ Execu\u00e7\u00e3o no terminal!
poetry shell\n

Agora com o ambiente virtual ativo, podemos iniciar nosso servidor FastAPI para iniciar nossa aplica\u00e7\u00e3o:

$ Execu\u00e7\u00e3o no terminal!
fastapi dev fast_zero/app.py\n

Esse comando diz ao FastAPI para iniciar o servidor de desenvolvimento (dev) usando o arquivo fast_zero/app.py

A resposta do comando no terminal deve ser parecida com essa:

Resposta do comando `fastapi dev fast_zero/app.py`
INFO     Using path fast_zero/app.py\nINFO     Resolved absolute path /home/dunossauro/git/fastapi-do-zero/codigo_das_aulas/01/fast_zero/app.py\nINFO     Searching for package file structure from directories with __init__.py files\nINFO     Importing from /home/dunossauro/git/fastapi-do-zero/codigo_das_aulas/01\n\n \u256d\u2500 Python package file structure \u2500\u256e\n \u2502                                 \u2502\n \u2502  \ud83d\udcc1 fast_zero                   \u2502\n \u2502  \u251c\u2500\u2500 \ud83d\udc0d __init__.py             \u2502\n \u2502  \u2514\u2500\u2500 \ud83d\udc0d app.py                  \u2502\n \u2502                                 \u2502\n \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\n\nINFO     Importing module fast_zero.app\nINFO     Found importable FastAPI app\n\n \u256d\u2500\u2500\u2500\u2500 Importable FastAPI app \u2500\u2500\u2500\u2500\u2500\u256e\n \u2502                                 \u2502\n \u2502  from fast_zero.app import app  \u2502\n \u2502                                 \u2502\n \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\n\nINFO     Using import string fast_zero.app:app\n\n \u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 FastAPI CLI - Development mode \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\n \u2502                                                     \u2502\n \u2502  Serving at: http://127.0.0.1:8000                  \u2502\n \u2502                                                     \u2502\n \u2502  API docs: http://127.0.0.1:8000/docs               \u2502\n \u2502                                                     \u2502\n \u2502  Running in development mode, for production use:   \u2502\n \u2502                                                     \u2502\n \u2502  fastapi run                                        \u2502\n \u2502                                                     \u2502\n \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\n\nINFO:     Will watch for changes in these directories: ['/home/dunossauro/git/fastapi-do-zero/codigo_das_aulas/01']\nINFO:     Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)\nINFO:     Started reloader process [893203] using WatchFiles\nINFO:     Started server process [893207]\nINFO:     Waiting for application startup.\nINFO:     Application startup complete.\n

A mensagem de resposta do CLI: serving: http://127.0.0.1:8000 tem uma informa\u00e7\u00e3o bastante importante.

  1. Ela nos diz qual o protocolo que ele est\u00e1 servido HTTP, o protocolo padr\u00e3o da web;
  2. O endere\u00e7o de rede (IP) no qual ele est\u00e1 escutando 127.0.0.1, endere\u00e7o especial (loopback) que aponta para a nossa pr\u00f3pria m\u00e1quina;
  3. A porta :8000, a qual \u00e9 a porta da nossa m\u00e1quina que est\u00e1 reservada para nossa aplica\u00e7\u00e3o.

Agora, com o servidor inicializado, podemos usar um cliente para acessar o endere\u00e7o http://127.0.0.1:8000.

O cliente mais tradicional da web \u00e9 o navegador, podemos digitar o endere\u00e7o na barra de navega\u00e7\u00e3o e se tudo ocorreu corretamente, voc\u00ea deve ver a mensagem \"Ol\u00e1 Mundo!\" em formato JSON.

Para parar a execu\u00e7\u00e3o do fastapi no shell, voc\u00ea pode digitar Ctrl+C e a mensagem Shutting down aparecer\u00e1 mostrando que o servidor foi finalizado.

Diferentes clientes para nossa aplica\u00e7\u00e3o

Caso exista uma curiosidade sobre outros clientes HTTP que n\u00e3o o browser, podemos usar aplica\u00e7\u00f5es de linha de comando como tradicional curl:

$ Execu\u00e7\u00e3o no terminal!
curl 127.0.0.1:8000\n{\"message\":\"Ol\u00e1 Mundo!\"}\n

Ou o meu cliente HTTP preferido (escrito em python), o HTTPie:

$ Execu\u00e7\u00e3o no terminal!
http 127.0.0.1:8000\nHTTP/1.1 200 OK\ncontent-length: 25\ncontent-type: application/json\ndate: Thu, 11 Jan 2024 11:46:32 GMT\nserver: uvicorn\n\n{\n    \"message\": \"Ol\u00e1 Mundo!\"\n}\n

Existem at\u00e9 mesmo aplica\u00e7\u00f5es gr\u00e1ficas de c\u00f3digo aberto pensadas para serem clientes HTTP para APIs. Como o hoppscotch:

Ou como o Bruno:

"},{"location":"01/#uvicorn","title":"Uvicorn","text":"

O FastAPI \u00e9 \u00f3timo para criar APIs, mas n\u00e3o pode disponibiliz\u00e1-las na rede sozinho. Embora o FastAPI tenha uma aplica\u00e7\u00e3o de terminal que facilita a execu\u00e7\u00e3o. Para podermos acessar essas APIs por um navegador ou de outras aplica\u00e7\u00f5es clientes, \u00e9 necess\u00e1rio um servidor. \u00c9 a\u00ed que o Uvicorn entra em cena. Ele atua como esse servidor, disponibilizando a API do FastAPI em rede. Isso permite que a API seja acessada de outros dispositivos ou programas.

Como notamos na resposta do comando fastapi dev fast_zero/app.py:

Fim da resposta de `fastapi dev fast_zero/app.py`
INFO:     Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)\nINFO:     Started reloader process [893203] using WatchFiles\nINFO:     Started server process [893207]\nINFO:     Waiting for application startup.\nINFO:     Application startup complete.\n

Sempre que usarmos o fastapi para inicializar a aplica\u00e7\u00e3o no shell, ele faz uma chamada interna para inicializar o uvicorn. Por esse motivo ele aparece nas respostas HTTP e tamb\u00e9m na execu\u00e7\u00e3o do comando.

Voc\u00ea poderia chamar a aplica\u00e7\u00e3o diretamente pelo Uvicorn tamb\u00e9m

$ Execu\u00e7\u00e3o no terminal!
uvicorn fast_zero.app:app\n
Esse comando diz ao uvicorn o seguinte: na pasta fast_zero existe um arquivo chamado app. Dentro desse arquivo, temos uma aplica\u00e7\u00e3o para ser servida com o nome de app. O comando \u00e9 composto por uvicorn pasta.arquivo:vari\u00e1vel.

A resposta do comando no terminal deve ser parecida com essa:

Resultado do comando
INFO:     Started server process [127946]\nINFO:     Waiting for application startup.\nINFO:     Application startup complete.\nINFO:     Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)\n
"},{"location":"01/#instalando-as-ferramentas-de-desenvolvimento","title":"Instalando as ferramentas de desenvolvimento","text":"

As escolhas de ferramentas de desenvolvimento, de forma geral, s\u00e3o escolhas bem particulares. N\u00e3o costumam ser consensuais nem mesmo em times de desenvolvimento. Dito isso, selecionei algumas ferramentas que gosto de usar e alinhadas com a utilidade que elas apresentam no desenvolvimento do projeto.

As ferramentas escolhidas s\u00e3o:

Para instalar essas ferramentas que usaremos em desenvolvimento, podemos usar um grupo (--group dev) do poetry focado nelas, para n\u00e3o serem instaladas quando nossa aplica\u00e7\u00e3o estiver em produ\u00e7\u00e3o:

$ Execu\u00e7\u00e3o no terminal!
poetry add --group dev pytest pytest-cov taskipy ruff httpx\n

O HTTPX foi inclu\u00eddo, pois ele \u00e9 uma depend\u00eancia do cliente de testes do FastAPI.

"},{"location":"01/#configurando-as-ferramentas-de-desenvolvimento","title":"Configurando as ferramentas de desenvolvimento","text":"

Ap\u00f3s a instala\u00e7\u00e3o das ferramentas de desenvolvimento, precisamos definir as configura\u00e7\u00f5es de cada uma individualmente no arquivo pyproject.toml.

"},{"location":"01/#ruff","title":"Ruff","text":"

O Ruff \u00e9 uma ferramenta moderna em python, escrita em rust, compat\u00edvel2 com os projetos de an\u00e1lise est\u00e1tica escritos e mantidos originalmente pela comunidade no projeto PYCQA3 e tem duas fun\u00e7\u00f5es principais:

  1. Analisar o c\u00f3digo de forma est\u00e1tica (Linter): Efetuar a verifica\u00e7\u00e3o se estamos programando de acordo com boas pr\u00e1ticas do python.
  2. Formatar o c\u00f3digo (Formatter): Efetuar a verifica\u00e7\u00e3o do c\u00f3digo para padronizar um estilo de c\u00f3digo pr\u00e9-definido.

Para configurar o ruff montamos a configura\u00e7\u00e3o em 3 tabelas distintas no arquivo pyproject.toml. Uma para as configura\u00e7\u00f5es globais, uma para o linter e uma para o formatador.

Configura\u00e7\u00e3o global

Na configura\u00e7\u00e3o global do Ruff queremos alterar somente duas coisas. O comprimento de linha para 79 caracteres (conforme sugerido na PEP-8) e em seguida, informaremos que o diret\u00f3rio de migra\u00e7\u00f5es de banco de dados ser\u00e1 ignorado na checagem e na formata\u00e7\u00e3o:

pyproject.toml
[tool.ruff]\nline-length = 79\nextend-exclude = ['migrations']\n
Nota sobre \"migrations\"

Nessa fase de configura\u00e7\u00e3o, excluiremos a pasta migrations, isso pode n\u00e3o fazer muito sentido nesse momento. Contudo, quando iniciarmos o trabalho com o banco de dados, a ferramenta Alembic faz gera\u00e7\u00e3o de c\u00f3digo autom\u00e1tico. Por serem c\u00f3digos gerados automaticamente, n\u00e3o queremos alterar a configura\u00e7\u00e3o feita por ela.

Linter

Durante a an\u00e1lise est\u00e1tica do c\u00f3digo, queremos buscar por coisas espec\u00edficas. No Ruff, precisamos dizer exatamente o que ele deve analisar. Isso \u00e9 feito por c\u00f3digos. Usaremos estes:

pyproject.toml
[tool.ruff.lint]\npreview = true\nselect = ['I', 'F', 'E', 'W', 'PL', 'PT']\n

Para mais informa\u00e7\u00f5es sobre a configura\u00e7\u00e3o e sobre os c\u00f3digos do ruff e dos projetos do PyCQA, voc\u00ea pode checar a documenta\u00e7\u00e3o do ruff ou as documenta\u00e7\u00f5es originais dos projetos PyQCA.

Formatter

A formata\u00e7\u00e3o do Ruff praticamente n\u00e3o precisa ser alterada. Pois ele vai seguir as boas pr\u00e1ticas e usar a configura\u00e7\u00e3o global de 79 caracteres por linha. A \u00fanica altera\u00e7\u00e3o que farei \u00e9 o uso de aspas simples ' no lugar de aspas duplas \":

pyproject.toml
[tool.ruff.format]\npreview = true\nquote-style = 'single'\n

Lembrando que a op\u00e7\u00e3o de usar aspas simples \u00e9 totalmente pessoal, voc\u00ea pode usar aspas duplas se quiser.

"},{"location":"01/#pytest","title":"pytest","text":"

O Pytest \u00e9 uma framework de testes, que usaremos para escrever e executar nossos testes. O configuraremos para reconhecer o caminho base para execu\u00e7\u00e3o dos testes na raiz do projeto .:

pyproject.toml
[tool.pytest.ini_options]\npythonpath = \".\"\naddopts = '-p no:warnings'\n

Na segunda linha dizemos para o pytest adicionar a op\u00e7\u00e3o no:warnings. Para ter uma visualiza\u00e7\u00e3o mais limpa dos testes, caso alguma biblioteca exiba uma mensagem de warning, isso ser\u00e1 suprimido pelo pytest.

"},{"location":"01/#taskipy","title":"Taskipy","text":"

A ideia do Taskipy \u00e9 ser um executor de tarefas (task runner) complementar em nossa aplica\u00e7\u00e3o. No lugar de ter que lembrar comandos como o do fastapi, que vimos na execu\u00e7\u00e3o da aplica\u00e7\u00e3o, que tal substituir ele simplesmente por task run?

Isso funcionaria para qualquer comando complicado em nossa aplica\u00e7\u00e3o. Simplificando as chamadas e tamb\u00e9m para n\u00e3o termos que lembrar de como executar todos os comandos de cabe\u00e7a.

Alguns comandos que criaremos agora no in\u00edcio:

pyproject.toml
[tool.taskipy.tasks]\nlint = 'ruff check .; ruff check . --diff'\nformat = 'ruff check . --fix; ruff format .'\nrun = 'fastapi dev fast_zero/app.py'\npre_test = 'task lint'\ntest = 'pytest -s -x --cov=fast_zero -vv'\npost_test = 'coverage html'\n

Os comandos definidos fazem o seguinte:

Para executar um comando, \u00e9 bem mais simples, precisando somente passar a palavra task <comando>.

Caso precise ver o arquivo todo

O meu est\u00e1 exatamente assim:

pyproject.toml
[tool.poetry]\nname = \"fast-zero\"\nversion = \"0.1.0\"\ndescription = \"\"\nauthors = [\"Your Name <you@example.com>\"]\nreadme = \"README.md\"\n\n[tool.poetry.dependencies]\npython = \"3.12.*\" # ou \"3.11.*\"\nfastapi = \"^0.113.0\"\n\n[tool.poetry.group.dev.dependencies]\npytest = \"^8.3.2\"\npytest-cov = \"^5.0.0\"\ntaskipy = \"^1.13.0\"\nruff = \"^0.6.4\"\nhttpx = \"^0.27.2\"\n\n[tool.ruff]\nline-length = 79\nextend-exclude = ['migrations']\n\n[tool.ruff.lint]\npreview = true\nselect = ['I', 'F', 'E', 'W', 'PL', 'PT']\n\n[tool.ruff.format]\npreview = true\nquote-style = 'single'\n\n[tool.pytest.ini_options]\npythonpath = \".\"\naddopts = '-p no:warnings'\n\n[tool.taskipy.tasks]\nlint = 'ruff check .; ruff check . --diff'\nformat = 'ruff check . --fix; ruff format .'\nrun = 'fastapi dev fast_zero/app.py'\npre_test = 'task lint'\ntest = 'pytest -s -x --cov=fast_zero -vv'\npost_test = 'coverage html'\n\n[build-system]\nrequires = [\"poetry-core\"]\nbuild-backend = \"poetry.core.masonry.api\"\n

Um ponto importante \u00e9 que as vers\u00f5es dos pacotes podem variar dependendo da data em que voc\u00ea fizer a instala\u00e7\u00e3o dos pacotes. Esse arquivo \u00e9 somente um exemplo.

"},{"location":"01/#os-efeitos-dessas-configuracoes-de-desenvolvimento","title":"Os efeitos dessas configura\u00e7\u00f5es de desenvolvimento","text":"

Caso voc\u00ea tenha copiado o c\u00f3digo que usamos para definir fast_zero/app.py, pode testar os comandos que criamos para o taskipy:

$ Execu\u00e7\u00e3o no terminal!
task lint\n

Dessa forma, veremos que cometemos algumas infra\u00e7\u00f5es na formata\u00e7\u00e3o da PEP-8. O ruff nos informar\u00e1 que dever\u00edamos ter adicionado duas linhas antes de uma defini\u00e7\u00e3o de fun\u00e7\u00e3o:

fast_zero/app.py:5:1: E302 [*] Expected 2 blank lines, found 1\nFound 1 error.\n[*] 1 fixable with the `--fix` option.\n--- fast_zero/app.py\n+++ fast_zero/app.py\n@@ -2,6 +2,7 @@\n\n app = FastAPI()\n\n+\n @app.get('/')\n def read_root():\n     return {'message': 'Ol\u00e1 Mundo!'}\n\nWould fix 1 error.\n

Para corrigir isso, podemos usar o nosso comando de formata\u00e7\u00e3o de c\u00f3digo:

ComandoResultado $ Execu\u00e7\u00e3o no terminal!
task format\nFound 1 error (1 fixed, 0 remaining).\n3 files left unchanged\n
fast_zero/app.py
from fastapi import FastAPI\n\napp = FastAPI()\n\n\n@app.get('/')\ndef read_root():\n    return {'message': 'Ol\u00e1 Mundo!'}\n
"},{"location":"01/#introducao-ao-pytest-testando-o-hello-world","title":"Introdu\u00e7\u00e3o ao Pytest: Testando o \"Hello, World!\"","text":"

Antes de entendermos a din\u00e2mica dos testes, precisamos entender o efeito que eles t\u00eam no nosso c\u00f3digo. Podemos come\u00e7ar analisando a cobertura (o quanto do nosso c\u00f3digo est\u00e1 sendo efetivamente testado). Vamos executar os testes:

$ Execu\u00e7\u00e3o no terminal!
task test\n

Teremos uma resposta como essa:

$ Execu\u00e7\u00e3o no terminal!
=========================== test session starts ===========================\nplatform linux -- Python 3.11.7, pytest-7.4.4, pluggy-1.3.0\ncachedir: .pytest_cache\nrootdir: /home/dunossauro/git/fast_zero\nconfigfile: pyproject.toml\nplugins: cov-4.1.0, anyio-4.2.0\ncollected 0 items\n\n---------- coverage: platform linux, python 3.11.7-final-0 -----------\nName                    Stmts   Miss  Cover\n-------------------------------------------\nfast_zero/__init__.py       0      0   100%\nfast_zero/app.py            5      5     0%\n-------------------------------------------\nTOTAL                       5      5     0%\n

As linhas no terminal s\u00e3o referentes ao pytest, que disse que coletou 0 itens. Nenhum teste foi executado.

Caso n\u00e3o tenha muita experi\u00eancia com Pytest

Temos uma live de Python explicando os conceitos b\u00e1sicos da biblioteca

Link direto

A parte importante dessa Mensagem est\u00e1 na tabela gerada pelo coverage. Que diz que temos 5 linhas de c\u00f3digo (Stmts) no arquivo fast_zero/app.py e nenhuma delas est\u00e1 coberta pelos nossos testes. Como podemos ver na coluna Miss.

Por n\u00e3o encontrar nenhum teste, o pytest retornou um \"erro\". Isso significa que nossa tarefa post_test n\u00e3o foi executada. Podemos execut\u00e1-la manualmente:

$ Execu\u00e7\u00e3o no terminal!
task post_test\nWrote HTML report to htmlcov/index.html\n

Isso gera um relat\u00f3rio de cobertura de testes em formato HTML. Podemos abrir esse arquivo em nosso navegador e entender exatamente quais linhas do c\u00f3digo n\u00e3o est\u00e3o sendo testadas.

Se clicarmos no arquivo fast_zero/app.py podemos ver em vermelho as linhas que n\u00e3o est\u00e3o sendo testadas:

Isto significa que precisamos testar todo esse arquivo.

"},{"location":"01/#escrevendo-o-teste","title":"Escrevendo o teste","text":"

Agora, escreveremos nosso primeiro teste com Pytest. Mas, antes de escrever o teste, precisamos criar um arquivo espec\u00edfico para eles. Na pasta tests, vamos criar um arquivo chamado test_app.py.

Por conven\u00e7\u00e3o, todos os arquivos de teste do pytest devem iniciar com um prefixo test_.py

Para testar o c\u00f3digo feito com FastAPI, precisamos de um cliente de teste. A grande vantagem \u00e9 que o FastAPI j\u00e1 conta com um cliente de testes no m\u00f3dulo fastapi.testclient com o objeto TestClient, que precisa receber nosso app como par\u00e2metro:

tests/test_app.py
from fastapi.testclient import TestClient  # (1)!\n\nfrom fast_zero.app import app  # (2)!\n\nclient = TestClient(app)  # (3)!\n
  1. Importa do m\u00f3dulo testclient o objeto TestClient
  2. Importa o nosso app definido em fast_zero
  3. Cria um cliente de testes usando a nossa aplica\u00e7\u00e3o como base

S\u00f3 o fato de termos definido um cliente, j\u00e1 nos mostra uma cobertura bastante diferente:

$ Execu\u00e7\u00e3o no terminal!
task test\n# parte da mensagem foi omitida\ncollected 0 items\n\n---------- coverage: platform linux, python 3.11.3-final-0 -----------\nName                    Stmts   Miss  Cover\n-------------------------------------------\nfast_zero/__init__.py       0      0   100%\nfast_zero/app.py            5      1    80%\n-------------------------------------------\nTOTAL                       5      1    80%\n

Por n\u00e3o coletar nenhum teste, o pytest ainda retornou um \"erro\". Para ver a cobertura, precisaremos executar novamente o post_test manualmente:

$ Execu\u00e7\u00e3o no terminal!
task post_test\nWrote HTML report to htmlcov/index.html\n

No navegador, podemos ver que a \u00fanica linha n\u00e3o \"testada\" \u00e9 aquela onde temos a l\u00f3gica (o corpo) da fun\u00e7\u00e3o read_root. As linhas de defini\u00e7\u00e3o est\u00e3o todas verdes:

No verde vemos o que foi executado quando chamamos o teste, no vermelho o que n\u00e3o foi.

Para resolver isso, temos que criar um teste de fato, fazendo uma chamada para nossa API usando o cliente de teste que definimos:

tests/test_app.py
from http import HTTPStatus  # (6)!\n\nfrom fastapi.testclient import TestClient\n\nfrom fast_zero.app import app\n\n\ndef test_root_deve_retornar_ok_e_ola_mundo():  # (1)!\n    client = TestClient(app)  # (2)!\n\n    response = client.get('/')  # (3)!\n\n    assert response.status_code == HTTPStatus.OK  # (4)!\n    assert response.json() == {'message': 'Ol\u00e1 Mundo!'}  # (5)!\n
  1. No nome da fun\u00e7\u00e3o, geralmente escrevemos o que o teste de fato faz. Aqui estamos dizendo que root deve retornar o status OK e a mensagem \"ol\u00e1 mundo\". Root \u00e9 o nome dado a raiz da URL. O caminho /, que colocamos na defini\u00e7\u00e3o do @app.get('/'). OK \u00e9 o status que diz que a requisi\u00e7\u00e3o aconteceu com sucesso no protocolo HTTP.
  2. Aqui criamos o cliente de teste do nosso app
  3. Nesse ponto, o client faz uma requisi\u00e7\u00e3o. Da mesma forma que o browser, um cliente da API. Nisso, chamamos o endere\u00e7o de root, usando o m\u00e9todo GET.
  4. Aqui fazemos a valida\u00e7\u00e3o do c\u00f3digo de resposta, para saber se a resposta \u00e9 referente ao c\u00f3digo 200, que significa OK. Mais informa\u00e7\u00f5es sobre esse t\u00f3pico aqui.
  5. No final, validamos se o dicion\u00e1rio que enviamos na fun\u00e7\u00e3o \u00e9 o mesmo que recebemos quando fizemos a requisi\u00e7\u00e3o.
  6. Importa\u00e7\u00e3o da biblioteca nativa do python que abstrai os c\u00f3digos de resposta e nos apresenta o status. Mais informa\u00e7\u00f5es sobre esse t\u00f3pico aqui.

Esse teste faz uma requisi\u00e7\u00e3o GET no endpoint / e verifica se o c\u00f3digo de status da resposta \u00e9 200 e se o conte\u00fado da resposta \u00e9 {'message': 'Ol\u00e1 Mundo!'}.

$ Execu\u00e7\u00e3o no terminal!
task test\n# parte da mensagem foi omitida\ncollected 1 item\n\ntests/test_app.py::test_root_deve_retornar_ok_e_ola_mundo PASSED\n\n---------- coverage: platform linux, python 3.11.3-final-0 -----------\nName                    Stmts   Miss  Cover\n-------------------------------------------\nfast_zero/__init__.py       0      0   100%\nfast_zero/app.py            5      0   100%\n-------------------------------------------\nTOTAL                       5      0   100%\n\n================ 1 passed in 1.39s ================\nWrote HTML report to htmlcov/index.html\n

Dessa forma, temos um teste que coletou 1 item (1 teste). Esse teste foi aprovado e a cobertura n\u00e3o deixou de abranger nenhuma linha de c\u00f3digo.

Como conseguimos coletar um item, o post_test foi executado e tamb\u00e9m gerou um HTML com a cobertura atualizada.

"},{"location":"01/#estrutura-de-um-teste","title":"Estrutura de um teste","text":"

Agora que escrevemos nosso primeiro teste de forma intuitiva, podemos entender o que cada passo do teste faz. Essa compreens\u00e3o \u00e9 vital, pois nos ajudar\u00e1 a escrever testes com mais confian\u00e7a e efic\u00e1cia. Para desvendar o m\u00e9todo por tr\u00e1s da nossa abordagem, exploraremos uma estrat\u00e9gia conhecida como AAA, que divide o teste em tr\u00eas fases distintas: Arrange, Act, Assert.

Caso fazer testes ainda seja complicado para voc\u00ea

Temos uma live de python focada em ensinar os primeiros passos no mundo dos testes.

Link direto

Para analisar todas as etapas de um teste, usaremos como exemplo este primeiro teste que escrevemos:

tests/test_app.py
from http import HTTPStatus\n\nfrom fastapi.testclient import TestClient\n\nfrom fast_zero.app import app\n\n\ndef test_root_deve_retornar_ok_e_ola_mundo():\n    client = TestClient(app)  # Arrange\n\n    response = client.get('/')  # Act\n\n    assert response.status_code == HTTPStatus.OK  # Assert\n    assert response.json() == {'message': 'Ol\u00e1 Mundo!'}  # Assert\n

Com base nesse c\u00f3digo, podemos observar as tr\u00eas fases:

"},{"location":"01/#fase-1-organizar-arrange","title":"Fase 1 - Organizar (Arrange)","text":"

Nesta primeira etapa, estamos preparando o ambiente para o teste. No exemplo, a linha com o coment\u00e1rio Arrange n\u00e3o \u00e9 o teste em si, ela monta o ambiente para que o teste possa ser executado. Estamos configurando um client de testes para fazer a requisi\u00e7\u00e3o ao app.

"},{"location":"01/#fase-2-agir-act","title":"Fase 2 - Agir (Act)","text":"

Aqui \u00e9 a etapa onde acontece a a\u00e7\u00e3o principal do teste, que consiste em chamar o Sistema Sob Teste (SUT). No nosso caso, o SUT \u00e9 a rota /, e a a\u00e7\u00e3o \u00e9 representada pela linha response = client.get('/'). Estamos exercitando a rota e armazenando sua resposta na vari\u00e1vel response. \u00c9 a fase em que o c\u00f3digo de testes executa o c\u00f3digo de produ\u00e7\u00e3o que est\u00e1 sendo testado. Agir aqui significa interagir diretamente com a parte do sistema que queremos avaliar, para ver como ela se comporta.

"},{"location":"01/#fase-3-afirmar-assert","title":"Fase 3 - Afirmar (Assert)","text":"

Esta \u00e9 a etapa de verificar se tudo correu como esperado. \u00c9 f\u00e1cil notar onde estamos fazendo a verifica\u00e7\u00e3o, pois essa linha sempre tem a palavra reservada assert. A verifica\u00e7\u00e3o \u00e9 booleana, ou est\u00e1 correta, ou n\u00e3o est\u00e1. Por isso, um teste deve sempre incluir um assert para verificar se o comportamento esperado est\u00e1 correto.

Agora que compreendemos o que cada linha de teste faz em espec\u00edfico, podemos nos orientar de forma clara nos testes que escreveremos no futuro. Cada uma das linhas usadas tem uma raz\u00e3o de estar no teste, e conhecer essa estrutura n\u00e3o s\u00f3 nos d\u00e1 uma compreens\u00e3o mais profunda do que estamos fazendo, mas tamb\u00e9m nos d\u00e1 confian\u00e7a para explorar e escrever testes mais complexos.

"},{"location":"01/#criando-nosso-repositorio-no-git","title":"Criando nosso reposit\u00f3rio no git","text":"

Antes de concluirmos a aula, precisamos executar alguns passos:

  1. Criar um arquivo .gitignore para n\u00e3o adicionar o ambiente virtual e outros arquivos desnecess\u00e1rios no versionamento de c\u00f3digo.
  2. Criar um novo reposit\u00f3rio no GitHub para versionar o c\u00f3digo.
  3. Subir o c\u00f3digo que fizemos para o GitHub.

Criando o arquivo .gitignore

Vamos iniciar com a cria\u00e7\u00e3o de um arquivo .gitignore espec\u00edfico para Python. Existem diversos modelos dispon\u00edveis na internet, como os dispon\u00edveis pelo pr\u00f3prio GitHub, ou o gitignore.io. Uma ferramenta \u00fatil \u00e9 a ignr, feita em Python, que faz o download autom\u00e1tico do arquivo para a nossa pasta de trabalho atual:

$ Execu\u00e7\u00e3o no terminal!
ignr -p python > .gitignore\n

O .gitignore \u00e9 importante porque ele nos ajuda a evitar que arquivos desnecess\u00e1rios ou sens\u00edveis sejam enviados para o reposit\u00f3rio. Isso inclui o ambiente virtual, arquivos de configura\u00e7\u00e3o pessoal, entre outros.

Criando um reposit\u00f3rio no github

Agora, com nossos arquivos indesejados ignorados, podemos iniciar o versionamento de c\u00f3digo usando o git. Para criar um reposit\u00f3rio local, usamos o comando git init .. Para criar esse reposit\u00f3rio no GitHub, utilizaremos o gh, um utilit\u00e1rio de linha de comando que nos auxilia nesse processo:

$ Execu\u00e7\u00e3o no terminal!
git init .\ngh repo create\n

Ao executar gh repo create, algumas informa\u00e7\u00f5es ser\u00e3o solicitadas, como o nome do reposit\u00f3rio e se ele ser\u00e1 p\u00fablico ou privado. Isso ir\u00e1 criar um reposit\u00f3rio tanto localmente quanto no GitHub.

Subindo nosso c\u00f3digo para o github

Com o reposit\u00f3rio pronto, vamos versionar nosso c\u00f3digo. Primeiro, adicionamos o c\u00f3digo ao pr\u00f3ximo commit com git add .. Em seguida, criamos um ponto na hist\u00f3ria do projeto com git commit -m \"Configura\u00e7\u00e3o inicial do projeto\". Por fim, sincronizamos o reposit\u00f3rio local com o remoto no GitHub usando git push:

$ Execu\u00e7\u00e3o no terminal!
git add .\ngit commit -m \"Configura\u00e7\u00e3o inicial do projeto\"\ngit push\n

Caso seja a primeira vez que est\u00e1 utilizando o git push, talvez seja necess\u00e1rio configurar suas credenciais do GitHub.

Esses passos garantem que todo o c\u00f3digo criado na aula esteja versionado e dispon\u00edvel para compartilhamento no GitHub.

"},{"location":"01/#exercicio","title":"Exerc\u00edcio","text":"
  1. Crie um reposit\u00f3rio para acompanhar o curso e suba em alguma plataforma, como Github, gitlab, codeberg, etc. E compartilhe o link no reposit\u00f3rio do curso para podermos aprender juntos.

Exerc\u00edcios resolvidos

"},{"location":"01/#conclusao","title":"Conclus\u00e3o","text":"

Pronto! Agora temos um ambiente de desenvolvimento totalmente configurado para come\u00e7ar a trabalhar com FastAPI e j\u00e1 fizemos nossa primeira imers\u00e3o no Desenvolvimento Orientado por Testes. Na pr\u00f3xima aula nos aprofundaremos na estrutura\u00e7\u00e3o da nossa aplica\u00e7\u00e3o FastAPI. At\u00e9 l\u00e1!

Agora que a aula acabou, \u00e9 um bom momento para voc\u00ea relembrar alguns conceitos e fixar melhor o conte\u00fado respondendo ao question\u00e1rio referente a ela.

Quiz

  1. Voc\u00ea n\u00e3o precisa se preocupar com o docker inicialmente, ele ser\u00e1 usado da aula 10 em diante\u00a0\u21a9

  2. Em alguns casos existe uma diverg\u00eancia de opini\u00f5es em os linter mais tradicionais. Mas, em geral funciona bem.\u00a0\u21a9\u21a9

  3. Em vers\u00f5es antigas do texto us\u00e1vamos as ferramentas do PyCQA como o pylint e o isort.\u00a0\u21a9

"},{"location":"02/","title":"Introdu\u00e7\u00e3o ao desenvolvimento WEB","text":""},{"location":"02/#introducao-ao-desenvolvimento-web","title":"Introdu\u00e7\u00e3o ao desenvolvimento WEB","text":"

Objetivos dessa aula:

Caso prefira ver a aula em v\u00eddeo

Esse aula ainda n\u00e3o est\u00e1 dispon\u00edvel em formato de v\u00eddeo, somente em texto ou live!

Aula Slides C\u00f3digo Quiz Exerc\u00edcios

Boas-vindas \u00e0 segunda aula do nosso curso de FastAPI. Agora que j\u00e1 temos o ambiente preparado, com algum c\u00f3digo escrito e testado, \u00e9 o momento ideal para entendermos o que viemos fazer aqui. At\u00e9 este ponto, voc\u00ea j\u00e1 deve saber que o FastAPI \u00e9 um framework para desenvolvimento de aplica\u00e7\u00f5es web, mais especificamente para o desenvolvimento de APIs web. \u00c9 aqui que ter um bom referencial te\u00f3rico se torna importante para compreendermos exatamente o que o framework \u00e9 capaz de fazer.

"},{"location":"02/#a-web","title":"A web","text":"

Sempre que nos referimos a aplica\u00e7\u00f5es web, estamos falando de aplica\u00e7\u00f5es que funcionam em rede. Essa rede pode ser privativa, como a sua rede dom\u00e9stica ou uma rede empresarial, ou podemos estar nos referindo \u00e0 World Wide Web (WWW), comumente conhecida como \"internet\". A internet, que tem uma longa hist\u00f3ria iniciada na d\u00e9cada de 1960, possui diversos padr\u00f5es definidos e vem se aperfei\u00e7oando desde ent\u00e3o. Compreender completamente sua complexidade \u00e9 um desafio, especialmente 6 d\u00e9cadas ap\u00f3s seu in\u00edcio.

Quando falamos em comunica\u00e7\u00e3o em rede, geralmente nos referimos \u00e0 comunica\u00e7\u00e3o entre dois ou mais dispositivos interconectados. A ideia \u00e9 que possamos nos comunicar com outros dispositivos usando a rede.

"},{"location":"02/#o-modelo-cliente-servidor","title":"O modelo cliente-servidor","text":"

No contexto de aplica\u00e7\u00f5es web, geralmente nos referimos a um modelo espec\u00edfico de comunica\u00e7\u00e3o: o cliente-servidor. Neste modelo, temos clientes, como aplicativos m\u00f3veis, terminais de comando, navegadores, etc., acessando recursos fornecidos por outro computador, conhecido como servidor.

Neste modelo, fazemos chamadas de um cliente, via rede, seguindo alguns padr\u00f5es, e recebemos respostas da nossa aplica\u00e7\u00e3o, o servidor. Por exemplo, podemos enviar um comando ao servidor: \"Crie um usu\u00e1rio para mim\". Em resposta, ele nos fornece um retorno, seja uma confirma\u00e7\u00e3o de sucesso ou uma mensagem de erro.

sequenceDiagram\n    participant Cliente\n    participant Servidor\n    Note left of Cliente: Fazendo a requisi\u00e7\u00e3o\n    Cliente->>Servidor: Crie um usu\u00e1rio\n    activate Servidor\n    Note right of Servidor: Processa a requisi\u00e7\u00e3o\n    Servidor-->>Cliente: Sucesso na requisi\u00e7\u00e3o: Usu\u00e1rio criado com sucesso\n    deactivate Servidor\n    Note left of Cliente: Obtivemos a resposta desejada\n\n    Cliente->>Servidor: Crie o mesmo usu\u00e1rio\n    activate Servidor\n    Note left of Cliente: Fazendo uma nova requisi\u00e7\u00e3o\n    Note right of Servidor: Processa a requisi\u00e7\u00e3o\n    Servidor-->>Cliente: Erro na requisi\u00e7\u00e3o: Usu\u00e1rio j\u00e1 existe\n    deactivate Servidor\n    Note left of Cliente: Obtivemos a resposta de erro

A comunica\u00e7\u00e3o \u00e9 bidirecional: um cliente faz uma requisi\u00e7\u00e3o ao servidor, que por sua vez emite uma resposta.

Por exemplo, ao construir um servidor, precisamos de uma biblioteca que consiga \"servir\" nossa aplica\u00e7\u00e3o. \u00c9 a\u00ed que entra o Uvicorn, respons\u00e1vel por servir nossa aplica\u00e7\u00e3o com FastAPI.

Quando executamos:

$ Execu\u00e7\u00e3o no terminal!
fastapi dev fast_zero/app.py\n

Quando executamos esse comando. O FastAPI faz uma chamada ao uvicorn e iniciamos um servidor em loopback, acess\u00edvel apenas internamente no nosso computador. Por isso, ao acessarmos http://127.0.0.1:8000/ no navegador, estamos fazendo uma requisi\u00e7\u00e3o ao servidor em 127.0.0.1:8000.

sequenceDiagram\n    participant Cliente\n    participant Servidor\n    Note left of Cliente: Fazendo a requisi\u00e7\u00e3o\n    Cliente->>Servidor: \n    activate Servidor\n    Note right of Servidor: Processa a requisi\u00e7\u00e3o\n    Servidor-->>Cliente: Sucesso na requisi\u00e7\u00e3o: {\"message\":\"Ol\u00e1 Mundo!\"}\n    deactivate Servidor\n    Note left of Cliente: Obtivemos a resposta desejada
"},{"location":"02/#usando-o-fastapi-na-rede-local","title":"Usando o fastapi na rede local","text":"

Falando em redes, o Uvicorn no seu PC tamb\u00e9m pode servir o FastAPI na sua rede local:

$ Execu\u00e7\u00e3o no terminal!
fastapi dev fast_zero/app.py --host 0.0.0.0\n
Esse comando tamb\u00e9m poderia ser executado com taskipy

Uma caracter\u00edstica interessante do taskipy \u00e9 que qualquer continua\u00e7\u00e3o ap\u00f3s o comando da task \u00e9 passado para o comando original. Poder\u00edamos ent\u00e3o executar dessa forma tamb\u00e9m:

$ Execu\u00e7\u00e3o no terminal!
task run --host 0.0.0.0\n

Assim, voc\u00ea pode acessar a aplica\u00e7\u00e3o de outro computador na sua rede usando o endere\u00e7o IP da sua m\u00e1quina.

Descobrindo o seu endere\u00e7o local usando python

Caso n\u00e3o esteja familiarizado com o terminal ou ferramentas para descobrir seu endere\u00e7o IP:

>>> Terminal interativo!
>>> import socket\n>>> s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)\n>>> s.connect((\"8.8.8.8\", 80))\n>>> s.getsockname()[0]\n'192.168.0.100'# (1)!\n
  1. O endere\u00e7o do meu computador na rede local

"},{"location":"02/#o-modelo-padrao-da-web","title":"O modelo padr\u00e3o da web","text":"

Ignorando muita hist\u00f3ria e diversas camadas de padr\u00f5es, podemos nos concentrar nos tr\u00eas padr\u00f5es principais que ser\u00e3o mais importantes para n\u00f3s agora:

graph\n    A[Web] --> B[URL]\n    A --> C[HTTP]\n    A --> D[HTML]
"},{"location":"02/#url","title":"URL","text":"

Uma URL (Uniform Resource Locator) \u00e9 como um endere\u00e7o que nos ajuda a encontrar um recurso espec\u00edfico em uma rede, como a URL http://127.0.0.1:8000 que usamos para acessar nossa aplica\u00e7\u00e3o.

Uma URL \u00e9 composta por v\u00e1rias partes, como neste exemplo: protocolo://endere\u00e7o:porta/caminho/recurso?query_string#fragmento. Neste primeiro momento, focaremos nos primeiros quatro componentes, essenciais para o andamento da aula:

  1. Protocolo: A primeira parte da URL, terminando com \"://\". Os mais comuns s\u00e3o \"http://\" e \"https://\". Este protocolo define como os dados s\u00e3o trocados entre seu computador e o local onde o recurso est\u00e1 armazenado, seja na internet ou numa rede local.

  2. Endere\u00e7o do Host: Pode ser um endere\u00e7o IP (como \"192.168.1.10\") ou um endere\u00e7o de DNS (como \"youtube.com\"). Ele identifica o dispositivo na rede que cont\u00e9m o recurso desejado.

  3. Porta (opcional): Ap\u00f3s o endere\u00e7o do host, pode haver um n\u00famero ap\u00f3s dois pontos, como em \"192.168.1.10:8080\". Este n\u00famero \u00e9 a porta, usada para direcionar sua solicita\u00e7\u00e3o ao servi\u00e7o espec\u00edfico no dispositivo. Por padr\u00e3o, as portas s\u00e3o 80 para HTTP e 443 para HTTPS, quando n\u00e3o especificadas.

  4. Caminho: Indica a localiza\u00e7\u00e3o exata do recurso no servidor ou dispositivo. Por exemplo, em \"192.168.1.10:8000/busca\", /busca \u00e9 o nome do recurso. Quando n\u00e3o especificado, o servidor responde com o recurso na raiz (/).

Ao acessarmos via navegador a URL http://127.0.0.1:8000, estamos acessando o servidor via protocolo HTTP, no endere\u00e7o do nosso pr\u00f3prio computador, na porta 8000, solicitando o recurso /.

"},{"location":"02/#http","title":"HTTP","text":"

Quando o cliente inicia uma requisi\u00e7\u00e3o para um endere\u00e7o na rede, isso \u00e9 feito via um protocolo e direcionado ao servidor do recurso. Em aplica\u00e7\u00f5es web, a maioria da comunica\u00e7\u00e3o ocorre via protocolo HTTP ou sua vers\u00e3o segura, o HTTPS.

HTTP, ou Hypertext Transfer Protocol (Protocolo de Transfer\u00eancia de Hipertexto), \u00e9 o protocolo fundamental na web para a transfer\u00eancia de dados e comunica\u00e7\u00e3o entre clientes e servidores. Ele baseia-se no modelo de requisi\u00e7\u00e3o-resposta: onde o cliente faz uma requisi\u00e7\u00e3o ao servidor, que responde a essa requisi\u00e7\u00e3o. Essas requisi\u00e7\u00f5es e respostas s\u00e3o formatadas conforme as regras do protocolo HTTP.

"},{"location":"02/#mensagens","title":"Mensagens","text":"

No contexto do HTTP, tanto requisi\u00e7\u00f5es quanto respostas s\u00e3o referidas como mensagens. As mensagens HTTP na vers\u00e3o 1 t\u00eam uma estrutura textual semelhante ao seguinte exemplo.

Um exemplo de mensagem HTTP enviada pelo cliente:

Exemplo da mensagem emitada pelo cliente
GET / HTTP/1.1\nAccept: */*\nAccept-Encoding: gzip, deflate\nConnection: keep-alive\nHost: 127.0.0.1:8000\nUser-Agent: HTTPie/3.2.2\n

Na primeira linha, temos o verbo GET, que solicita um recurso, neste caso, o recurso \u00e9 /. As linhas seguintes comp\u00f5em o cabe\u00e7alho da mensagem. Elas informam que o cliente aceita qualquer tipo de resposta (Accept: */*), indicam a URL destino (Host: 127.0.0.1:8000) e identificam o cliente que gerou a requisi\u00e7\u00e3o (User-Agent: HTTPie/3.2.2), que neste caso foi o cliente HTTPie.

Em resposta a esta mensagem, o servidor enviou o seguinte:

Exemplo da mensagem de resposta do servidor
HTTP/1.1 200 OK\ncontent-length: 24\ncontent-type: application/json\ndate: Fri, 19 Jan 2024 04:05:50 GMT\nserver: uvicorn\n\n{\n    \"message\": \"Ol\u00e1 mundo\"\n}\n

Aqui, na primeira linha da resposta, temos a vers\u00e3o do protocolo HTTP utilizada e o c\u00f3digo de resposta 200 OK, indicando que a requisi\u00e7\u00e3o foi bem-sucedida. O cabe\u00e7alho da resposta inclui informa\u00e7\u00f5es como o content-length e content-type, que especificam o tamanho e o tipo do conte\u00fado da resposta, respectivamente. A data e o servidor que processou a requisi\u00e7\u00e3o tamb\u00e9m s\u00e3o indicados. Finalmente, o corpo da resposta, formatado em JSON, cont\u00e9m a mensagem \"Ol\u00e1 mundo\".

Como as mensagens do HTTP foram geradas?

A visualiza\u00e7\u00e3o das mensagens foram geradas com o cliente CLI do HTTPie: http GET http://127.0.0.1:8000 -v

GET / HTTP/1.1\nAccept: */*\nAccept-Encoding: gzip, deflate\nConnection: keep-alive\nHost: 127.0.0.1:8000\nUser-Agent: HTTPie/3.2.2\n\nHTTP/1.1 200 OK\ncontent-length: 24\ncontent-type: application/json\ndate: Fri, 19 Jan 2024 04:05:50 GMT\nserver: uvicorn\n\n{\n    \"message\": \"Ol\u00e1 mundo\"\n}\n
"},{"location":"02/#cabecalho","title":"Cabe\u00e7alho","text":"

O cabe\u00e7alho de uma mensagem HTTP cont\u00e9m metadados essenciais sobre a requisi\u00e7\u00e3o ou resposta. Alguns elementos comuns que podem ser inclu\u00eddos no cabe\u00e7alho s\u00e3o:

"},{"location":"02/#corpo","title":"Corpo","text":"

O corpo da mensagem cont\u00e9m os dados propriamente ditos, variando conforme o tipo de m\u00eddia. Exemplos podem incluir um objeto JSON ou uma estrutura HTML.

"},{"location":"02/#verbos","title":"Verbos","text":"

Quando um cliente faz uma requisi\u00e7\u00e3o HTTP, ele indica sua inten\u00e7\u00e3o ao servidor utilizando verbos. Estes verbos sinalizam diferentes a\u00e7\u00f5es no protocolo HTTP. Vejamos alguns exemplos:

Na nossa aplica\u00e7\u00e3o FastAPI, definimos que a fun\u00e7\u00e3o read_root que ser\u00e1 executada quando uma requisi\u00e7\u00e3o GET for feita por um cliente no caminho /:

fast_zero/app.py
@app.get('/')\ndef read_root():\n    return {'message': 'Ol\u00e1 Mundo!'}\n

Quando realizamos a requisi\u00e7\u00e3o via navegador, o verbo padr\u00e3o \u00e9 o GET. Por isso, obtemos na tela a mensagem {'message': 'Ol\u00e1 Mundo!'}.

Essa \u00e9 exatamente a resposta fornecida pela execu\u00e7\u00e3o da fun\u00e7\u00e3o read_root. No futuro, criaremos fun\u00e7\u00f5es para lidar com os outros verbos HTTP.

"},{"location":"02/#codigos-de-resposta","title":"C\u00f3digos de resposta","text":"

No mundo das requisi\u00e7\u00f5es usando o protocolo HTTP, al\u00e9m da resposta obtida quando nos comunicamos com o servidor, tamb\u00e9m recebemos um c\u00f3digo de resposta (status code). Os c\u00f3digos s\u00e3o formas de mostrar ao cliente como o servidor lidou com a sua requisi\u00e7\u00e3o. Os c\u00f3digos s\u00e3o divididos em classes e as classes s\u00e3o distribu\u00eddas por centenas:

Para mais informa\u00e7\u00f5es a cerca do status code acesse a documenta\u00e7\u00e3o do iana

Sempre que fazemos uma requisi\u00e7\u00e3o, obtemos um c\u00f3digo de resposta. Por exemplo, em nosso arquivo de teste, quando efetuamos a requisi\u00e7\u00e3o, fazemos a checagem para ver se recebemos um c\u00f3digo de sucesso, o c\u00f3digo 200 OK:

tests/test_app.py
def test_root_deve_retornar_ok_e_ola_mundo():\n    client = TestClient(app)\n\n    response = client.get('/')\n\n    assert response.status_code == 200\n
"},{"location":"02/#boas-praticas-para-constantes","title":"Boas pr\u00e1ticas para constantes","text":"

Quando comparamos valores, a boa pr\u00e1tica \u00e9 que eles nunca sejam expl\u00edcitos no c\u00f3digo como status_code == 200. Neste caso em espec\u00edfico \u00e9 f\u00e1cil saber o que 200 significa pois estamos estudando exatamente essa parte. Mas, em alguns momentos podemos nos deparar com c\u00f3digos que n\u00e3o sabemos o significado. Por exemplo um c\u00f3digo 209. O que ele significa?1

Quando trabalhamos com \"valores m\u00e1gicos\" no c\u00f3digo, a PEP-8 recomenda que criemos constantes que representem esses valores. Como por exemplo OK = 200.

A boa pr\u00e1tica para lidar com c\u00f3digos de status \u00e9 usar a classe http.HTTPStatus, que j\u00e1 mapeia todos os status code em um \u00fanico objeto. Fazendo que a compara\u00e7\u00e3o seja feita de forma simples, como:

tests/test_app.py
def test_root_deve_retornar_ok_e_ola_mundo():\n    client = TestClient(app)\n\n    response = client.get('/')\n\n    assert response.status_code == HTTPStatus.OK\n
"},{"location":"02/#o-lado-do-servidor","title":"O lado do servidor","text":"

Para garantir que a resposta obtida pelo cliente seja considerada um sucesso, o FastAPI, por padr\u00e3o, envia o c\u00f3digo de sucesso 200 para o m\u00e9todo GET. No entanto, tamb\u00e9m podemos deixar isso expl\u00edcito na defini\u00e7\u00e3o da rota:

Apenas o d\u00edgitoUsando HTTPStatus fast_zero/app.py
@app.get(\"/\", status_code=200)\ndef read_root():\n    return {'message': 'Ol\u00e1 Mundo!'}\n
fast_zero/app.py
@app.get(\"/\", status_code=HTTPStatus.OK)\ndef read_root():\n    return {'message': 'Ol\u00e1 Mundo!'}\n

Temos diversos c\u00f3digos a explorar durante nossa jornada, mas gostaria de listar os mais comuns dentro do nosso escopo:

Assim, podemos ir ao terceiro pilar do desenvolvimento web que s\u00e3o os conte\u00fados relacionados as respostas.

"},{"location":"02/#html","title":"HTML","text":"

Sobre o c\u00f3digo apresentado nesse t\u00f3pico!

Todo o c\u00f3digo apresentado neste t\u00f3pico \u00e9 apenas um exemplo b\u00e1sico do uso de HTML com FastAPI e n\u00e3o ser\u00e1 utilizado no curso. No entanto, \u00e9 extremamente importante mencionar este t\u00f3pico.

Embora este t\u00f3pico abranja apenas HTML puro, o FastAPI pode utilizar Jinja como sistema de templates para uma aplica\u00e7\u00e3o mais eficiente.

Interessado em aprender sobre a aplica\u00e7\u00e3o de templates?

Na live sobre websockets com FastAPI, discutimos bastante sobre templates. Voc\u00ea pode assistir ao v\u00eddeo aqui:

A documenta\u00e7\u00e3o do FastAPI tamb\u00e9m oferece um t\u00f3pico focado em Templates.

O terceiro pilar fundamental da web \u00e9 o HTML, sigla para Hypertext Markup Language. Trata-se da linguagem de marca\u00e7\u00e3o padr\u00e3o usada para criar e estruturar p\u00e1ginas na internet. Quando acessamos um site, o que vemos em nossos navegadores \u00e9 o resultado da interpreta\u00e7\u00e3o do HTML. Esta linguagem utiliza uma s\u00e9rie de 'tags' \u2013 como <html>, <head>, <body>, <h1>, <p> e outras \u2013 para definir a estrutura e o conte\u00fado de uma p\u00e1gina web.

A beleza do HTML reside em sua simplicidade e efic\u00e1cia. Mais do que uma linguagem, \u00e9 uma forma de organizar e apresentar informa\u00e7\u00f5es na web. Cada tag tem um prop\u00f3sito espec\u00edfico: <h1> a <h6> s\u00e3o usadas para t\u00edtulos e subt\u00edtulos; <p> para par\u00e1grafos; <a> para links; enquanto <div> e <span> auxiliam na organiza\u00e7\u00e3o e estilo do conte\u00fado. Juntas, essas tags formam a espinha dorsal de quase todas as p\u00e1ginas da internet.

Se nosso objetivo fosse apresentar um HTML simples, poder\u00edamos usar a classe de resposta HTMLResponse:

from fastapi import FastAPI\nfrom fastapi.responses import HTMLResponse\n\napp = FastAPI()\n\n\n@app.get('/', response_class=HTMLResponse)\ndef read_root():\n    return \"\"\"\n    <html>\n      <head>\n        <title> Nosso ol\u00e1 mundo!</title>\n      </head>\n      <body>\n        <h1> Ol\u00e1 Mundo </h1>\n      </body>\n    </html>\"\"\"\n

Ao acessarmos nossa URL no navegador, podemos ver o HTML sendo renderizado:

Embora o HTML seja crucial para a estrutura\u00e7\u00e3o de p\u00e1ginas web, nosso curso foca em uma perspectiva diferente: a transfer\u00eancia de dados. Enquanto o HTML \u00e9 usado para apresentar dados visualmente nos navegadores, existe outra camada focada na transfer\u00eancia de informa\u00e7\u00f5es entre sistemas e servidores. Aqui entra o conceito de APIs (Application Programming Interfaces), que frequentemente utilizam JSON (JavaScript Object Notation) para a troca de dados. JSON \u00e9 um formato leve de troca de dados, f\u00e1cil de ler e escrever para humanos, e simples de interpretar e gerar para m\u00e1quinas.

Portanto, embora n\u00e3o aprofundemos no HTML como linguagem, \u00e9 importante entender seu papel como a camada de apresenta\u00e7\u00e3o padr\u00e3o da web. Agora, direcionaremos nossa aten\u00e7\u00e3o para as APIs e a troca de dados em JSON, explorando como essas tecnologias permitem a comunica\u00e7\u00e3o eficiente entre diferentes sistemas e aplicativos.

"},{"location":"02/#apis","title":"APIs","text":"

Quando falamos sobre aplica\u00e7\u00f5es web que n\u00e3o envolvem uma camada de visualiza\u00e7\u00e3o, como HTML, geralmente estamos nos referindo a APIs. A sigla API vem de Application Programming Interface (Interface de Programa\u00e7\u00e3o de Aplica\u00e7\u00f5es). Uma API \u00e9 projetada para ser uma interface claramente definida e documentada, que facilita a intera\u00e7\u00e3o por meio do protocolo HTTP.

A ess\u00eancia das APIs reside no modelo cliente-servidor, onde o cliente troca dados com o servidor atrav\u00e9s de endpoints, respeitando as regras estabelecidas pelo protocolo HTTP. Por exemplo, para solicitar dados ao servidor, usamos o verbo GET, direcionando a requisi\u00e7\u00e3o a um endpoint espec\u00edfico do servidor, que em resposta nos fornece o dado ou recurso solicitado.

"},{"location":"02/#endpoint","title":"Endpoint","text":"

O termo \"endpoint\" refere-se a um ponto espec\u00edfico em uma API para onde as requisi\u00e7\u00f5es s\u00e3o enviadas. Basicamente, \u00e9 um endere\u00e7o na web (URL) onde o servidor ou a API est\u00e1 ativo e pronto para responder a requisi\u00e7\u00f5es dos clientes. Cada endpoint est\u00e1 associado a uma fun\u00e7\u00e3o espec\u00edfica da API, como recuperar dados, criar novos registros, atualizar ou deletar dados existentes.

A localiza\u00e7\u00e3o e estrutura de um endpoint, que incluem o caminho na URL e os m\u00e9todos HTTP permitidos, definem como os clientes devem formatar suas requisi\u00e7\u00f5es para serem compreendidas e processadas pelo servidor. Por exemplo, um endpoint para recuperar informa\u00e7\u00f5es de um usu\u00e1rio pode ter um endere\u00e7o como https://api.exemplo.com/usuarios/{id}, onde {id} \u00e9 o identificador \u00fanico do usu\u00e1rio desejado.

Atualmente, em nossa aplica\u00e7\u00e3o, temos apenas um endpoint dispon\u00edvel: o /. Vejamos o exemplo:

aula_00.py
@app.get('/')\ndef read_root():\n    return {'message': 'Ol\u00e1 Mundo!'}\n

Quando utilizamos o decorador @app.get('/'), estamos instruindo nossa API que, para chamadas de m\u00e9todo GET no endpoint /, a fun\u00e7\u00e3o read_root ser\u00e1 executada. O resultado dessa fun\u00e7\u00e3o, neste caso {'message': 'Ol\u00e1 Mundo!'}, \u00e9 o que ser\u00e1 retornado ao cliente.

"},{"location":"02/#documentacao","title":"Documenta\u00e7\u00e3o","text":"

Uma pergunta comum nesse est\u00e1gio \u00e9: \"Ok, mas como descobrir ou conhecer os endpoints dispon\u00edveis em uma API?\". A resposta reside na documenta\u00e7\u00e3o. Uma documenta\u00e7\u00e3o eficaz \u00e9 essencial em APIs, especialmente quando muitos clientes diferentes precisam se comunicar com o servidor. A melhor pr\u00e1tica \u00e9 fornecer uma documenta\u00e7\u00e3o detalhada, clara e acess\u00edvel sobre os endpoints dispon\u00edveis, incluindo informa\u00e7\u00f5es sobre o formato e a estrutura dos dados que podem ser enviados e recebidos.

A documenta\u00e7\u00e3o de uma API serve como um guia ou um manual, facilitando o entendimento e a utiliza\u00e7\u00e3o por desenvolvedores e usu\u00e1rios finais. Ela desempenha um papel crucial ao:

"},{"location":"02/#openapi-e-documentacao-automatica","title":"OpenAPI e documenta\u00e7\u00e3o autom\u00e1tica","text":"

Uma das solu\u00e7\u00f5es mais eficazes para a documenta\u00e7\u00e3o de APIs \u00e9 a utiliza\u00e7\u00e3o da especifica\u00e7\u00e3o OpenAPI, dispon\u00edvel em OpenAPI Specification. Essa especifica\u00e7\u00e3o fornece um padr\u00e3o robusto para descrever APIs, permitindo aos desenvolvedores criar documenta\u00e7\u00f5es precisas e test\u00e1veis de forma autom\u00e1tica. Esta abordagem n\u00e3o apenas simplifica o processo de documenta\u00e7\u00e3o, mas tamb\u00e9m garante que a documenta\u00e7\u00e3o seja consistentemente atualizada e precisa.

Para visualizar e interagir com essa documenta\u00e7\u00e3o, ferramentas como Swagger UI e Redoc s\u00e3o amplamente utilizadas. Elas transformam a especifica\u00e7\u00e3o OpenAPI em visualiza\u00e7\u00f5es interativas, fornecendo uma interface f\u00e1cil de navegar onde os usu\u00e1rios podem n\u00e3o apenas ler a documenta\u00e7\u00e3o, mas tamb\u00e9m experimentar a API diretamente na interface. Esta funcionalidade interativa \u00e9 fundamental para uma compreens\u00e3o pr\u00e1tica de como a API funciona, al\u00e9m de oferecer uma maneira eficiente de testar suas funcionalidades em tempo real.

No contexto do FastAPI, h\u00e1 suporte autom\u00e1tico tanto para Swagger UI quanto para Redoc. Para explorar a documenta\u00e7\u00e3o atual da nossa aplica\u00e7\u00e3o, basta iniciar o servidor com o seguinte comando:

$ Execu\u00e7\u00e3o no terminal!
task run\n
"},{"location":"02/#swagger-ui","title":"Swagger UI","text":"

Ao acessarmos o endere\u00e7o http://127.0.0.1:8000/docs, nos deparamos com a interface do Swagger UI:

Esta imagem nos d\u00e1 uma vis\u00e3o geral dos endpoints dispon\u00edveis na nossa aplica\u00e7\u00e3o, neste caso, o endpoint / que aceita o verbo HTTP GET. Ao explorar mais a fundo e clicar nesse m\u00e9todo:

Na documenta\u00e7\u00e3o, \u00e9 poss\u00edvel observar diversas informa\u00e7\u00f5es cruciais, como o c\u00f3digo de resposta 200, que indica sucesso, o tipo de dado retornado pelo cabe\u00e7alho (application/json) e um exemplo do valor de retorno. Contudo, a documenta\u00e7\u00e3o atual sugere, incorretamente, que o retorno \u00e9 uma string, quando, na verdade, nossa aplica\u00e7\u00e3o retorna um objeto JSON. Essa diferen\u00e7a ser\u00e1 abordada em breve.

Um aspecto interessante do Swagger UI \u00e9 a possibilidade de interagir diretamente com a API atrav\u00e9s da interface. Ao clicar em Try it out, um bot\u00e3o Execute se torna dispon\u00edvel:

Clicar em Execute faz do Swagger um cliente tempor\u00e1rio da nossa API, enviando uma requisi\u00e7\u00e3o ao servidor e exibindo a resposta:

A resposta ilustra como fazer a chamada usando o Curl, a URL utilizada, o c\u00f3digo de resposta 200, e detalhes da resposta do servidor, incluindo o corpo da mensagem (body) e os cabe\u00e7alhos (headers).

Caso queira saber mais sobre OpenAPI e Swagger

Temos uma live focada em OpenAPI, que s\u00e3o as especifica\u00e7\u00f5es do Swagger:

"},{"location":"02/#redoc","title":"Redoc","text":"

Assim como o Swagger UI, o Redoc \u00e9 outra ferramenta popular para visualizar a documenta\u00e7\u00e3o de APIs OpenAPI, mas com um foco em uma apresenta\u00e7\u00e3o mais limpa e leg\u00edvel. Para acessar a documenta\u00e7\u00e3o Redoc da nossa aplica\u00e7\u00e3o, podemos visitar o endere\u00e7o http://127.0.0.1:8000/redoc. O Redoc organiza a documenta\u00e7\u00e3o de uma maneira mais linear e de f\u00e1cil leitura, destacando as descri\u00e7\u00f5es dos endpoints, os m\u00e9todos HTTP dispon\u00edveis, os schemas dos dados de entrada e sa\u00edda, al\u00e9m de exemplos de requisi\u00e7\u00f5es e respostas.

"},{"location":"02/#trafegando-json","title":"Trafegando JSON","text":"

Quando discutimos APIs \"modernas\"2, nos referimos a APIs que priorizam o tr\u00e1fego de dados, deixando de lado a camada de apresenta\u00e7\u00e3o, como o HTML. O objetivo \u00e9 transmitir dados de forma agn\u00f3stica para diferentes tipos de clientes. Nesse contexto, o JSON (JavaScript Object Notation) se tornou a m\u00eddia padr\u00e3o, gra\u00e7as \u00e0 sua leveza e facilidade de leitura tanto por humanos quanto por m\u00e1quinas.

O JSON \u00e9 apreciado por sua simplicidade, apresentando dados em estruturas de documento chave-valor, onde os valores podem ser strings, n\u00fameros, booleanos, arrays, entre outros.

Abaixo, um exemplo ilustra o formato JSON:

Exemplo de um JSON
{\n    \"livros\": [\n        {\n            \"titulo\": \"O apanhador no campo de centeio\",\n            \"autor\": \"J.D. Salinger\",\n            \"ano\": 1945,\n            \"disponivel\": false\n        },\n        {\n            \"titulo\": \"O mestre e a margarida\",\n            \"autor\": \"Mikhail Bulg\u00e1kov\",\n            \"ano\": 1966,\n            \"disponivel\": true\n        }\n    ]\n}\n

Este exemplo demonstra como o JSON organiza dados de forma intuitiva e acess\u00edvel, tornando-o ideal para a comunica\u00e7\u00e3o de dados em uma ampla variedade de aplica\u00e7\u00f5es.

"},{"location":"02/#contratos-em-apis-json","title":"Contratos em APIs JSON","text":"

Quando falamos sobre o compartilhamento de JSON entre cliente e servidor, \u00e9 crucial estabelecer um entendimento m\u00fatuo sobre a estrutura dos dados que ser\u00e3o trocados. A este entendimento, denominamos schema, que atua como um contrato definindo a forma e o conte\u00fado dos dados trafegados.

O schema de uma API desempenha um papel fundamental ao assegurar que ambos, cliente e servidor, estejam alinhados quanto \u00e0 estrutura dos dados. Este \"contrato\" especifica:

Por exemplo, para nossa mensagem simples retornada por read_root ({'message': 'Ol\u00e1 mundo!'}), ter\u00edamos um schema assim:

{\n  \"type\": \"object\",\n  \"properties\": {\n    \"message\": {\n      \"type\": \"string\"\n    }\n  },\n  \"required\": [\"message\"]\n}\n

Onde estamos dizendo ao cliente que ao chamar a API, ser\u00e1 retornado um objeto, esse objeto cont\u00e9m a propriedade \"message\", a mensagem ser\u00e1 do tipo string. Ao final, vemos que o campo message \u00e9 requerido. Isso quer dizer que ele sempre ser\u00e1 enviado na resposta.

E em casos mais complicados, como ficaria o schema?

Para o exemplo que fizemos antes, sobre os livros, o schema seria assim:

{\n  \"type\": \"object\",\n  \"properties\": {\n    \"livros\": {\n      \"type\": \"array\",\n      \"items\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"titulo\": {\n            \"type\": \"string\"\n          },\n          \"autor\": {\n            \"type\": \"string\"\n          },\n          \"ano\": {\n            \"type\": \"integer\"\n          },\n          \"disponivel\": {\n            \"type\": \"boolean\"\n          }\n        },\n        \"required\": [\"titulo\", \"autor\", \"ano\", \"disponivel\"]\n      }\n    }\n  },\n  \"required\": [\"livros\"]\n}\n
"},{"location":"02/#pydantic","title":"Pydantic","text":"

No universo de APIs e contratos de dados, especialmente ao trabalhar com Python, o Pydantic se destaca como uma ferramenta poderosa e vers\u00e1til. Essa biblioteca, altamente integrada ao ecossistema Python, especializa-se na cria\u00e7\u00e3o de schemas de dados e na valida\u00e7\u00e3o de tipos. Com o Pydantic, \u00e9 poss\u00edvel expressar schemas JSON de maneira elegante e eficiente atrav\u00e9s de classes Python, proporcionando uma ponte robusta entre a flexibilidade do JSON e a seguran\u00e7a de tipos de Python.

Por exemplo, o schema JSON {'message': 'Ol\u00e1 mundo!'}. Com o Pydantic, podemos representar esse schema na forma de uma classe Python chamada Message. Isso \u00e9 feito de maneira intuitiva e direta:

fast_zero/schemas.py
from pydantic import BaseModel\n\n\nclass Message(BaseModel):\n    message: str\n

Para iniciar o desenvolvimento com schemas no contexto do FastAPI, podemos criar um arquivo chamado fast_zero/schemas.py e definir a classe Message. Vale ressaltar que o Pydantic \u00e9 uma depend\u00eancia integrada do FastAPI (n\u00e3o precisa ser instalado), refletindo a import\u00e2ncia dessa biblioteca no processo de valida\u00e7\u00e3o de dados e na gera\u00e7\u00e3o de documenta\u00e7\u00e3o autom\u00e1tica para APIs, como a documenta\u00e7\u00e3o OpenAPI.

"},{"location":"02/#integrando-o-pydantic-com-o-fastapi","title":"Integrando o Pydantic com o FastAPI","text":"

A integra\u00e7\u00e3o do modelo Pydantic (ou schema JSON) com o FastAPI \u00e9 feita ao especificar o modelo no campo response_model do decorador do endpoint. Isso garante que a resposta da API esteja alinhada com o schema definido, al\u00e9m de auxiliar na valida\u00e7\u00e3o dos dados retornados:

fast_zero/app.py
from http import HTTPStatus\n\nfrom fastapi import FastAPI\n\nfrom fast_zero.schemas import Message\n\napp = FastAPI()\n\n\n@app.get('/', status_code=HTTPStatus.OK, response_model=Message)\ndef read_root():\n    return {'message': 'Ol\u00e1 Mundo!'}\n

Com essa abordagem, ao iniciar o servidor (task run) e acessar a Swagger UI em http://127.0.0.1:8000/docs, observamos uma evolu\u00e7\u00e3o significativa na documenta\u00e7\u00e3o. Um novo campo Schemas \u00e9 exibido, destacando a estrutura do modelo Message que definimos:

Al\u00e9m disso, na se\u00e7\u00e3o de Responses, temos um exemplo claro da sa\u00edda esperada do endpoint: {\"message\": \"string\"}. Isso ilustra como a API ir\u00e1 responder, especificando que o campo obrigat\u00f3rio \"message\" ser\u00e1 retornado com um valor do tipo \"string\".

"},{"location":"02/#exercicio","title":"Exerc\u00edcio","text":"
  1. Crie um endpoint que retorne \"ol\u00e1 mundo\" usando HTML e escreva seu teste. Dica: para capturar a resposta do HTML do cliente de testes, voc\u00ea pode usar response.text

Exerc\u00edcios resolvidos

"},{"location":"02/#conclusao","title":"Conclus\u00e3o","text":"

Nesta aula, navegamos brevemente pelo vasto mundo do desenvolvimento web com foco em APIs, abra\u00e7ando desde os fundamentos da comunica\u00e7\u00e3o na web at\u00e9 as pr\u00e1ticas de troca de dados. Exploramos o modelo cliente-servidor, entendemos algumas das nuances das mensagens HTTP e tivemos uma introdu\u00e7\u00e3o sobre URLs e HTML. Embora o HTML desempenhe um papel central na camada de apresenta\u00e7\u00e3o, o nosso foco recaiu sobre as APIs, particularmente aquelas que trafegam JSON, um formato de dados.

Aprofundamos no uso de ferramentas e conceitos vitais para a cria\u00e7\u00e3o de APIs, como o FastAPI e o Pydantic, que juntos oferecem uma poderosa combina\u00e7\u00e3o para a valida\u00e7\u00e3o de dados e a gera\u00e7\u00e3o autom\u00e1tica de documenta\u00e7\u00e3o. A explora\u00e7\u00e3o do Swagger UI e do Redoc enriqueceu nosso entendimento sobre a import\u00e2ncia da documenta\u00e7\u00e3o acess\u00edvel e clara para APIs, facilitando tanto o desenvolvimento quanto a usabilidade.

Essa aula nos deu uma fundamenta\u00e7\u00e3o b\u00e1sica para avan\u00e7armos na constru\u00e7\u00e3o de APIs. Embora tenhamos exemplificado os conceitos com FastAPI, esses conceitos te\u00f3ricos podem ajudar voc\u00ea a desenvolver ferramentas web em qualquer tecnologia ou linguagem.

Nos vemos nas pr\u00f3ximas aulas para aplicar todos esses conceitos de forma mais aprofundada!

Agora que a aula acabou, \u00e9 um bom momento para voc\u00ea relembrar alguns conceitos e fixar melhor o conte\u00fado respondendo ao question\u00e1rio referente a ela.

Quiz

  1. O c\u00f3digo 209 n\u00e3o \u00e9 um status code v\u00e1lido no http, por isso o usei como exemplo.\u00a0\u21a9

  2. Apesar da no\u00e7\u00e3o comum de que APIs modernas s\u00e3o projetadas para trafegar JSON, existem debates intensos sobre as melhores pr\u00e1ticas para a transfer\u00eancia de dados em APIs. Uma leitura recomendada \u00e9 o livro hypermedia systems, que \u00e9 gratuito e oferece percep\u00e7\u00f5es valiosas.\u00a0\u21a9

"},{"location":"03/","title":"Estruturando o Projeto e Criando Rotas CRUD","text":""},{"location":"03/#estruturando-o-projeto-e-criando-rotas-crud","title":"Estruturando o Projeto e Criando Rotas CRUD","text":"

Objetivos dessa aula:

Caso prefira ver a aula em v\u00eddeo

Esse aula ainda n\u00e3o est\u00e1 dispon\u00edvel em formato de v\u00eddeo, somente em texto ou live!

Aula Slides C\u00f3digo Quiz Exerc\u00edcios

Boas-vindas de volta \u00e0 nossa s\u00e9rie de aulas sobre a constru\u00e7\u00e3o de uma aplica\u00e7\u00e3o utilizando FastAPI. Na \u00faltima aula, abordamos conceitos b\u00e1sicos do desenvolvimento web e finalizamos a configura\u00e7\u00e3o do nosso ambiente. Hoje, avan\u00e7aremos na estrutura\u00e7\u00e3o dos primeiros endpoints da nossa API, concentrando-nos nas quatro opera\u00e7\u00f5es fundamentais de CRUD - Criar, Ler, Atualizar e Deletar. Exploraremos como estas opera\u00e7\u00f5es se aplicam tanto \u00e0 comunica\u00e7\u00e3o web quanto \u00e0 intera\u00e7\u00e3o com o banco de dados.

O objetivo desta aula \u00e9 implementar um sistema de cadastro de usu\u00e1rios na nossa aplica\u00e7\u00e3o. Ao final, voc\u00ea conseguir\u00e1 cadastrar, listar, alterar e deletar usu\u00e1rios, al\u00e9m de realizar testes para validar estas funcionalidades.

Nota para pessoas mais experiente sobre essa aula

O princ\u00edpio por tr\u00e1s dessa aula \u00e9 demonstrar como construir os endpoints e os testes mais b\u00e1sicos poss\u00edveis.

Talvez lhe cause estranhamento o uso de um banco de dados em uma lista e os testes sendo constru\u00eddos a partir de efeitos colaterais. Mas o objetivo principal \u00e9 que as pessoas consigam se concentrar na cria\u00e7\u00e3o dos primeiros testes sem muito atrito.

Estas quest\u00f5es ser\u00e3o resolvidas nas aulas seguintes.

"},{"location":"03/#crud-e-http","title":"CRUD e HTTP","text":"

No desenvolvimento de APIs, existem quatro a\u00e7\u00f5es principais que fazemos com os dados: criar, ler, atualizar e excluir. Essas a\u00e7\u00f5es ajudam a gerenciar os dados no banco de dados e na aplica\u00e7\u00e3o web. Vamos nos focar nesse primeiro momento nas rela\u00e7\u00f5es entre os dados.

CRUD \u00e9 um acr\u00f4nimo que representa as quatro opera\u00e7\u00f5es b\u00e1sicas que voc\u00ea pode realizar em qualquer banco de dados persistente:

Com essas opera\u00e7\u00f5es podemos realizar qualquer tipo de comportamento em uma base dados. Podemos criar um registro, em seguida alter\u00e1-lo, quem sabe depois disso tudo delet\u00e1-lo.

Quando falamos de APIs servindo dados, todas essas opera\u00e7\u00f5es t\u00eam alguma forma similar no protocolo HTTP. O protocolo tem verbos para indicar essas mesmas a\u00e7\u00f5es que queremos representar no banco de dados.

Dessa forma podemos criar associa\u00e7\u00f5es entre os endpoints e a base de dados. Por exemplo: quando quisermos inserir um dado no banco de dados, n\u00f3s como clientes devemos comunicar essa inten\u00e7\u00e3o ao servidor usando o m\u00e9todo POST enviando os dados (em nosso caso no formato JSON) que devem ser persistidos na base de dados. Com isso iniciamos o processo de create na base de dados.

"},{"location":"03/#respostas-da-api","title":"Respostas da API","text":"

Usamos c\u00f3digos de status para informar ao cliente o resultado das opera\u00e7\u00f5es no servidor, como se um dado foi criado, encontrado, atualizado ou exclu\u00eddo com sucesso. Por isso investiremos mais algum momento aqui.

Os c\u00f3digos que devemos prestar aten\u00e7\u00e3o para responder corretamente as requisi\u00e7\u00f5es. Os casos de sucesso incluem:

Os c\u00f3digos de erro mais comuns que temos que conhecer para lidar com poss\u00edveis erros na aplica\u00e7\u00e3o, s\u00e3o:

Compreendendo esses c\u00f3digos, estamos prontos para iniciar a implementa\u00e7\u00e3o de alguns endpoints e colocar esses conceitos em pr\u00e1tica.

"},{"location":"03/#implementando-endpoints","title":"Implementando endpoints","text":"

Para facilitar o aprendizado, sugiro dividir a cria\u00e7\u00e3o de novos endpoints em tr\u00eas etapas:

  1. Rela\u00e7\u00e3o com o HTTP: Determinar o verbo HTTP esperado e os c\u00f3digos de resposta para situa\u00e7\u00f5es de sucesso e erro.
  2. Modelos de Dados: Definir o formato do JSON esperado, campos e seus tipos, e pensar nos modelos de resposta para situa\u00e7\u00f5es de sucesso e erro.
  3. Implementa\u00e7\u00e3o do Corpo: Decidir o tratamento dos dados recebidos e o tipo de processamento aplicado.

As duas primeiras etapas nos ajudam a definir a interface de comunica\u00e7\u00e3o e como ela ser\u00e1 documentada. A terceira etapa \u00e9 mais espec\u00edfica e envolve decis\u00f5es sobre a intera\u00e7\u00e3o com o banco de dados, valida\u00e7\u00f5es adicionais e a defini\u00e7\u00e3o do que constitui sucesso ou erro na requisi\u00e7\u00e3o.

Essas etapas nos orientam na implementa\u00e7\u00e3o completa do endpoint, garantindo que nada seja esquecido.

"},{"location":"03/#iniciando-a-implementacao-da-rota-post","title":"Iniciando a implementa\u00e7\u00e3o da rota POST","text":"

Nesta aula, nosso foco principal ser\u00e1 desenvolver um sistema de cadastro de usu\u00e1rios. Para isso, a implementa\u00e7\u00e3o de uma forma eficiente para criar novos usu\u00e1rios na base de dados \u00e9 essencial. Exploraremos como utilizar o verbo HTTP POST, fundamental para comunicar ao servi\u00e7o a nossa inten\u00e7\u00e3o de enviar novos dados, como no cadastro de usu\u00e1rios.

"},{"location":"03/#implementacao-do-endpoint","title":"Implementa\u00e7\u00e3o do endpoint","text":"

Para iniciar, criaremos um endpoint que aceita o verbo POST com dados em formato JSON. Esse endpoint responder\u00e1 com o status 201 em caso de sucesso na cria\u00e7\u00e3o do recurso. Com isso, estabelecemos a base para a nossa funcionalidade de cadastro.

Usaremos o decorador @app.post() do FastAPI para definir nosso endpoint, que come\u00e7ar\u00e1 com a URL /users/, indicando onde receberemos os dados para criar novos usu\u00e1rios:

fast_zero/app.py
@app.post('/users/')\ndef create_user():\n    ...\n
"},{"location":"03/#status-code-de-resposta","title":"Status code de resposta","text":"

\u00c9 crucial definir que, ao cadastrar um usu\u00e1rio com sucesso, o sistema deve retornar o c\u00f3digo de resposta 201 CREATED, indicando a cria\u00e7\u00e3o bem-sucedida do recurso. Para isso, adicionamos o par\u00e2metro status_code ao decorador:

fast_zero/app.py
@app.post('/users/', status_code=HTTPStatus.CREATED)\ndef create_user():\n    ...\n

Conversaremos em breve sobre os c\u00f3digos de resposta no t\u00f3pico do pydantic

"},{"location":"03/#modelo-de-dados","title":"Modelo de dados","text":"

O modelo de dados \u00e9 uma parte fundamental, onde consideramos tanto os dados recebidos do cliente quanto os dados que ser\u00e3o retornados a ele. Esta abordagem assegura uma comunica\u00e7\u00e3o eficaz e clara.

"},{"location":"03/#modelo-de-entrada-de-dados","title":"Modelo de entrada de dados","text":"

Para os dados de entrada, como estamos pensando em um cadastro de usu\u00e1rio na aplica\u00e7\u00e3o, \u00e9 importante que tenhamos insumos para identific\u00e1-lo como o email, uma senha (password) para que ele consiga fazer o login no futuro e seu nome de usu\u00e1rio (username). Dessa forma, podemos imaginar um modelo de entrada desta forma:

{\n    \"username\": \"joao123\",\n    \"email\": \"joao123@email.com\",\n    \"password\": \"segredo123\"\n}\n

Para a aplica\u00e7\u00e3o conseguir expor esse modelo na documenta\u00e7\u00e3o, devemos criar uma classe do pydantic em nosso arquivo de schemas (fast_zero/schemas.py) que represente esse schema:

fast_zero/schemas.py
class UserSchema(BaseModel):\n    username: str\n    email: str\n    password: str\n

Como j\u00e1 temos o endpoint definido, precisamos fazer a associa\u00e7\u00e3o do modelo com ele. Para fazer isso basta que o endpoint receba um par\u00e2metro e esse par\u00e2metro esteja associado a um modelo via anota\u00e7\u00e3o de par\u00e2metros:

fast_zero/app.py
from fast_zero.schemas import Message, UserSchema\n\n# ...\n\n@app.post('/users/', status_code=HTTPStatus.CREATED)\ndef create_user(user: UserSchema):\n    ...\n

Dessa forma, o modelo de entrada, o que o endpoint espera receber j\u00e1 est\u00e1 documentado e aparecer\u00e1 no swagger UI.

Para visualizar, temos que iniciar o servidor:

$ Execu\u00e7\u00e3o no terminal!
task run\n

E acessar a p\u00e1gina http://127.0.01:8000/docs. Isso nos mostrar\u00e1 as defini\u00e7\u00f5es do nosso endpoint usando o modelo no swagger:

"},{"location":"03/#modelo-de-saida-de-dados","title":"Modelo de sa\u00edda de dados","text":"

O modelo de sa\u00edda explica ao cliente quais dados ser\u00e3o retornados quando a chamada a esse endpoint for feita. Para a API ter um uso flu\u00eddo, temos que especificar o retorno corretamente na documenta\u00e7\u00e3o.

Se dermos uma olhada no estado atual de resposta da nossa API, podemos ver que a resposta no swagger \u00e9 \"string\" para o c\u00f3digo de resposta 201:

Quando fazemos uma chamada com o m\u00e9todo POST o esperado \u00e9 que os dados criados sejam retornados ao cliente. Poder\u00edamos usar o mesmo modelo de antes o UserSchema, por\u00e9m, por uma quest\u00e3o de seguran\u00e7a, seria ideal n\u00e3o retornar a senha do usu\u00e1rio. Quanto menos ela trafegar na rede, melhor.

Desta forma, podemos pensar no mesmo schema, por\u00e9m, sem a senha. Algo como:

{\n    \"username\": \"joao123\",\n    \"email\": \"joao123@email.com\"\n}\n

Transcrevendo isso em um modelo do pydantic em nosso arquivo de schemas (fast_zero/schemas.py) temos isso:

fast_zero/schemas.py
class UserPublic(BaseModel):\n    username: str\n    email: str\n

Para aplicar um modelo a resposta do endpoint, temos que passar o modelo ao par\u00e2metro response_model, como fizemos na aula passada:

fast_zero/app.py
from fast_zero.schemas import Message, UserPublic, UserSchema\n\n# C\u00f3digo omitido\n\n@app.post('/users/', status_code=HTTPStatus.CREATED, response_model=UserPublic)\ndef create_user(user: UserSchema):\n    ...\n

Tendo um modelo descritivo de resposta para o cliente na documenta\u00e7\u00e3o:

"},{"location":"03/#validacao-e-pydantic","title":"Valida\u00e7\u00e3o e pydantic","text":"

Um ponto crucial do Pydantic \u00e9 sua habilidade de checar se os dados est\u00e3o corretos enquanto o programa est\u00e1 rodando, garantindo que tudo esteja conforme esperado. Fazendo com que, caso o cliente envie um dado que n\u00e3o corresponde com o schema definido, seja levantado um erro 422. E caso a nossa resposta como servidor tamb\u00e9m n\u00e3o siga o schema, ser\u00e1 levantado um erro 500. Fazendo com que ele seja uma garantia de duas vias, nossa API segue a especifica\u00e7\u00e3o da documenta\u00e7\u00e3o.

Quando relacionamos um modelo \u00e0 resposta do enpoint, o Pydantic de forma autom\u00e1tica, cria um schema chamado HTTPValidationError:

Esse modelo \u00e9 usado quando o JSON enviado na requisi\u00e7\u00e3o n\u00e3o cumpre os requisitos do schema.

Por exemplo, se fizermos uma requisi\u00e7\u00e3o que foge dos padr\u00f5es definidos no schema:

Essa requisi\u00e7\u00e3o foge dos padr\u00f5es, pois n\u00e3o envia o campo password e envia o tipo de dado errado para email.

Com isso, receberemos um erro 422 UNPROCESSABLE ENTITY, dizendo que nosso schema foi violado e a resposta cont\u00e9m os detalhes dos campos faltantes ou mal formatados:

A mensagem completa de retorno do servidor mostra de forma detalhada os erros de valida\u00e7\u00e3o encontrados em cada campo, individualmente no campo details:

{\n  \"detail\": [\n    {\n      \"type\": \"string_type\", # (1)!\n      \"loc\": [\n        \"body\",\n        \"email\" # (2)!\n      ],\n      \"msg\": \"Input should be a valid string\", #(3)!\n      \"input\": 1,\n      \"url\": \"https://errors.pydantic.dev/2.5/v/string_type\"\n    },\n    {\n      \"type\": \"missing\", #(4)!\n      \"loc\": [\n        \"body\",\n        \"password\" #(5)!\n      ],\n      \"msg\": \"Field required\", #(6)!\n      \"input\": {\n        \"username\": \"string\",\n        \"email\": 1\n      },\n      \"url\": \"https://errors.pydantic.dev/2.5/v/missing\"\n    }\n  ]\n}\n
  1. Tipo de erro no campo: Era esperado que o valor fosse string (string_type)
  2. Campo notificado
  3. Mensagem que acompanha o erro
  4. Tipo de erro no campo: Era esperado que o valor fosse enviado (missing)
  5. Campo notificado
  6. Mensagem que acompanha o erro

Vemos que o pydantic desempenha um papel bastante importante no funcionamento da API. Pois ele consegue \"barrar\" o request antes dele ser exposto \u00e0 nossa fun\u00e7\u00e3o de endpoint. Evitando que diversos casos estranhos sejam cobertos de forma transparente. Tanto em rela\u00e7\u00e3o aos tipos dos campos, quanto em rela\u00e7\u00e3o aos campos que deveriam ser enviados, mas n\u00e3o foram.

"},{"location":"03/#estendendo-a-validacao-com-e-mail","title":"Estendendo a valida\u00e7\u00e3o com e-mail","text":"

Outro ponto que deve ser considerado \u00e9 a capacidade de estender os campos usados pelo pydantic nas anota\u00e7\u00f5es de tipo.

Para garantir que o campo email realmente contenha um e-mail v\u00e1lido, podemos usar uma ferramenta especial do Pydantic que verifica se o e-mail tem o formato correto, como a presen\u00e7a de @ e um dom\u00ednio v\u00e1lido.

Para isso, o pydantic tem um tipo de dado espec\u00edfico, o EmailStr. Que garante que o valor que ser\u00e1 recebido pelo schema, seja de fato um e-mail em formato v\u00e1lido. Podemos adicion\u00e1-lo ao campo email nos modelos UserSchema e UserPublic:

from pydantic import BaseModel, EmailStr\n\n# C\u00f3digo omitido\n\nclass UserSchema(BaseModel):\n    username: str\n    email: EmailStr\n    password: str\n\nclass UserPublic(BaseModel):\n    username: str\n    email: EmailStr\n

Embora essa seja um defini\u00e7\u00e3o valida do schema, o pydantic depende que a extens\u00e3o de emails seja instalada em nosso ambiente. Ao executar o servidor, podemos ver que o erro \u00e9 bastante descritivo:

File \"<...>/.venv/<...>/pydantic/networks.py\", line 354, in import_email_validator\n  raise ImportError('email-validator is not installed, run `pip install pydantic[email]`') from e\nImportError: email-validator is not installed, run `pip install pydantic[email]`\n

Traduzindo de forma literal, ter\u00edamos algo como: ImportError: email-validator n\u00e3o est\u00e1 instalado, rode 'pip install pydantic[email]'.

Como estamos usando o poetry como gerenciador de pacotes, podemos executar dessa forma:

$ Execu\u00e7\u00e3o no terminal!
poetry add \"pydantic[email]\"\n

Com isso, o pydantic ir\u00e1 oferecer um exemplo de email no swagger \"user@example.com\" e acerta os schemas para fazer essas valida\u00e7\u00f5es:

Dessa forma, o campo esperar\u00e1 n\u00e3o somente uma string como antes, mas um endere\u00e7o de email v\u00e1lido.

"},{"location":"03/#validacao-da-resposta","title":"Valida\u00e7\u00e3o da resposta","text":"

Ap\u00f3s aperfei\u00e7oarmos nossos modelos do Pydantic para garantir que os dados de entrada e sa\u00edda estejam corretamente validados, chegamos a um ponto crucial: a implementa\u00e7\u00e3o do corpo do nosso endpoint. At\u00e9 agora, nosso endpoint est\u00e1 definido, mas sem uma l\u00f3gica de processamento real, conforme mostrado abaixo:

fast_zero/app.py
@app.post('/users/', status_code=HTTPStatus.CREATED, response_model=UserPublic)\ndef create_user(user: UserSchema):\n    ...\n

Este \u00e9 o momento perfeito para realizar um request e observar diretamente a atua\u00e7\u00e3o do Pydantic na valida\u00e7\u00e3o da resposta. Ao tentarmos executar um request v\u00e1lido, sem a implementa\u00e7\u00e3o adequada do endpoint, nos deparamos com uma situa\u00e7\u00e3o interessante: o Pydantic tenta validar a resposta que o nosso endpoint deveria fornecer, mas, como ainda n\u00e3o implementamos essa l\u00f3gica, o resultado n\u00e3o atende ao schema definido.

A tentativa resulta em uma mensagem de erro exibida diretamente no Swagger, indicando um erro de servidor interno (HTTP 500). Esse tipo de erro sugere que algo deu errado no lado do servidor, mas n\u00e3o oferece detalhes espec\u00edficos sobre a natureza do problema para o cliente. O erro 500 \u00e9 uma resposta gen\u00e9rica para indicar falhas no servidor.

Para investigar a causa exata do erro 500, \u00e9 necess\u00e1rio consultar o console ou os logs de nossa aplica\u00e7\u00e3o, onde os detalhes do erro s\u00e3o registrados. Neste caso, o erro apresentado nos logs \u00e9 o seguinte:

raise ResponseValidationError( # (1)!\nfastapi.exceptions.ResponseValidationError: 1 validation errors:\n{\n  \"type\":\"model_attributes_type\",\n  \"loc\": (\"response\"),\n  \"msg\":\"Input should be a valid dictionary or object to extract fields from\",#(2)!\n  \"input\":\"None\", #(3)!\n  \"url\":\"https://errors.pydantic.dev/2.6/v/model_attributes_type\"\n}\n
  1. O json dessa mensagem de erro aparece em uma \u00fanica linha. Eu fiz a quebra para que ficasse mais f\u00e1cil de lermos a resposta.
  2. A mensagem de erro diz que o input passado ao modelo do pydantic deveria ser um dicion\u00e1rio v\u00e1lido ou um objeto onde os campos poderiam ser extra\u00eddos.
  3. O objeto enviado ao modelo

Essencialmente, o erro nos informa que o modelo esperava receber um objeto v\u00e1lido para processamento, mas, em vez disso, recebeu None. Isso ocorre porque ainda n\u00e3o implementamos a l\u00f3gica para processar o input recebido e retornar uma resposta adequada que corresponda ao modelo UserPublic.

Agora, tendo identificado a capacidade do Pydantic em validar as respostas e encontrarmos um erro devido \u00e0 falta de implementa\u00e7\u00e3o, podemos proceder com uma solu\u00e7\u00e3o simples. Para come\u00e7ar, podemos utilizar diretamente os dados recebidos em user e retorn\u00e1-los. Esta a\u00e7\u00e3o simples j\u00e1 \u00e9 suficiente para satisfazer o schema, pois o objeto user cont\u00e9m os atributos email e username, esperados pelo modelo UserPublic:

fast_zero/app.py
@app.post('/users/', status_code=HTTPStatus.CREATED, response_model=UserPublic)\ndef create_user(user: UserSchema):\n    return user\n

Este retorno simples do objeto user garante que o schema seja cumprido. Agora, ao realizarmos novamente a chamada no Swagger, o objeto que enviamos \u00e9 retornado conforme esperado, mas sem expor a senha, alinhado ao modelo UserPublic e emitindo uma resposta com o c\u00f3digo 201:

Essa abordagem nos permite fechar o ciclo de valida\u00e7\u00e3o, demonstrando a efic\u00e1cia do Pydantic na garantia de que os dados de resposta estejam corretos. Com essa implementa\u00e7\u00e3o simples, estabelecemos a base para o desenvolvimento real do c\u00f3digo do endpoint POST, preparando o terreno para uma l\u00f3gica mais complexa que envolver\u00e1 a cria\u00e7\u00e3o e o manejo de usu\u00e1rios dentro de nossa aplica\u00e7\u00e3o.

"},{"location":"03/#de-volta-ao-post","title":"De volta ao POST","text":"

Agora que j\u00e1 dominamos a defini\u00e7\u00e3o dos modelos, podemos prosseguir com a aula e a implementa\u00e7\u00e3o dos endpoints. Vamos retomar a implementa\u00e7\u00e3o do POST, adicionando um banco de dados falso/simulado em mem\u00f3ria. Isso nos permitir\u00e1 explorar as opera\u00e7\u00f5es do CRUD sem a complexidade da implementa\u00e7\u00e3o de um banco de dados real, facilitando a assimila\u00e7\u00e3o dos muitos conceitos discutidos nesta aula.

"},{"location":"03/#criando-um-banco-de-dados-falso","title":"Criando um banco de dados falso","text":"

Para interagir com essas rotas de maneira pr\u00e1tica, vamos criar uma lista provis\u00f3ria que simular\u00e1 um banco de dados. Isso nos permitir\u00e1 adicionar dados e entender o funcionamento do FastAPI. Portanto, introduzimos uma lista provis\u00f3ria para atuar como nosso \"banco\" e modificamos nosso endpoint para inserir nossos modelos do Pydantic nessa lista:

fast_zero/app.py
from fast_zero.schemas import Message, UserDB, UserPublic, UserSchema\n\n# c\u00f3digo omitido\n\ndatabase = []  #(1)!\n\n# c\u00f3digo omitido\n\n@app.post('/users/', status_code=HTTPStatus.CREATED, response_model=UserPublic)\ndef create_user(user: UserSchema):\n    user_with_id = UserDB(**user.model_dump(), id=len(database) + 1)  #(2)!\n\n    database.append(user_with_id)\n\n    return user_with_id\n
  1. A lista database \u00e9 provis\u00f3ria. Ela est\u00e1 aqui s\u00f3 para entendermos os conceitos de crud, nas pr\u00f3ximas aulas vamos trabalhar com a cria\u00e7\u00e3o do banco de dados definitivo.

  2. .model_dump() \u00e9 um m\u00e9todo de modelos do pydantic que converte o objeto em dicion\u00e1rio. Por exemplo, user.model_dump() faria a convers\u00e3o em {'username': 'nome do usu\u00e1rio', 'password': 'senha do usu\u00e1rio', 'email': 'email do usu\u00e1rio'}. Os ** querem dizer que o dicion\u00e1rio ser\u00e1 desempacotado em par\u00e2metros. Fazendo com que a chamada seja equivalente a UserDB(username='nome do usu\u00e1rio', password='senha do usu\u00e1rio', email='email do usu\u00e1rio', id=len(database) + 1)

Para simular um banco de dados de forma mais realista, \u00e9 essencial que cada usu\u00e1rio tenha um ID \u00fanico. Portanto, ajustamos nosso modelo de resposta p\u00fablica (UserPublic) para incluir o ID do usu\u00e1rio. Tamb\u00e9m introduzimos um novo modelo, UserDB, que inclui tanto a senha do usu\u00e1rio quanto seu identificador \u00fanico:

fast_zero/schemas.py
class UserPublic(BaseModel):\n    id: int\n    username: str\n    email: EmailStr\n\n\nclass UserDB(UserSchema):\n    id: int\n

Essa abordagem simples nos permite avan\u00e7ar na constru\u00e7\u00e3o dos outros endpoints. \u00c9 crucial testar esse endpoint para assegurar seu correto funcionamento.

"},{"location":"03/#implementando-o-teste-da-rota-post","title":"Implementando o teste da rota POST","text":"

Antes de prosseguir, vamos verificar a cobertura de nossos testes:

$ Execu\u00e7\u00e3o no terminal!
task test\n\n# parte da resposta foi omitida\n\n---------- coverage: platform linux, python 3.11.3-final-0 -----------\nName                    Stmts   Miss  Cover\n-------------------------------------------\nfast_zero/__init__.py       0      0   100%\nfast_zero/app.py           12      3    75%\nfast_zero/schemas.py       11      0   100%\n-------------------------------------------\nTOTAL                      23      3    87%\n\n# parte da resposta foi omitida\n

Vemos que temos 3 Miss. Possivelmente das linhas que acabamos de escrever. Podemos olhar o HTML do coverage para ter certeza:

Ent\u00e3o, vamos escrever nosso teste. Esse teste para a rota POST precisa verificar se a cria\u00e7\u00e3o de um novo usu\u00e1rio funciona corretamente. Enviamos uma solicita\u00e7\u00e3o POST com um novo usu\u00e1rio para a rota /users/. Em seguida, verificamos se a resposta tem o status HTTP 201 (Criado) e se a resposta cont\u00e9m o novo usu\u00e1rio criado.

tests/test_app.py
def test_create_user():\n    client = TestClient(app)\n\n    response = client.post(\n        '/users/',\n        json={\n            'username': 'alice',\n            'email': 'alice@example.com',\n            'password': 'secret',\n        },\n    )\n    assert response.status_code == HTTPStatus.CREATED\n    assert response.json() == {\n        'username': 'alice',\n        'email': 'alice@example.com',\n        'id': 1,\n    }\n

Ao executar o teste:

$ Execu\u00e7\u00e3o no terminal!
task test\n\n# parte da resposta foi omitida\n\ntests/test_app.py::test_root_deve_retornar_ok_e_ola_mundo PASSED\ntests/test_app.py::test_create_user PASSED\n\n---------- coverage: platform linux, python 3.11.3-final-0 -----------\nName                    Stmts   Miss  Cover\n-------------------------------------------\nfast_zero/__init__.py       0      0   100%\nfast_zero/app.py           12      0   100%\nfast_zero/schemas.py       11      0   100%\n-------------------------------------------\nTOTAL                      23      0   100%\n\n# parte da resposta foi omitida\n
"},{"location":"03/#nao-se-repita-dry","title":"N\u00e3o se repita (DRY)","text":"

Voc\u00ea deve ter notado que a linha client = TestClient(app) est\u00e1 repetida na primeira linha dos dois testes que fizemos. Repetir c\u00f3digo pode tornar o gerenciamento de testes mais complexo \u00e0 medida que cresce, e \u00e9 aqui que o princ\u00edpio de \"N\u00e3o se repita\" (DRY) entra em jogo. DRY incentiva a redu\u00e7\u00e3o da repeti\u00e7\u00e3o, criando um c\u00f3digo mais limpo e manuten\u00edvel.

Para solucionar essa repeti\u00e7\u00e3o, podemos usar uma funcionalidade do pytest chamada Fixture. Uma fixture \u00e9 como uma fun\u00e7\u00e3o que prepara dados ou estado necess\u00e1rios para o teste. Pode ser pensada como uma forma de n\u00e3o repetir a fase de Arrange de um teste, simplificando a chamada e n\u00e3o repetindo c\u00f3digo.

Se fixtures s\u00e3o uma novidade para voc\u00ea

Existe uma live de Python onde discutimos especificamente sobre fixtures

Link direto

Neste caso, criaremos uma fixture que retorna nosso client. Para fazer isso, precisamos criar o arquivo tests/conftest.py. O arquivo conftest.py \u00e9 um arquivo especial reconhecido pelo pytest que permite definir fixtures que podem ser reutilizadas em diferentes m\u00f3dulos de teste em um projeto. \u00c9 uma forma de centralizar recursos comuns de teste.

tests/conftest.py
import pytest\nfrom fastapi.testclient import TestClient\n\nfrom fast_zero.app import app\n\n\n@pytest.fixture\ndef client():\n    return TestClient(app)\n

Agora, em vez de repetir a cria\u00e7\u00e3o do client em cada teste, podemos simplesmente passar a fixture como um argumento nos nossos testes:

tests/test_app.py
from http import HTTPStatus\n\n\ndef test_root_deve_retornar_ok_e_ola_mundo(client):\n    response = client.get('/')\n\n    assert response.status_code == HTTPStatus.OK\n    assert response.json() == {'message': 'Ol\u00e1 Mundo!'}\n\n\ndef test_create_user(client):\n    response = client.post(\n        '/users/',\n        json={\n            'username': 'alice',\n            'email': 'alice@example.com',\n            'password': 'secret',\n        },\n    )\n    assert response.status_code == HTTPStatus.CREATED\n    assert response.json() == {\n        'username': 'alice',\n        'email': 'alice@example.com',\n        'id': 1,\n    }\n

Com essa simples mudan\u00e7a, conseguimos tornar nosso c\u00f3digo mais limpo e f\u00e1cil de manter, seguindo o princ\u00edpio DRY.

Vemos que estamos no caminho certo. Agora que a rota POST est\u00e1 implementada, seguiremos para a pr\u00f3xima opera\u00e7\u00e3o CRUD: Read.

"},{"location":"03/#implementando-a-rota-get","title":"Implementando a Rota GET","text":"

A rota GET \u00e9 usada para recuperar informa\u00e7\u00f5es de um ou mais usu\u00e1rios do nosso sistema. No contexto do CRUD, o verbo HTTP GET est\u00e1 associado \u00e0 opera\u00e7\u00e3o \"Read\". Se a solicita\u00e7\u00e3o for bem-sucedida, a rota deve retornar o status HTTP 200 (OK).

Para estruturar a resposta dessa rota, podemos criar um novo modelo chamado UserList. Este modelo representar\u00e1 uma lista de usu\u00e1rios e cont\u00e9m apenas um campo chamado users, que \u00e9 uma lista de UserPublic. Isso nos permite retornar m\u00faltiplos usu\u00e1rios de uma vez.

fast_zero/schemas.py
class UserList(BaseModel):\n    users: list[UserPublic]\n

Com esse modelo definido, podemos criar nosso endpoint GET. Este endpoint retornar\u00e1 uma inst\u00e2ncia de UserList, que por sua vez cont\u00e9m uma lista de UserPublic. Cada UserPublic \u00e9 criado a partir dos dados de um usu\u00e1rio em nosso banco de dados fict\u00edcio.

fast_zero/app.py
from fast_zero.schemas import Message, UserDB, UserList, UserPublic, UserSchema\n\n# c\u00f3digo omitido\n\n@app.get('/users/', response_model=UserList)\ndef read_users():\n    return {'users': database}\n

Com essa implementa\u00e7\u00e3o, nossa API agora pode retornar uma lista de usu\u00e1rios. No entanto, nosso trabalho ainda n\u00e3o acabou. A pr\u00f3xima etapa \u00e9 escrever testes para garantir que nossa rota GET est\u00e1 funcionando corretamente. Isso nos ajudar\u00e1 a identificar e corrigir quaisquer problemas antes de prosseguirmos com a implementa\u00e7\u00e3o de outras rotas.

"},{"location":"03/#implementando-o-teste-da-rota-de-get","title":"Implementando o teste da rota de GET","text":"

Nosso teste da rota GET tem que verificar se a recupera\u00e7\u00e3o dos usu\u00e1rios est\u00e1 funcionando corretamente. Enviamos uma solicita\u00e7\u00e3o GET para a rota /users/. Em seguida, verificamos se a resposta tem o status HTTP 200 (OK) e se a resposta cont\u00e9m a lista de usu\u00e1rios.

tests/test_app.py
def test_read_users(client):\n    response = client.get('/users/')\n    assert response.status_code == HTTPStatus.OK\n    assert response.json() == {\n        'users': [\n            {\n                'username': 'alice',\n                'email': 'alice@example.com',\n                'id': 1,\n            }\n        ]\n    }\n

Com as rotas POST e GET implementadas, agora podemos criar e recuperar usu\u00e1rios. Implementaremos a pr\u00f3xima opera\u00e7\u00e3o CRUD: Update.

Coisas que devemos considerar sobre este e os pr\u00f3ximos testes

Note que para que esse teste seja executado com sucesso o teste do endpoint de POST tem que ser executado antes. Isso \u00e9 problem\u00e1tico no mundo dos testes. Pois cada teste deve estar isolado e n\u00e3o depender da execu\u00e7\u00e3o de nada externo a ele.

Para que isso aconte\u00e7a, precisaremos de um mecanismo que reinicie o banco de dados a cada teste, mas ainda n\u00e3o temos um banco de dados real. O banco de dados ser\u00e1 introduzido na aplica\u00e7\u00e3o na aula 04.

O mecanismo que far\u00e1 com que os testes n\u00e3o interfiram em outros e sejam independentes ser\u00e1 introduzido na aula 05.

"},{"location":"03/#implementando-a-rota-put","title":"Implementando a Rota PUT","text":"

A rota PUT \u00e9 usada para atualizar as informa\u00e7\u00f5es de um usu\u00e1rio existente. No contexto do CRUD, o verbo HTTP PUT est\u00e1 associado \u00e0 opera\u00e7\u00e3o \"Update\". Se a solicita\u00e7\u00e3o for bem-sucedida, a rota deve retornar o status HTTP 200 (OK). No entanto, se o usu\u00e1rio solicitado n\u00e3o for encontrado, dever\u00edamos retornar o status HTTP 404 (N\u00e3o Encontrado).

Uma caracter\u00edstica importante do verbo PUT \u00e9 que ele \u00e9 direcionado a um recurso em espec\u00edfico. Nesse caso, estamos direcionando a altera\u00e7\u00e3o a um user em espec\u00edfico na base de dados. O identificador de user \u00e9 o campo id que estamos usando nos modelos do Pydantic. Nesse caso, nosso endpoint deve receber o identificador de quem ser\u00e1 alterado.

Para fazer essa identifica\u00e7\u00e3o do recurso na URL usamos a seguinte combina\u00e7\u00e3o /caminho/recurso. Mas, como o recurso \u00e9 din\u00e2mico, ele deve ser enviado pelo cliente. Fazendo com que o valor tenha que ser uma vari\u00e1vel. Dentro do FastAPI, as vari\u00e1veis de recursos s\u00e3o descritas dentro de {}, como {user_id}. Fazendo com que o caminho completo do nosso endpoint seja '/users/{user_id}'. Da seguinte forma:

fast_zero/app.py
from fastapi import FastAPI, HTTPException\n\n# ...\n\n@app.put('/users/{user_id}', response_model=UserPublic)\ndef update_user(user_id: int, user: UserSchema):\n    if user_id > len(database) or user_id < 1: #(1)!\n        raise HTTPException(\n            status_code=HTTPStatus.NOT_FOUND, detail='User not found'\n        ) #(2)!\n\n    user_with_id = UserDB(**user.model_dump(), id=user_id)\n    database[user_id - 1] = user_with_id #(3)!\n\n    return user_with_id\n
  1. Aqui fazemos uma valida\u00e7\u00e3o simples, verificamos se o id n\u00e3o \u00e9 maior que o tamanho da lista user_id > len(database) e o n\u00famero enviado para o user_id \u00e9 um valor positivo user_id < 1.
  2. Levantamos um erro do tipo HTTPException para dizer que o usu\u00e1rio n\u00e3o existe na base de dados. Esse modelo j\u00e1 est\u00e1 presente no swagger com HTTPValidationError.
  3. Nessa linha, estamos substituindo a posi\u00e7\u00e3o (user_id - 1) na lista pelo novo objeto.

Para que essa vari\u00e1vel definida na URL seja transferida para nosso endpoint, devemos adicionar um par\u00e2metro na fun\u00e7\u00e3o com o mesmo nome da vari\u00e1vel definida. Como def update_user(user_id: int) na linha em destaque.

"},{"location":"03/#implementando-o-teste-da-rota-de-put","title":"Implementando o teste da rota de PUT","text":"

Nosso teste da rota PUT precisa verificar se a atualiza\u00e7\u00e3o de um usu\u00e1rio existente funciona corretamente. Enviamos uma solicita\u00e7\u00e3o PUT com as novas informa\u00e7\u00f5es do usu\u00e1rio para a rota /users/{user_id}. Em seguida, verificamos se a resposta tem o status HTTP 200 (OK) e se a resposta cont\u00e9m o usu\u00e1rio atualizado.

tests/test_app.py
def test_update_user(client):\n    response = client.put(\n        '/users/1',\n        json={\n            'username': 'bob',\n            'email': 'bob@example.com',\n            'password': 'mynewpassword',\n        },\n    )\n    assert response.status_code == HTTPStatus.OK\n    assert response.json() == {\n        'username': 'bob',\n        'email': 'bob@example.com',\n        'id': 1,\n    }\n

Com as rotas POST, GET e PUT implementadas, agora podemos criar, recuperar e atualizar usu\u00e1rios. A \u00faltima opera\u00e7\u00e3o CRUD que precisamos implementar \u00e9 Delete.

"},{"location":"03/#implementando-a-rota-delete","title":"Implementando a Rota DELETE","text":"

A rota DELETE \u00e9 usada para excluir um usu\u00e1rio do nosso sistema. No contexto do CRUD, o verbo HTTP DELETE est\u00e1 associado \u00e0 opera\u00e7\u00e3o \"Delete\". Se a solicita\u00e7\u00e3o for bem-sucedida, a rota deve retornar o status HTTP 200 (OK). No entanto, se o usu\u00e1rio solicitado n\u00e3o for encontrado, dever\u00edamos retornar o status HTTP 404 (N\u00e3o Encontrado).

Este endpoint receber\u00e1 o ID do usu\u00e1rio que queremos excluir. Note que, estamos lan\u00e7ando uma exce\u00e7\u00e3o HTTP quando o ID do usu\u00e1rio est\u00e1 fora do range da nossa lista (simula\u00e7\u00e3o do nosso banco de dados). Quando conseguimos excluir o usu\u00e1rio com sucesso, retornamos a mensagem de sucesso em um modelo do tipo Message.

fast_zero/app.py
@app.delete('/users/{user_id}', response_model=Message)\ndef delete_user(user_id: int):\n    if user_id > len(database) or user_id < 1:\n        raise HTTPException(\n            status_code=HTTPStatus.NOT_FOUND, detail='User not found'\n        )\n\n    del database[user_id - 1]\n\n    return {'message': 'User deleted'}\n

Com a implementa\u00e7\u00e3o da rota DELETE conclu\u00edda, \u00e9 fundamental garantirmos que essa rota est\u00e1 funcionando conforme o esperado. Para isso, precisamos escrever testes para essa rota.

"},{"location":"03/#implementando-o-teste-da-rota-de-delete","title":"Implementando o teste da rota de DELETE","text":"

Nosso teste da rota DELETE precisa verificar se a exclus\u00e3o de um usu\u00e1rio existente funciona corretamente. Enviamos uma solicita\u00e7\u00e3o DELETE para a rota /users/{user_id}. Em seguida, verificamos se a resposta tem o status HTTP 200 (OK) e se a resposta cont\u00e9m uma mensagem informando que o usu\u00e1rio foi exclu\u00eddo.

tests/test_app.py
def test_delete_user(client):\n    response = client.delete('/users/1')\n\n    assert response.status_code == HTTPStatus.OK\n    assert response.json() == {'message': 'User deleted'}\n
"},{"location":"03/#checando-tudo-antes-do-commit","title":"Checando tudo antes do commit","text":"

Antes de fazermos o commit, \u00e9 uma boa pr\u00e1tica checarmos todo o c\u00f3digo, e podemos fazer isso com as a\u00e7\u00f5es que criamos com o taskipy.

$ Execu\u00e7\u00e3o no terminal!
$ task lint\n
$ Execu\u00e7\u00e3o no terminal!
$ task format\n6 files left unchanged\n
$ Execu\u00e7\u00e3o no terminal!
$ task test\n...\ntests/test_app.py::test_root_deve_retornar_ok_e_ola_mundo PASSED\ntests/test_app.py::test_create_user PASSED\ntests/test_app.py::test_read_users PASSED\ntests/test_app.py::test_update_user PASSED\ntests/test_app.py::test_delete_user PASSED\n\n---------- coverage: platform linux, python 3.11.4-final-0 -----------\nName                   Stmts   Miss  Cover\n------------------------------------------\nfastzero/__init__.py       0      0   100%\nfastzero/app.py           28      2    93%\nfastzero/schemas.py       15      0   100%\n------------------------------------------\nTOTAL                     43      2    95%\n\n\n============================ 5 passed in 1.48s =============================\nWrote HTML report to htmlcov/index.html\n
"},{"location":"03/#commit","title":"Commit","text":"

Ap\u00f3s toda essa jornada de aprendizado, constru\u00e7\u00e3o e teste de rotas, chegou a hora de registrar nosso progresso utilizando o git. Fazer commits regulares \u00e9 uma boa pr\u00e1tica, pois mant\u00e9m um hist\u00f3rico detalhado das altera\u00e7\u00f5es e facilita a volta a uma vers\u00e3o anterior do c\u00f3digo, se necess\u00e1rio.

Primeiramente, verificaremos as altera\u00e7\u00f5es feitas no projeto com o comando git status. Este comando nos mostrar\u00e1 todos os arquivos modificados que ainda n\u00e3o foram inclu\u00eddos em um commit.

$ Execu\u00e7\u00e3o no terminal!
git status\n

Em seguida, adicionaremos todas as altera\u00e7\u00f5es para o pr\u00f3ximo commit. O comando git add . adiciona todas as altera\u00e7\u00f5es feitas em todos os arquivos do projeto.

$ Execu\u00e7\u00e3o no terminal!
git add .\n

Agora, estamos prontos para fazer o commit. Com o comando git commit, criamos uma nova entrada no hist\u00f3rico do nosso projeto. \u00c9 importante adicionar uma mensagem descritiva ao commit, para que, no futuro, outras pessoas ou n\u00f3s mesmos entendamos o que foi alterado. Nesse caso, a mensagem do commit poderia ser \"Implementando rotas CRUD\".

$ Execu\u00e7\u00e3o no terminal!
git commit -m \"Implementando rotas CRUD\"\n

Por fim, enviamos nossas altera\u00e7\u00f5es para o reposit\u00f3rio remoto com git push. Se voc\u00ea tiver v\u00e1rias branches, certifique-se de estar na branch correta antes de executar este comando.

$ Execu\u00e7\u00e3o no terminal!
git push\n

E pronto! As altera\u00e7\u00f5es est\u00e3o seguras no hist\u00f3rico do git, e podemos continuar com o pr\u00f3ximo passo do projeto.

"},{"location":"03/#exercicios","title":"Exerc\u00edcios","text":"
  1. Escrever um teste para o erro de 404 (NOT FOUND) para o endpoint de PUT;
  2. Escrever um teste para o erro de 404 (NOT FOUND) para o endpoint de DELETE;
  3. Criar um endpoint de GET para pegar um \u00fanico recurso como users/{id} e fazer seus testes para 200 e 404.

Exerc\u00edcios resolvidos

"},{"location":"03/#conclusao","title":"Conclus\u00e3o","text":"

Com a implementa\u00e7\u00e3o bem-sucedida das rotas CRUD, demos um passo significativo na constru\u00e7\u00e3o de uma funcional com FastAPI. Agora podemos manipular usu\u00e1rios - criar, ler, atualizar e excluir - o que \u00e9 fundamental para muitos sistemas de informa\u00e7\u00e3o.

O papel dos testes em cada etapa n\u00e3o pode ser subestimado. Testes n\u00e3o apenas nos ajudam a assegurar que nosso c\u00f3digo est\u00e1 funcionando como esperado, mas tamb\u00e9m nos permitem refinar nossas solu\u00e7\u00f5es e detectar problemas potenciais antes que eles afetem a funcionalidade geral do nosso sistema. Nunca subestime a import\u00e2ncia de executar seus testes sempre que fizer uma altera\u00e7\u00e3o em seu c\u00f3digo!

At\u00e9 aqui, no entanto, trabalhamos com um \"banco de dados\" provis\u00f3rio, na forma de uma lista Python, que \u00e9 vol\u00e1til e n\u00e3o persiste os dados de uma execu\u00e7\u00e3o do aplicativo para outra. Para nosso aplicativo ser \u00fatil em um cen\u00e1rio do mundo real, precisamos armazenar nossos dados de forma mais duradoura. \u00c9 a\u00ed que os bancos de dados entram.

Outro ponto que deve ser destacado \u00e9 que nossas implementa\u00e7\u00f5es de testes sofrem interfer\u00eancia dos testes anteriores. Testes devem funcionar de forma isolada, sem a depend\u00eancia de um teste anterior. Vamos ajustar isso no futuro.

No pr\u00f3ximo t\u00f3pico, exploraremos uma das partes mais cr\u00edticas de qualquer aplicativo - a conex\u00e3o e intera\u00e7\u00e3o com um banco de dados. Aprenderemos a integrar nosso aplicativo FastAPI com um banco de dados real, permitindo a persist\u00eancia de nossos dados de usu\u00e1rio entre as sess\u00f5es do aplicativo.

Agora que a aula acabou, \u00e9 um bom momento para voc\u00ea relembrar alguns conceitos e fixar melhor o conte\u00fado respondendo ao question\u00e1rio referente a ela.

Quiz

"},{"location":"04/","title":"Configurando o banco de dados e gerenciando migra\u00e7\u00f5es com Alembic","text":""},{"location":"04/#configurando-o-banco-de-dados-e-gerenciando-migracoes-com-alembic","title":"Configurando o Banco de Dados e Gerenciando Migra\u00e7\u00f5es com Alembic","text":"

Objetivos dessa aula:

Caso prefira ver a aula em v\u00eddeo

Esse aula ainda n\u00e3o est\u00e1 dispon\u00edvel em formato de v\u00eddeo, somente em texto ou live!

Aula Slides C\u00f3digo Quiz Exerc\u00edcios

Com os endpoints da nossa API j\u00e1 estabelecidos, estamos, por ora, utilizando um banco de dados simulado, armazenando uma lista em mem\u00f3ria. Nesta aula, iniciaremos o processo de configura\u00e7\u00e3o do nosso banco de dados real. Nossa agenda inclui a instala\u00e7\u00e3o do SQLAlchemy, a defini\u00e7\u00e3o do modelo de usu\u00e1rios, e a execu\u00e7\u00e3o da primeira migra\u00e7\u00e3o com o Alembic para um banco de dados evolutivo. Al\u00e9m disso, exploraremos como desacoplar as configura\u00e7\u00f5es do banco de dados da aplica\u00e7\u00e3o, seguindo os princ\u00edpios dos 12 fatores.

Antes de prosseguirmos com a instala\u00e7\u00e3o e a configura\u00e7\u00e3o, \u00e9 crucial entender alguns conceitos fundamentais sobre ORMs (Object-Relational Mapping).

"},{"location":"04/#o-que-e-um-orm-e-por-que-usamos-um","title":"O que \u00e9 um ORM e por que usamos um?","text":"

ORM significa Mapeamento Objeto-Relacional. \u00c9 uma t\u00e9cnica de programa\u00e7\u00e3o que vincula (ou mapeia) objetos a registros de banco de dados. Em outras palavras, um ORM permite que voc\u00ea interaja com seu banco de dados, como se voc\u00ea estivesse trabalhando com objetos Python.

O SQLAlchemy \u00e9 um exemplo de ORM. Ele permite que voc\u00ea trabalhe com bancos de dados SQL de maneira mais natural aos programadores Python. Em vez de escrever consultas SQL cruas, voc\u00ea pode usar m\u00e9todos e atributos Python para manipular seus registros de banco de dados.

Mas por que usar\u00edamos um ORM? Aqui est\u00e3o algumas raz\u00f5es:

"},{"location":"04/#configuracoes-de-ambiente-e-os-12-fatores","title":"Configura\u00e7\u00f5es de ambiente e os 12 fatores","text":"

Uma boa pr\u00e1tica no desenvolvimento de aplica\u00e7\u00f5es \u00e9 separar as configura\u00e7\u00f5es do c\u00f3digo. Configura\u00e7\u00f5es, como credenciais de banco de dados, s\u00e3o propensas a mudan\u00e7as entre ambientes diferentes (como desenvolvimento, teste e produ\u00e7\u00e3o). Mistur\u00e1-las com o c\u00f3digo pode tornar o processo de mudan\u00e7a entre esses ambientes complicado e propenso a erros.

Caso queira saber mais sobre 12 fatores

Temos uma live focada nesse assunto com a participa\u00e7\u00e3o especial do Bruno Rocha

Link direto

Al\u00e9m disso, expor credenciais de banco de dados e outras informa\u00e7\u00f5es sens\u00edveis no c\u00f3digo-fonte \u00e9 uma pr\u00e1tica de seguran\u00e7a ruim. Se esse c\u00f3digo fosse comprometido, essas informa\u00e7\u00f5es poderiam ser usadas para acessar e manipular seus recursos.

Por isso, usaremos o pydantic-settings para gerenciar nossas configura\u00e7\u00f5es de ambiente. A biblioteca permite que voc\u00ea defina configura\u00e7\u00f5es em arquivos separados ou vari\u00e1veis de ambiente e acesse-as de uma maneira estruturada e segura em seu c\u00f3digo.

Isso est\u00e1 alinhado com a metodologia dos 12 fatores, um conjunto de melhores pr\u00e1ticas para desenvolvimento de aplica\u00e7\u00f5es modernas. O terceiro fator, \"Config\", afirma que as configura\u00e7\u00f5es que variam entre os ambientes devem ser armazenadas no ambiente e n\u00e3o no c\u00f3digo.

Agora que entendemos melhor esses conceitos, come\u00e7aremos instalando as bibliotecas que iremos usar. O primeiro passo \u00e9 instalar o SQLAlchemy, um ORM que nos permite trabalhar com bancos de dados SQL de maneira Pythonica. Al\u00e9m disso, o Alembic, que \u00e9 uma ferramenta de migra\u00e7\u00e3o de banco de dados, funciona muito bem com o SQLAlchemy e nos ajudar\u00e1 a gerenciar as altera\u00e7\u00f5es do esquema do nosso banco de dados.

$ Execu\u00e7\u00e3o no terminal!
poetry add sqlalchemy\n

Al\u00e9m disso, para evitar a escrita de configura\u00e7\u00f5es do banco de dados diretamente no c\u00f3digo-fonte, usaremos o pydantic-settings. Este pacote nos permite gerenciar as configura\u00e7\u00f5es do nosso aplicativo de uma maneira mais segura e estruturada.

$ Execu\u00e7\u00e3o no terminal!
poetry add pydantic-settings\n

Agora estamos prontos para mergulhar na configura\u00e7\u00e3o do nosso banco de dados! Vamos em frente.

"},{"location":"04/#o-basico-sobre-sqlalchemy","title":"O b\u00e1sico sobre SQLAlchemy","text":"

SQLAlchemy \u00e9 uma biblioteca Python vers\u00e1til, concebida para intermediar a intera\u00e7\u00e3o entre Python e bancos de dados relacionais, como MySQL, PostgreSQL e SQLite. A biblioteca \u00e9 constitu\u00edda por duas partes principais: o Core e o ORM (Object Relational Mapper).

Caso nunca tenha trabalhado com SQLAlchemy

Temos uma live de Python cobrindo as mudan\u00e7as e o b\u00e1sico sobre o SQLAlchemy na vers\u00e3o 2+:

Al\u00e9m do Core e do ORM, o SQLAlchemy conta com outros componentes cruciais que ser\u00e3o foco desta aula, a Engine e a Session:

"},{"location":"04/#engine","title":"Engine","text":"

A 'Engine' do SQLAlchemy \u00e9 o ponto de contato com o banco de dados, estabelecendo e gerenciando as conex\u00f5es. Ela \u00e9 instanciada atrav\u00e9s da fun\u00e7\u00e3o create_engine(), que recebe as credenciais do banco de dados, o endere\u00e7o de conex\u00e3o (URI) e configura o pool de conex\u00f5es.

"},{"location":"04/#session","title":"Session","text":"

Quanto \u00e0 persist\u00eancia de dados e consultas ao banco de dados utilizando o ORM, a Session \u00e9 a principal interface. Ela atua como um intermedi\u00e1rio entre o aplicativo Python e o banco de dados, mediada pela Engine. A Session \u00e9 encarregada de todas as transa\u00e7\u00f5es, fornecendo uma API para conduzi-las.

Agora que conhecemos a Engine e a Session, vamos explorar a defini\u00e7\u00e3o de modelos de dados.

"},{"location":"04/#definindo-os-modelos-de-dados-com-sqlalchemy","title":"Definindo os Modelos de Dados com SQLAlchemy","text":"

Os modelos de dados definem a estrutura de como os dados ser\u00e3o armazenados no banco de dados. No ORM do SQLAlchemy, esses modelos s\u00e3o definidos como classes Python que podem ser herdados ou registradas (isso depende de como voc\u00ea usa o ORM). Vamos usar o registrador de tabelas, que j\u00e1 faz a convers\u00e3o autom\u00e1tica das classes em dataclasses

Cada classe que \u00e9 registrada pelo objeto registry \u00e9 automaticamente mapeada para uma tabela no banco de dados. Adicionalmente, a classe base inclui um objeto de metadados que \u00e9 uma cole\u00e7\u00e3o de todas as tabelas declaradas. Este objeto \u00e9 utilizado para gerenciar opera\u00e7\u00f5es como cria\u00e7\u00e3o, modifica\u00e7\u00e3o e exclus\u00e3o de tabelas.

Agora definiremos nosso modelo User. No diret\u00f3rio fast_zero, crie um novo arquivo chamado models.py e incluiremos o seguinte c\u00f3digo no arquivo:

fast_zero/models.py
from datetime import datetime\nfrom sqlalchemy.orm import Mapped, registry\n\ntable_registry = registry()\n\n\n@table_registry.mapped_as_dataclass\nclass User:\n    __tablename__ = 'users'\n\n    id: Mapped[int]\n    username: Mapped[str]\n    password: Mapped[str]\n    email: Mapped[str]\n    created_at: Mapped[datetime]\n

Aqui, Mapped refere-se a um atributo Python que \u00e9 associado (ou mapeado) a uma coluna espec\u00edfica em uma tabela de banco de dados. Por exemplo, Mapped[int] indica que este atributo \u00e9 um inteiro que ser\u00e1 mapeado para uma coluna correspondente em uma tabela de banco de dados. Da mesma forma, Mapped[str] se referiria a um atributo de string que seria mapeado para uma coluna de string correspondente. Esta abordagem permite ao SQLAlchemy realizar a convers\u00e3o entre os tipos de dados Python e os tipos de dados do banco de dados, al\u00e9m de oferecer uma interface Pythonica para a intera\u00e7\u00e3o entre eles.

Em especial, devemos nos atentar com o campo __tablename__. Ele \u00e9 referente ao nome que a tabela ter\u00e1 no banco de dados. Como geralmente um objeto no python representa somente uma entidade, usarei 'users' no plural para representar a tabela.

"},{"location":"04/#o-uso-do-modelo","title":"O uso do modelo","text":"

Se quisermos usar esse objeto, ela se comporta como uma dataclass tradicional. Podendo ser instanciada da forma tradicional:

C\u00f3digo de exemplo
eduardo = User(\n    id=1,\n    username='dunossauro',\n    password='senha123',\n    email='duno@ssauro.com',\n    created_at=datetime.now()\n)\n

Por padr\u00e3o, todos os atributos precisam ser especificados. O que pode n\u00e3o ser muito interessante, pois alguns dados devem ser preenchidos pelo banco de dados. Como o identificador da linha no banco ou a hora em que o registro foi criado.

Para isso, precisamos adicionar mais informa\u00e7\u00f5es ao modelo.

"},{"location":"04/#configuracoes-de-colunas","title":"Configura\u00e7\u00f5es de colunas","text":"

Quando definimos tabelas no banco de dados, as colunas podem apresentar propriedades espec\u00edficas. Por exemplo:

Para esses casos, o SQLAlchemy conta com a fun\u00e7\u00e3o mapped_column. Dentro dela, voc\u00ea pode definir diversas propriedades.

Para o nosso caso, gostaria que email e username n\u00e3o se repetissem na base de dados e que as colunas id e created_at tivessem o valor definido pelo pr\u00f3prio banco de dados, quando o registro fosse criado.

Para isso, vamos aplicar alguns par\u00e2metros nas colunas usando mapped_column:

fast_zero/models.py
from datetime import datetime\n\nfrom sqlalchemy import func\nfrom sqlalchemy.orm import Mapped, mapped_column, registry\n\ntable_registry = registry()\n\n\n@table_registry.mapped_as_dataclass\nclass User:\n    __tablename__ = 'users'\n\n    id: Mapped[int] = mapped_column(init=False, primary_key=True)#(1)!\n    username: Mapped[str] = mapped_column(unique=True)#(2)!\n    password: Mapped[str]\n    email: Mapped[str] = mapped_column(unique=True)\n    created_at: Mapped[datetime] = mapped_column(#(3)!\n        init=False, server_default=func.now()\n    )\n
  1. init=False diz que, quando o objeto for instanciado, esse par\u00e2metro n\u00e3o deve ser passado. primary_key=True diz que o campo id \u00e9 a chave prim\u00e1ria dessa tabela.
  2. unique=True diz que esse campo n\u00e3o deve se repetir na tabela. Por exemplo, se tivermos um username \"dunossauro\", n\u00e3o podemos ter outro com o mesmo valor.
  3. server_default=func.now() diz que, quando a classe for instanciada, o resultado de func.now() ser\u00e1 o valor atribu\u00eddo a esse atributo. No caso, a data e hora em que ele foi instanciado.

Desta forma, unimos tanto o uso que queremos ter no python, quanto a configura\u00e7\u00e3o esperada da tabela no banco de dados. Os par\u00e2metros de mapeamento dizem:

O campo init n\u00e3o tem uma rela\u00e7\u00e3o direta com o banco de dados, mas sim com a forma em que vamos usar o objeto do modelo no c\u00f3digo. Ele diz que os atributos marcados com init=false n\u00e3o devem ser passados no momento em que User for instanciado. Por exemplo:

C\u00f3digo de exemplo
eduardo = User(\n    username='dunossauro', password='senha123', email='duno@ssauro.com',\n)\n

Por n\u00e3o passarmos estes par\u00e2metros para User, o SQLAlchemy se encarregar\u00e1 de atribuir os valores a eles de forma autom\u00e1tica.

O campo created_at ser\u00e1 preenchido pelo resultado da fun\u00e7\u00e3o passada em server_default. O campo id, por contar com primary_key=True, ser\u00e1 autopreenchido com o id correspondente quando for armazenado no banco de dados.

Existem diversas op\u00e7\u00f5es nessa fun\u00e7\u00e3o. Caso queira ver mais possibilidades de mapeamento, aqui est\u00e1 a referencia para mais campos

"},{"location":"04/#testando-as-tabelas","title":"Testando as Tabelas","text":"

Antes de prosseguirmos, uma boa pr\u00e1tica seria criar um teste para validar se toda a estrutura do banco de dados funciona. Criaremos um arquivo para validar isso: test_db.py.

A partir daqui, voc\u00ea pode prosseguir com a estrutura\u00e7\u00e3o do conte\u00fado desse arquivo para definir os testes necess\u00e1rios para validar o seu modelo de usu\u00e1rio e sua intera\u00e7\u00e3o com o banco de dados.

"},{"location":"04/#antes-de-escrever-os-testes","title":"Antes de Escrever os Testes","text":"

A essa altura, se estiv\u00e9ssemos buscando apenas cobertura, poder\u00edamos simplesmente testar utilizando o modelo, e isso seria suficiente. No entanto, queremos verificar se toda a nossa intera\u00e7\u00e3o com o banco de dados ocorrer\u00e1 com sucesso. Isso inclui saber se os tipos de dados na tabela foram mapeados corretamente, se \u00e9 poss\u00edvel interagir com o banco de dados, se o ORM est\u00e1 estruturado adequadamente com a classe base. Precisamos garantir que todo esse esquema funcione.

graph\n  A[Aplicativo Python] -- utiliza --> B[SQLAlchemy ORM]\n  B -- fornece --> D[Session]\n  D -- interage com --> C[Modelos]\n  C -- mapeados para --> G[Tabelas no Banco de Dados]\n  D -- depende de --> E[Engine]\n  E -- conecta-se com --> F[Banco de Dados]\n  C -- associa-se a --> H[Metadata]\n  H -- mant\u00e9m informa\u00e7\u00f5es de --> G[Tabelas no Banco de Dados]

Neste diagrama, vemos a rela\u00e7\u00e3o completa entre o aplicativo Python e o banco de dados. A conex\u00e3o \u00e9 estabelecida atrav\u00e9s do SQLAlchemy ORM, que fornece uma Session para interagir com os Modelos. Esses modelos s\u00e3o mapeados para as tabelas no banco de dados, enquanto a Engine se conecta com o banco de dados e depende de Metadata para manter as informa\u00e7\u00f5es das tabelas.

Portanto, criaremos uma fixture para podermos usar todo esse esquema sempre que necess\u00e1rio.

"},{"location":"04/#criando-uma-fixture-para-interacoes-com-o-banco-de-dados","title":"Criando uma Fixture para intera\u00e7\u00f5es com o Banco de Dados","text":"

Para testar o banco, temos que fazer diversos passos, e isso pode tornar nosso teste bastante grande. Uma fixture pode ajudar a isolar toda essa configura\u00e7\u00e3o do banco de dados fora do teste. Assim, evitamos repetir o mesmo c\u00f3digo em todos os testes e ainda garantimos que cada teste tenha sua pr\u00f3pria vers\u00e3o limpa do banco de dados.

Criaremos uma fixture para a conex\u00e3o com o banco de dados chamada session:

tests/conftest.py
import pytest\nfrom fastapi.testclient import TestClient\nfrom sqlalchemy import create_engine\nfrom sqlalchemy.orm import Session\n\nfrom fast_zero.app import app\nfrom fast_zero.models import table_registry\n\n\n@pytest.fixture\ndef client():\n    return TestClient(app)\n\n\n@pytest.fixture\ndef session():\n    engine = create_engine('sqlite:///:memory:')#(1)!\n    table_registry.metadata.create_all(engine)#(2)!\n\n    with Session(engine) as session:#(3)!\n        yield session#(4)!\n\n    table_registry.metadata.drop_all(engine)#(5)!\n

Aqui, estamos utilizando o SQLite como o banco de dados em mem\u00f3ria para os testes. Essa \u00e9 uma pr\u00e1tica comum em testes unit\u00e1rios, pois a utiliza\u00e7\u00e3o de um banco de dados em mem\u00f3ria \u00e9 mais r\u00e1pida do que um banco de dados persistido em disco. Com o SQLite em mem\u00f3ria, podemos criar e destruir bancos de dados facilmente, o que \u00e9 \u00fatil para isolar os testes e garantir que os dados de um teste n\u00e3o afetem outros testes. Al\u00e9m disso, n\u00e3o precisamos nos preocupar com a limpeza dos dados ap\u00f3s a execu\u00e7\u00e3o dos testes, j\u00e1 que o banco de dados em mem\u00f3ria \u00e9 descartado quando o programa \u00e9 encerrado.

O que cada linha da fixture faz?

  1. create_engine('sqlite:///:memory:'): cria um mecanismo de banco de dados SQLite em mem\u00f3ria usando SQLAlchemy. Este mecanismo ser\u00e1 usado para criar uma sess\u00e3o de banco de dados para nossos testes.

  2. table_registry.metadata.create_all(engine): cria todas as tabelas no banco de dados de teste antes de cada teste que usa a fixture session.

  3. Session(engine): cria uma sess\u00e3o Session para que os testes possam se comunicar com o banco de dados. Por conta do yield a sess\u00e3o \u00e9 sempre renovada ap\u00f3s cada teste.

  4. yield Session(): fornece uma inst\u00e2ncia de Session que ser\u00e1 injetada em cada teste que solicita a fixture session. Essa sess\u00e3o ser\u00e1 usada para interagir com o banco de dados de teste.

  5. table_registry.metadata.drop_all(engine): ap\u00f3s cada teste que usa a fixture session, todas as tabelas do banco de dados de teste s\u00e3o eliminadas, garantindo que cada teste seja executado contra um banco de dados limpo.

Resumindo, essa fixture est\u00e1 configurando e limpando um banco de dados de teste para cada teste que o solicita, assegurando que cada teste seja isolado e tenha seu pr\u00f3prio ambiente limpo para trabalhar. Isso \u00e9 uma boa pr\u00e1tica em testes de unidade, j\u00e1 que queremos que cada teste seja independente e n\u00e3o afete os demais.

"},{"location":"04/#criando-um-teste-para-a-nossa-tabela","title":"Criando um Teste para a Nossa Tabela","text":"

Agora, no arquivo test_db.py, escreveremos um teste para a cria\u00e7\u00e3o de um usu\u00e1rio. Este teste adiciona um novo usu\u00e1rio ao banco de dados, faz commit das mudan\u00e7as, e depois verifica se o usu\u00e1rio foi devidamente criado consultando-o pelo nome de usu\u00e1rio. Se o usu\u00e1rio foi criado corretamente, o teste passa. Caso contr\u00e1rio, o teste falha, indicando que h\u00e1 algo errado com nossa fun\u00e7\u00e3o de cria\u00e7\u00e3o de usu\u00e1rio.

tests/test_db.py
from sqlalchemy import select\n\nfrom fast_zero.models import User\n\n\ndef test_create_user(session):\n    new_user = User(username='alice', password='secret', email='teste@test')\n    session.add(new_user)#(1)!\n    session.commit()#(2)!\n\n    user = session.scalar(select(User).where(User.username == 'alice'))#(3)!\n\n    assert user.username == 'alice'\n
  1. O m\u00e9todo .add da sess\u00e3o, adiciona o registro a sess\u00e3o. O dado fica em um estado transiente. Ele n\u00e3o foi adicionado ao banco de dados ainda. Mas j\u00e1 est\u00e1 reservado na sess\u00e3o. Ele \u00e9 uma aplica\u00e7\u00e3o do padr\u00e3o de projeto Unidade de trabalho.
  2. No momento em que existem dados transientes na sess\u00e3o e queremos \"performar\" efetivamente as a\u00e7\u00f5es no banco de dados. Usamos o m\u00e9todo .commit.
  3. O m\u00e9todo .scalar \u00e9 usado para performar buscas no banco (queries). Ele pega o primeiro resultado da busca e faz uma opera\u00e7\u00e3o de converter o resultado do banco de dados em um Objeto criado pelo SQLAlchemy, nesse caso, caso encontre um resultado, ele ir\u00e1 converter na classe User. A fun\u00e7\u00e3o de select \u00e9 uma fun\u00e7\u00e3o de busca de dados no banco. Nesse caso estamos procurando em todos os Users onde (where) o nome \u00e9 igual a \"alice\".
"},{"location":"04/#executando-o-teste","title":"Executando o teste","text":"

A execu\u00e7\u00e3o de testes \u00e9 uma parte vital do desenvolvimento de qualquer aplica\u00e7\u00e3o. Os testes nos ajudam a identificar e corrigir problemas antes que eles se tornem mais s\u00e9rios. Eles tamb\u00e9m fornecem a confian\u00e7a de que nossas mudan\u00e7as n\u00e3o quebraram nenhuma funcionalidade existente. No nosso caso, executaremos os testes para validar nossos modelos de usu\u00e1rio e garantir que eles estejam funcionando como esperado.

Para executar os testes, digite o seguinte comando:

$ Execu\u00e7\u00e3o no terminal!
task test\n
# ...\n\ntests/test_app.py::test_root_deve_retornar_ok_e_ola_mundo PASSED\ntests/test_app.py::test_create_user PASSED\ntests/test_app.py::test_read_users PASSED\ntests/test_app.py::test_update_user PASSED\ntests/test_app.py::test_delete_user PASSED\ntests/test_db.py::test_create_user PASSED\n\n---------- coverage: platform linux, python 3.11.3-final-0 -----------\nName                    Stmts   Miss  Cover\n-------------------------------------------\nfast_zero/__init__.py       0      0   100%\nfast_zero/app.py           28      2    93%\nfast_zero/models.py        11      0   100%\nfast_zero/schemas.py       15      0   100%\n-------------------------------------------\nTOTAL                      54      2    96%\n

Neste caso, podemos ver que todos os nossos testes passaram com sucesso. Isso significa que nossa funcionalidade de cria\u00e7\u00e3o de usu\u00e1rio est\u00e1 funcionando corretamente e que nosso modelo de usu\u00e1rio est\u00e1 sendo corretamente persistido no banco de dados.

Com nossos modelos e testes de banco de dados agora em ordem, estamos prontos para avan\u00e7ar para a pr\u00f3xima fase de configura\u00e7\u00e3o de nosso banco de dados e gerenciamento de migra\u00e7\u00f5es.

"},{"location":"04/#configuracao-do-ambiente-do-banco-de-dados","title":"Configura\u00e7\u00e3o do ambiente do banco de dados","text":"

Por fim, configuraremos nosso banco de dados. Primeiro, criaremos um novo arquivo chamado settings.py dentro do diret\u00f3rio fast_zero. Aqui, usaremos o Pydantic para criar uma classe Settings que ir\u00e1 pegar as configura\u00e7\u00f5es do nosso arquivo .env. Neste arquivo, a classe Settings \u00e9 definida como:

fast_zero/settings.py
from pydantic_settings import BaseSettings, SettingsConfigDict\n\n\nclass Settings(BaseSettings):\n    model_config = SettingsConfigDict( #(1)!\n        env_file='.env', env_file_encoding='utf-8'#(2)!\n    )\n\n    DATABASE_URL: str#(3)!\n
  1. SettingsConfigDict: \u00e9 um objeto do pydantic-settings que carrega as vari\u00e1veis em um arquivo de configura\u00e7\u00e3o. Por exemplo, um .env.
  2. Aqui definimos o caminho para o arquivo de configura\u00e7\u00e3o e o encoding dele.
  3. DATABASE_URL: Essa vari\u00e1vel sera preenchida com o valor encontrado com o mesmo nome no arquivo .env.

Agora, definiremos o DATABASE_URL no nosso arquivo de ambiente .env. Crie o arquivo na raiz do projeto e adicione a seguinte linha:

.env
DATABASE_URL=\"sqlite:///database.db\"\n

Com isso, quando a classe Settings for instanciada, ela ir\u00e1 automaticamente carregar as configura\u00e7\u00f5es do arquivo .env.

Finalmente, adicione o arquivo de banco de dados, database.db, ao .gitignore para garantir que n\u00e3o seja inclu\u00eddo no controle de vers\u00e3o. Adicionar informa\u00e7\u00f5es sens\u00edveis ou arquivos bin\u00e1rios ao controle de vers\u00e3o \u00e9 geralmente considerado uma pr\u00e1tica ruim.

$ Execu\u00e7\u00e3o no terminal!
echo 'database.db' >> .gitignore\n
"},{"location":"04/#instalando-o-alembic-e-criando-a-primeira-migracao","title":"Instalando o Alembic e Criando a Primeira Migra\u00e7\u00e3o","text":"

Antes de avan\u00e7armos, \u00e9 importante entender o que s\u00e3o migra\u00e7\u00f5es de banco de dados e por que s\u00e3o \u00fateis. As migra\u00e7\u00f5es s\u00e3o uma maneira de fazer altera\u00e7\u00f5es ou atualiza\u00e7\u00f5es no banco de dados, como adicionar uma tabela ou uma coluna a uma tabela, ou alterar o tipo de dados de uma coluna. Elas s\u00e3o extremamente \u00fateis, pois nos permitem manter o controle de todas as altera\u00e7\u00f5es feitas no esquema do banco de dados ao longo do tempo. Elas tamb\u00e9m nos permitem reverter para uma vers\u00e3o anterior do esquema do banco de dados, se necess\u00e1rio.

Caso nunca tenha trabalhado com Migra\u00e7\u00f5es

Temos uma live de Python focada nesse assunto em espec\u00edfico

Link direto

Agora, come\u00e7aremos instalando o Alembic, que \u00e9 uma ferramenta de migra\u00e7\u00e3o de banco de dados para SQLAlchemy. Usaremos o Poetry para adicionar o Alembic ao nosso projeto:

$ Execu\u00e7\u00e3o no terminal!
poetry add alembic\n

Ap\u00f3s a instala\u00e7\u00e3o do Alembic, precisamos inici\u00e1-lo em nosso projeto. O comando de inicializa\u00e7\u00e3o criar\u00e1 um diret\u00f3rio migrations e um arquivo de configura\u00e7\u00e3o alembic.ini:

$ Execu\u00e7\u00e3o no terminal!
alembic init migrations\n

Com isso, a estrutura do nosso projeto sofre algumas altera\u00e7\u00f5es e novos arquivos s\u00e3o criados:

.\n\u251c\u2500\u2500 .env\n\u251c\u2500\u2500 alembic.ini\n\u251c\u2500\u2500 fast_zero\n\u2502  \u251c\u2500\u2500 __init__.py\n\u2502  \u251c\u2500\u2500 app.py\n\u2502  \u251c\u2500\u2500 models.py\n\u2502  \u251c\u2500\u2500 schemas.py\n\u2502  \u2514\u2500\u2500 settings.py\n\u251c\u2500\u2500 migrations\n\u2502  \u251c\u2500\u2500 env.py\n\u2502  \u251c\u2500\u2500 README\n\u2502  \u251c\u2500\u2500 script.py.mako\n\u2502  \u2514\u2500\u2500 versions\n\u251c\u2500\u2500 poetry.lock\n\u251c\u2500\u2500 pyproject.toml\n\u251c\u2500\u2500 README.md\n\u2514\u2500\u2500 tests\n   \u251c\u2500\u2500 __init__.py\n   \u251c\u2500\u2500 conftest.py\n   \u251c\u2500\u2500 test_app.py\n   \u2514\u2500\u2500 test_db.py\n

No arquivo alembic.ini: ficam as configura\u00e7\u00f5es gerais das nossas migra\u00e7\u00f5es. Na pasta migrations foram criados um arquivo chamado env.py, esse arquivo \u00e9 respons\u00e1vel por como as migra\u00e7\u00f5es ser\u00e3o feitas, e o arquivo script.py.mako \u00e9 um template para as novas migra\u00e7\u00f5es.

"},{"location":"04/#criando-uma-migracao-automatica","title":"Criando uma migra\u00e7\u00e3o autom\u00e1tica","text":"

Com o Alembic devidamente instalado e iniciado, agora \u00e9 o momento de gerar nossa primeira migra\u00e7\u00e3o. Mas, antes disso, precisamos garantir que o Alembic consiga acessar nossas configura\u00e7\u00f5es e modelos corretamente. Para isso, faremos algumas altera\u00e7\u00f5es no arquivo migrations/env.py.

Neste arquivo, precisamos:

  1. Importar as Settings do nosso arquivo settings.py e a table_registry dos nossos modelos.
  2. Configurar a URL do SQLAlchemy para ser a mesma que definimos em Settings.
  3. Verificar a exist\u00eancia do arquivo de configura\u00e7\u00e3o do Alembic e, se presente, l\u00ea-lo.
  4. Definir os metadados de destino como table_registry.metadata, que \u00e9 o que o Alembic utilizar\u00e1 para gerar automaticamente as migra\u00e7\u00f5es.

O arquivo migrations/env.py modificado ficar\u00e1 assim:

migrations/env.py
from logging.config import fileConfig\n\nfrom sqlalchemy import engine_from_config\nfrom sqlalchemy import pool\n\nfrom alembic import context\n\nfrom fast_zero.models import table_registry\nfrom fast_zero.settings import Settings\n\nconfig = context.config\nconfig.set_main_option('sqlalchemy.url', Settings().DATABASE_URL)\n\nif config.config_file_name is not None:\n    fileConfig(config.config_file_name)\n\ntarget_metadata = table_registry.metadata\n\n# other values from the config, defined by the needs of env.py,\n# ...\n

Feitas essas altera\u00e7\u00f5es, estamos prontos para gerar nossa primeira migra\u00e7\u00e3o autom\u00e1tica. O Alembic \u00e9 capaz de gerar migra\u00e7\u00f5es a partir das mudan\u00e7as detectadas nos nossos modelos do SQLAlchemy.

Para criar a migra\u00e7\u00e3o, utilizamos o seguinte comando:

$ Execu\u00e7\u00e3o no terminal!
alembic revision --autogenerate -m \"create users table\"\n

Este comando instrui o Alembic a criar uma nova revis\u00e3o de migra\u00e7\u00e3o no diret\u00f3rio migrations/versions. A revis\u00e3o gerada conter\u00e1 os comandos SQL necess\u00e1rios para aplicar a migra\u00e7\u00e3o (criar a tabela de usu\u00e1rios) e para reverter essa migra\u00e7\u00e3o, caso seja necess\u00e1rio.

"},{"location":"04/#analisando-a-migracao-automatica","title":"Analisando a migra\u00e7\u00e3o autom\u00e1tica","text":"

Ao criar uma migra\u00e7\u00e3o autom\u00e1tica com o Alembic, um arquivo \u00e9 gerado dentro da pasta migrations/versions. O nome deste arquivo come\u00e7a com um ID de revis\u00e3o (um hash \u00fanico gerado pelo Alembic), seguido por uma breve descri\u00e7\u00e3o que fornecemos no momento da cria\u00e7\u00e3o da migra\u00e7\u00e3o, neste caso, create_users_table.

Vamos analisar o arquivo de migra\u00e7\u00e3o:

migrations/versions/e018397cecf4_create_users_table.py
\"\"\"create users table\n\nRevision ID: e018397cecf4\nRevises:\nCreate Date: 2023-07-13 03:43:03.730534\n\n\"\"\"\nfrom typing import Sequence, Union\n\nfrom alembic import op\nimport sqlalchemy as sa\n\n\n# revision identifiers, used by Alembic.\nrevision: str = 'e018397cecf4'\ndown_revision: Union[str, None] = None\nbranch_labels: Union[str, Sequence[str], None] = None\ndepends_on: Union[str, Sequence[str], None] = None\n\n\ndef upgrade() -> None:\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.create_table('users',\n    sa.Column('id', sa.Integer(), nullable=False),\n    sa.Column('username', sa.String(), nullable=False),\n    sa.Column('password', sa.String(), nullable=False),\n    sa.Column('email', sa.String(), nullable=False),\n    sa.Column('created_at', sa.DateTime(), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False),\n    sa.PrimaryKeyConstraint('id'),\n    sa.UniqueConstraint('email'),\n    sa.UniqueConstraint('username')\n    )\n    # ### end Alembic commands ###\n\n\ndef downgrade() -> None:\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.drop_table('users')\n    # ### end Alembic commands ###\n

Esse arquivo descreve as mudan\u00e7as a serem feitas no banco de dados. Ele usa a linguagem core do SQLAlchemy, que \u00e9 mais baixo n\u00edvel que o ORM. As fun\u00e7\u00f5es upgrade e downgrade definem, respectivamente, o que fazer para aplicar e para desfazer a migra\u00e7\u00e3o. No nosso caso, a fun\u00e7\u00e3o upgrade cria a tabela 'users' com os campos que definimos em fast_zero/models.pye a fun\u00e7\u00e3o downgrade a remove.

"},{"location":"04/#analisando-o-banco-de-dados","title":"Analisando o banco de dados","text":"

Ao criar a migra\u00e7\u00e3o, o Alembic teve que observar se j\u00e1 existiam migra\u00e7\u00f5es anteriores no banco de dados. Como o banco de dados n\u00e3o existia, ele criou um novo banco sqlite com o nome que definimos na vari\u00e1vel de ambiente DATABASE_URL. No caso database.db.

Se olharmos a estrutura de pastas, esse arquivo agora existe:

.\n\u251c\u2500\u2500 .env\n\u251c\u2500\u2500 alembic.ini\n\u251c\u2500\u2500 database.db\n\u251c\u2500\u2500 fast_zero\n\u2502  \u2514\u2500\u2500 ...\n\u251c\u2500\u2500 migrations\n\u2502  \u2514\u2500\u2500 ...\n\u251c\u2500\u2500 poetry.lock\n\u251c\u2500\u2500 pyproject.toml\n\u251c\u2500\u2500 README.md\n\u2514\u2500\u2500 tests\n   \u2514\u2500\u2500 ...\n

Pelo fato do sqlite3 ser um banco baseado em um \u00fanico arquivo, no momento das migra\u00e7\u00f5es, o sqlalchemy faz a cria\u00e7\u00e3o do arquivo de banco de dados caso ele n\u00e3o exista.

No momento da verifica\u00e7\u00e3o, caso n\u00e3o exista a tabela de migra\u00e7\u00f5es, ela ser\u00e1 criada. A tabela de migra\u00e7\u00f5es \u00e9 nomeada como alembic_version.

Vamos acessar o console do sqlite e verificar se isso foi feito. Precisamos chamar sqlite3 nome_do_arquivo.db:

$ Execu\u00e7\u00e3o no terminal!
sqlite3 database.db\n
Caso n\u00e3o tenha o SQLite instalado na sua m\u00e1quina: Arch
pacman -S sqlite\n
Debian/Ubuntu
sudo apt install sqlite3\n
Mac
brew install sqlite\n
Windows
winget install --id SQLite.SQLite\n

Quando executamos esse comando, o console do sqlite ser\u00e1 inicializado. E dentro dele podemos executar alguns comandos. Como fazer consultas, ver as tabelas criadas, adicionar dados, etc.

A cara do console \u00e9 essa:

SQLite version 3.45.1 2024-01-30 16:01:20\nEnter \".help\" for usage hints.\nsqlite>\n

Aqui voc\u00ea pode digitar comandos, da mesma forma em que fazemos no terminal interativo do python. O comando .schema nos mostra todas as tabelas criadas no banco de dados:

sqlite> .schema\nCREATE TABLE alembic_version (\n    version_num VARCHAR(32) NOT NULL,\n    CONSTRAINT alembic_version_pkc PRIMARY KEY (version_num)\n);\n

Nisso vemos que o Alembic criou uma tabela chamada alembic_version no banco de dados. Nessa tabela temos um \u00fanico campo chamado version_num que \u00e9 o campo que marca a vers\u00e3o atual da migra\u00e7\u00e3o no banco.

Para ver a vers\u00e3o atual do banco, podemos executar uma busca no campo e ver o resultado:

sqlite> select version_num from alembic_version;\n

O resultado deve ser vazio, pois n\u00e3o aplicamos nenhuma migra\u00e7\u00e3o, ele somente criou a tabela de migra\u00e7\u00f5es.

Para sair do console do sqlite temos que digitar o comando .quit:

sqlite> .quit\n

Agora que temos o terminal de volta, podemos aplicar as migra\u00e7\u00f5es.

"},{"location":"04/#aplicando-a-migracao","title":"Aplicando a migra\u00e7\u00e3o","text":"

Para aplicar as migra\u00e7\u00f5es, usamos o comando upgrade do CLI Alembic. O argumento head indica que queremos aplicar todas as migra\u00e7\u00f5es que ainda n\u00e3o foram aplicadas:

$ Execu\u00e7\u00e3o no terminal!
alembic upgrade head\n

Teremos a seguinte resposta:

INFO  [alembic.runtime.migration] Context impl SQLiteImpl.\nINFO  [alembic.runtime.migration] Will assume non-transactional DDL.\nINFO  [alembic.runtime.migration] Running upgrade  -> e018397cecf4, create users table\n

Vemos na \u00faltima linha executada a migra\u00e7\u00e3o de c\u00f3digo e018397cecf4, com o nome create users table.

Agora, se examinarmos nosso banco de dados novamente:

$ Execu\u00e7\u00e3o no terminal!
sqlite3 database.db\n

Podemos verificar se a tabela users foi criada no schema do banco:

sqlite> .schema\nCREATE TABLE alembic_version (\n    version_num VARCHAR(32) NOT NULL,\n    CONSTRAINT alembic_version_pkc PRIMARY KEY (version_num)\n);\nCREATE TABLE users (\n        id INTEGER NOT NULL,\n        username VARCHAR NOT NULL,\n        password VARCHAR NOT NULL,\n        email VARCHAR NOT NULL,\n        created_at DATETIME DEFAULT (CURRENT_TIMESTAMP) NOT NULL,\n        PRIMARY KEY (id),\n        UNIQUE (email),\n        UNIQUE (username)\n);\n

Se examinarmos os dados da tabela alembic_version podemos ver que o n\u00famero da migra\u00e7\u00e3o \u00e9 referente ao valor criado no arquivo de migra\u00e7\u00e3o e018397cecf4_create_users_table.py

sqlite> select version_num from alembic_version;\ne018397cecf4\nsqlite> .quit\n

Com isso, finalizamos a cria\u00e7\u00e3o do banco de dados. Lembre-se de que todas essas mudan\u00e7as que fizemos s\u00f3 existem localmente no seu ambiente de trabalho at\u00e9 agora. Para serem compartilhadas com outras pessoas, precisamos fazer commit dessas mudan\u00e7as no nosso sistema de controle de vers\u00e3o.

"},{"location":"04/#commit","title":"Commit","text":"

Primeiro, verificaremos o status do nosso reposit\u00f3rio para ver as mudan\u00e7as que fizemos:

$ Execu\u00e7\u00e3o no terminal!
git status\n

Voc\u00ea ver\u00e1 uma lista de arquivos modificados ou adicionados. As altera\u00e7\u00f5es devem incluir os arquivos de migra\u00e7\u00e3o que criamos, bem como quaisquer altera\u00e7\u00f5es que fizemos em nossos arquivos de modelo e configura\u00e7\u00e3o.

Em seguida, adicionaremos todas as mudan\u00e7as ao pr\u00f3ximo commit:

$ Execu\u00e7\u00e3o no terminal!
git add .\n

Agora, estamos prontos para fazer o commit das nossas altera\u00e7\u00f5es. Escreveremos uma mensagem de commit que descreve as mudan\u00e7as que fizemos:

$ Execu\u00e7\u00e3o no terminal!
git commit -m \"Adicionada a primeira migra\u00e7\u00e3o com Alembic. Criada tabela de usu\u00e1rios.\"\n

Finalmente, enviaremos as mudan\u00e7as para o reposit\u00f3rio remoto:

$ Execu\u00e7\u00e3o no terminal!
git push\n

E pronto! As mudan\u00e7as que fizemos foram salvas no hist\u00f3rico do Git e agora est\u00e3o dispon\u00edveis no GitHub.

"},{"location":"04/#exercicios","title":"Exerc\u00edcios","text":"
  1. Fazer uma altera\u00e7\u00e3o no modelo (tabela User) e adicionar um campo chamado updated_at:
    • Esse campo deve ser mapeado para o tipo datetime
    • Esse campo n\u00e3o deve ser inicializado por padr\u00e3o init=False
    • O valor padr\u00e3o deve ser now
    • Toda vez que a tabela for atualizada esse campo deve ser atualizado:
      mapped_column(onupdate=func.now())\n
  2. Criar uma nova migra\u00e7\u00e3o autogerada com alembic
  3. Aplicar essa migra\u00e7\u00e3o ao banco de dados

Exerc\u00edcios resolvidos

"},{"location":"04/#conclusao","title":"Conclus\u00e3o","text":"

Nesta aula, demos passos significativos para preparar nosso projeto FastAPI para interagir com um banco de dados. Come\u00e7amos definindo nosso primeiro modelo de dados, o User, utilizando o SQLAlchemy. Al\u00e9m disso, conforme as pr\u00e1ticas de Desenvolvimento Orientado por Testes (TDD), implementamos um teste para assegurar que a funcionalidade de cria\u00e7\u00e3o de um novo usu\u00e1rio no banco de dados esteja operando corretamente.

Avan\u00e7amos para configurar o ambiente de desenvolvimento, onde estabelecemos um arquivo .env para armazenar nossa DATABASE_URL e ajustamos o SQLAlchemy para utilizar essa URL. Complementarmente, inclu\u00edmos o arquivo do banco de dados ao .gitignore para evitar que seja rastreado pelo controle de vers\u00e3o.

Na \u00faltima parte desta aula, focamos na instala\u00e7\u00e3o e configura\u00e7\u00e3o do Alembic, uma ferramenta de migra\u00e7\u00e3o de banco de dados para SQLAlchemy. Usando o Alembic, criamos nossa primeira migra\u00e7\u00e3o que, automaticamente, gera o esquema do banco de dados a partir dos nossos modelos SQLAlchemy.

Com esses passos, nosso projeto est\u00e1 bem encaminhado para come\u00e7ar a persistir dados. Na pr\u00f3xima aula, avan\u00e7aremos para a fase crucial de conectar o SQLAlchemy aos endpoints do nosso projeto. Isso permitir\u00e1 a realiza\u00e7\u00e3o de opera\u00e7\u00f5es de CRUD nos nossos usu\u00e1rios diretamente atrav\u00e9s da API.

Agora que a aula acabou, \u00e9 um bom momento para voc\u00ea relembrar alguns conceitos e fixar melhor o conte\u00fado respondendo ao question\u00e1rio referente a ela.

Quiz

"},{"location":"05/","title":"Integrando Banco de Dados a API","text":""},{"location":"05/#integrando-banco-de-dados-a-api","title":"Integrando Banco de Dados a API","text":"

Objetivos dessa aula:

Caso prefira ver a aula em v\u00eddeo

Esse aula ainda n\u00e3o est\u00e1 dispon\u00edvel em formato de v\u00eddeo, somente em texto ou live!

Aula Slides C\u00f3digo Quiz Exerc\u00edcios

Ap\u00f3s termos estabelecido nossos modelos e migra\u00e7\u00f5es na aula anterior, \u00e9 hora de darmos um passo significativo: a integra\u00e7\u00e3o do banco de dados real com a nossa aplica\u00e7\u00e3o FastAPI. Deixaremos para tr\u00e1s o banco de dados simulado que utilizamos at\u00e9 ent\u00e3o e nos dedicaremos \u00e0 implementa\u00e7\u00e3o de um banco de dados real e plenamente operacional. Al\u00e9m disso, adaptaremos a estrutura dos nossos testes para que eles sejam compat\u00edveis com o banco de dados, incluindo a cria\u00e7\u00e3o de novas fixtures.

"},{"location":"05/#integrando-sqlalchemy-a-nossa-aplicacao-fastapi","title":"Integrando SQLAlchemy \u00e0 Nossa Aplica\u00e7\u00e3o FastAPI","text":"

Para aqueles que n\u00e3o est\u00e3o familiarizados, o SQLAlchemy \u00e9 uma biblioteca Python que facilita a intera\u00e7\u00e3o com um banco de dados SQL. Ele faz isso oferecendo uma forma de trabalhar com bancos de dados que aproveita a facilidade e o poder do Python, ao mesmo tempo, em que mant\u00e9m a efici\u00eancia e a flexibilidade dos bancos de dados SQL.

Caso nunca tenha trabalhado com SQLAlchemy

Recomendo fortemente que voc\u00ea assista a essa live de python onde os conceitos principais do sqlalchemy s\u00e3o expostos em uma discuss\u00e3o divertida.

Link direto

Uma pe\u00e7a chave do SQLAlchemy \u00e9 o conceito de uma \"sess\u00e3o\". Se voc\u00ea \u00e9 novo no mundo dos bancos de dados, pode pensar na sess\u00e3o como um carrinho de compras virtual: conforme voc\u00ea navega pelo site (ou, neste caso, conforme seu c\u00f3digo executa), voc\u00ea pode adicionar ou remover itens desse carrinho. No entanto, nenhuma altera\u00e7\u00e3o \u00e9 realmente feita at\u00e9 que voc\u00ea decida finalizar a compra. No contexto do SQLAlchemy, \"finalizar a compra\" \u00e9 equivalente a fazer o commit das suas altera\u00e7\u00f5es.

A sess\u00e3o no SQLAlchemy \u00e9 t\u00e3o poderosa que, na verdade, incorpora tr\u00eas padr\u00f5es de arquitetura importantes.

  1. Mapa de Identidade: Imagine que voc\u00ea esteja comprando frutas em uma loja online. Cada fruta que voc\u00ea adiciona ao seu carrinho recebe um c\u00f3digo de barras \u00fanico, para a loja saber exatamente qual fruta voc\u00ea quer. O Mapa de Identidade no SQLAlchemy \u00e9 esse sistema de c\u00f3digo de barras: ele garante que cada objeto na sess\u00e3o seja \u00fanico e facilmente identific\u00e1vel.

  2. Reposit\u00f3rio: A sess\u00e3o tamb\u00e9m atua como um reposit\u00f3rio. Isso significa que ela \u00e9 como um porteiro: ela controla todas as comunica\u00e7\u00f5es entre o seu c\u00f3digo Python e o banco de dados. Todos os comandos que voc\u00ea deseja enviar para o banco de dados devem passar pela sess\u00e3o.

  3. Unidade de Trabalho: Finalmente, a sess\u00e3o age como uma unidade de trabalho. Isso significa que ela mant\u00e9m o controle de todas as altera\u00e7\u00f5es que voc\u00ea quer fazer no banco de dados. Se voc\u00ea adicionar uma fruta ao seu carrinho e depois mudar de ideia e remover, a sess\u00e3o lembrar\u00e1 de ambas as a\u00e7\u00f5es. Ent\u00e3o, quando voc\u00ea finalmente decidir finalizar a compra, ela enviar\u00e1 todas as suas altera\u00e7\u00f5es para o banco de dados de uma s\u00f3 vez.

Entender esses conceitos \u00e9 importante, pois nos ajuda a entender melhor como o SQLAlchemy funciona e como podemos us\u00e1-lo de forma mais eficaz. Agora que temos uma ideia do que \u00e9 uma sess\u00e3o, configuraremos uma para nosso projeto.

Para isso, criaremos a fun\u00e7\u00e3o get_session e tamb\u00e9m definiremos Session no arquivo database.py:

fast_zero/database.py
from sqlalchemy import create_engine\nfrom sqlalchemy.orm import Session\n\nfrom fast_zero.settings import Settings\n\nengine = create_engine(Settings().DATABASE_URL)\n\n\ndef get_session():#(1)!\n    with Session(engine) as session:\n        yield session\n
  1. Quando os testes forem executados, essa linha nunca ser\u00e1 coberta. Pois os testes substitui\u00e7\u00e3o esse bloco por uma fixture em tempo de execu\u00e7\u00e3o aqui. Uma forma de fugir disso e explicar ao coverage que esse bloco n\u00e3o dever\u00e1 ser considerado na cobertura \u00e9 adicionar o coment\u00e1rio # pragma: no cover. Isso far\u00e1 ele ignorar esse bloco na contagem.
"},{"location":"05/#gerenciando-dependencias-com-fastapi","title":"Gerenciando Depend\u00eancias com FastAPI","text":"

Assim como a sess\u00e3o SQLAlchemy, que implementa v\u00e1rios padr\u00f5es arquiteturais importantes, FastAPI tamb\u00e9m usa um conceito de padr\u00e3o arquitetural chamado \"Inje\u00e7\u00e3o de Depend\u00eancia\".

No mundo do desenvolvimento de software, uma \"depend\u00eancia\" \u00e9 um componente que um m\u00f3dulo de software precisa para realizar sua fun\u00e7\u00e3o. Imagine um m\u00f3dulo como uma f\u00e1brica e as depend\u00eancias como as partes ou mat\u00e9rias-primas que a f\u00e1brica precisa para produzir seus produtos. Em vez de a f\u00e1brica ter que buscar essas pe\u00e7as por conta pr\u00f3pria (o que seria ineficiente), elas s\u00e3o entregues \u00e0 f\u00e1brica, prontas para serem usadas. Este \u00e9 o conceito de Inje\u00e7\u00e3o de Depend\u00eancia.

A Inje\u00e7\u00e3o de Depend\u00eancia permite que mantenhamos um baixo n\u00edvel de acoplamento entre diferentes m\u00f3dulos de um sistema. As depend\u00eancias entre os m\u00f3dulos n\u00e3o s\u00e3o definidas no c\u00f3digo, mas sim pela configura\u00e7\u00e3o de uma infraestrutura de software (container) respons\u00e1vel por \"injetar\" em cada componente suas depend\u00eancias declaradas.

Em termos pr\u00e1ticos, o que isso significa \u00e9 que, em vez de cada parte do nosso c\u00f3digo ter que criar suas pr\u00f3prias inst\u00e2ncias de classes ou servi\u00e7os de que depende (o que pode levar a duplica\u00e7\u00e3o de c\u00f3digo e tornar os testes mais dif\u00edceis), essas inst\u00e2ncias s\u00e3o criadas uma vez e depois injetadas onde s\u00e3o necess\u00e1rias.

FastAPI fornece a fun\u00e7\u00e3o Depends para ajudar a declarar e gerenciar essas depend\u00eancias. \u00c9 uma maneira declarativa de dizer ao FastAPI: \"Antes de executar esta fun\u00e7\u00e3o, execute primeiro essa outra fun\u00e7\u00e3o e passe-me o resultado\". Isso \u00e9 especialmente \u00fatil quando temos opera\u00e7\u00f5es que precisam ser realizadas antes de cada request, como abrir uma sess\u00e3o de banco de dados.

"},{"location":"05/#modificando-o-endpoint-post-users","title":"Modificando o Endpoint POST /users","text":"

Agora que temos a nossa sess\u00e3o de banco de dados gerenciada por meio do FastAPI e da inje\u00e7\u00e3o de depend\u00eancias, atualizaremos nossos endpoints para poderem tirar proveito disso. Come\u00e7aremos com a rota de POST para a cria\u00e7\u00e3o de usu\u00e1rios. Ao inv\u00e9s de usarmos o banco de dados falso que criamos inicialmente, agora faremos a inser\u00e7\u00e3o real dos usu\u00e1rios no nosso banco de dados.

Para isso, vamos modificar o nosso endpoint da seguinte maneira:

fast_zero/app.py
from http import HTTPStatus\n\nfrom fastapi import Depends, FastAPI, HTTPException\nfrom sqlalchemy import select\nfrom sqlalchemy.orm import Session\n\nfrom fast_zero.database import get_session\nfrom fast_zero.models import User\nfrom fast_zero.schemas import Message, UserDB, UserList, UserPublic, UserSchema\n\n# ...\n\n\n@app.post('/users/', status_code=HTTPStatus.CREATED, response_model=UserPublic)\ndef create_user(user: UserSchema, session: Session = Depends(get_session)):#(1)!\n    db_user = session.scalar(\n        select(User).where(\n            (User.username == user.username) | (User.email == user.email)#(2)!\n        )\n    )\n\n    if db_user:\n        if db_user.username == user.username:#(3)!\n            raise HTTPException(\n                status_code=HTTPStatus.BAD_REQUEST,\n                detail='Username already exists',\n            )\n        elif db_user.email == user.email:\n            raise HTTPException(\n                status_code=HTTPStatus.BAD_REQUEST,\n                detail='Email already exists',\n            )\n\n    db_user = User(\n        username=user.username, password=user.password, email=user.email\n    )\n    session.add(db_user)\n    session.commit()\n    session.refresh(db_user)\n\n    return db_user\n
  1. session: Session = Depends(get_session) diz que a fun\u00e7\u00e3o get_session ser\u00e1 executada antes da execu\u00e7\u00e3o da fun\u00e7\u00e3o e o valor retornado por get_session ser\u00e1 atribu\u00eddo ao par\u00e2metro session.
  2. Faz uma busca por User onde (where) o username \u00e9 igual ao que veio no request, ou (|) o email \u00e9 igual ao que veio no request. A busca tem o objetivo de achar um registro que tenha ou o email ou o username cadastrado. Pois, eles s\u00e3o \u00fanicos na base de dados. Precisamos validar para ver se j\u00e1 n\u00e3o constam na base de dados.
  3. Por conta do ou (|), precisamos validar o que \u00e9 que j\u00e1 existe na base. Se \u00e9 o username ou se \u00e9 o email

Vamos analisar esse c\u00f3digo com um pouco de calma, diversas coisas est\u00e3o acontecendo aqui, ent\u00e3o, vamos ter um pouco mais de cuidado em um \"bloco a bloco\":

Ao final disso, temos uma integra\u00e7\u00e3o entre o m\u00e9todo POST da API e uma inser\u00e7\u00e3o no banco de dados.

"},{"location":"05/#testando-o-endpoint-post-users-com-pytest-e-fixtures","title":"Testando o Endpoint POST /users com Pytest e Fixtures","text":"

Agora que nossa rota de POST est\u00e1 funcionando com o banco de dados real, precisamos atualizar nossos testes para refletir essa mudan\u00e7a. Como estamos usando a inje\u00e7\u00e3o de depend\u00eancias, precisamos tamb\u00e9m usar essa funcionalidade nos nossos testes para podermos injetar a sess\u00e3o de banco de dados de teste.

Alteraremos a nossa fixture client para substituir a fun\u00e7\u00e3o get_session que estamos injetando no endpoint pela sess\u00e3o do banco em mem\u00f3ria que j\u00e1 t\u00ednhamos definido para banco de dados.

tests/conftest.py
import pytest\nfrom fastapi.testclient import TestClient\nfrom sqlalchemy import create_engine\nfrom sqlalchemy.orm import Session\n\nfrom fast_zero.app import app\nfrom fast_zero.database import get_session\nfrom fast_zero.models import table_registry\n\n\n@pytest.fixture\ndef client(session):\n    def get_session_override():#(1)!\n        return session\n\n    with TestClient(app) as client:\n        app.dependency_overrides[get_session] = get_session_override#(2)!\n        yield client\n\n    app.dependency_overrides.clear()#(3)!\n\n# ...\n
  1. Criamos uma fun\u00e7\u00e3o para retornar nossa fixture de session definida anteriormente.
  2. Substitui a fun\u00e7\u00e3o get_session que usamos para a aplica\u00e7\u00e3o real, pela nossa fun\u00e7\u00e3o que retorna a fixture de testes.
  3. Limpa a sobrescrita que fizemos no app para usar a fixture de session.

Com isso, quando o FastAPI tentar injetar a sess\u00e3o em nossos endpoints, ele injetar\u00e1 a sess\u00e3o de teste que definimos, em vez da sess\u00e3o real. E como estamos usando um banco de dados em mem\u00f3ria para os testes, nossos testes n\u00e3o v\u00e3o interferir nos dados reais do nosso aplicativo.

tests/test_app.py
def test_create_user(client):\n    response = client.post(\n        '/users',\n        json={\n            'username': 'alice',\n            'email': 'alice@example.com',\n            'password': 'secret',\n        },\n    )\n    assert response.status_code == HTTPStatus.CREATED\n    assert response.json() == {\n        'username': 'alice',\n        'email': 'alice@example.com',\n        'id': 1,\n    }\n

Agora que temos a nossa fixture configurada, atualizaremos o nosso teste test_create_user para usar o novo cliente de teste e verificar que o usu\u00e1rio est\u00e1 sendo realmente criado no banco de dados.

$ Execu\u00e7\u00e3o no terminal!
task test\n\n# ...\n\ntests/test_app.py::test_root_deve_retornar_ok_e_ola_mundo PASSED\ntests/test_app.py::test_create_user FAILED\n

O nosso teste ainda n\u00e3o consegue ser executado, mas existe um motivo para isso.

"},{"location":"05/#threads-e-conexoes","title":"Threads e conex\u00f5es","text":"

No ambiente de testes do FastAPI, a aplica\u00e7\u00e3o e os testes podem rodar em threads diferentes. Isso pode levar a um erro com o SQLite, pois os objetos SQLite criados em uma thread s\u00f3 podem ser usados na mesma thread.

Para contornar isso, adicionaremos os seguintes par\u00e2metros na cria\u00e7\u00e3o da engine:

  1. connect_args={'check_same_thread': False}: essa configura\u00e7\u00e3o desativa a verifica\u00e7\u00e3o de que o objeto SQLite est\u00e1 sendo usado na mesma thread em que foi criado. Isso permite que a conex\u00e3o seja compartilhada entre threads diferentes sem levar a erros.

  2. poolclass=StaticPool: esse par\u00e2metro faz com que a engine use um pool de conex\u00f5es est\u00e1tico, ou seja, reutilize a mesma conex\u00e3o para todas as solicita\u00e7\u00f5es. Isso garante que as duas threads usem o mesmo canal de comunica\u00e7\u00e3o, evitando erros relacionados ao uso de diferentes conex\u00f5es em threads diferentes.

Assim, nossa fixture deve ficar dessa forma:

tests/conftest.py
from sqlalchemy.pool import StaticPool\n\n# ...\n\n@pytest.fixture\ndef session():\n    engine = create_engine(\n        'sqlite:///:memory:',\n        connect_args={'check_same_thread': False},\n        poolclass=StaticPool,\n    )\n    table_registry.metadata.create_all(engine)\n\n    with Session(engine) as session:\n        yield session\n\n    table_registry.metadata.drop_all(engine)\n

Depois de realizar essas mudan\u00e7as, podemos executar nossos testes e verificar se est\u00e3o passando. Por\u00e9m, embora o teste test_create_user tenha passado, precisamos agora ajustar os outros endpoints para que eles tamb\u00e9m utilizem a nossa sess\u00e3o de banco de dados.

$ Execu\u00e7\u00e3o no terminal!
task test\n\n# ...\n\ntests/test_app.py::test_create_user PASSED\ntests/test_app.py::test_read_users FAILED\ntests/test_app.py::test_update_user FAILED\n\n# ...\n

Nos pr\u00f3ximos passos, vamos realizar essas modifica\u00e7\u00f5es para garantir que todo o nosso aplicativo esteja usando o banco de dados real.

"},{"location":"05/#modificando-o-endpoint-get-users","title":"Modificando o Endpoint GET /users","text":"

Agora que temos o nosso banco de dados configurado e funcionando, \u00e9 o momento de atualizar o nosso endpoint de GET para interagir com o banco de dados real. Em vez de trabalhar com uma lista fict\u00edcia de usu\u00e1rios, queremos buscar os usu\u00e1rios diretamente do nosso banco de dados, permitindo uma intera\u00e7\u00e3o din\u00e2mica e real com os dados.

fast_zero/app.py
@app.get('/users/', response_model=UserList)\ndef read_users(\n    skip: int = 0, limit: int = 100, session: Session = Depends(get_session)\n):\n    users = session.scalars(select(User).offset(skip).limit(limit)).all()\n    return {'users': users}\n

Neste c\u00f3digo, adicionamos algumas funcionalidades essenciais para a busca de dados. Os par\u00e2metros offset e limit s\u00e3o utilizados para paginar os resultados, o que \u00e9 especialmente \u00fatil quando se tem um grande volume de dados.

Os par\u00e2metros sendo passados na fun\u00e7\u00e3o, como skip e limit, se tornam par\u00e2metros de query. Se olharmos no swagger. Podemos ver que isso agora \u00e9 parametriz\u00e1vel durante a chamada:

Isso faz com que as chamadas para o endpoint possa ser realizadas desta forma: http://localhost:8000/users/?skip=0&limit=100. Passando skip=0 ou limit=100. Esses valores podem mudar a quantidade e os registros que ser\u00e3o retornados pelo banco de dados. Por padr\u00e3o ser\u00e3o retornados 100 registros iniciando no 0.

Recomendo que voc\u00ea insira diversos valores no banco de dados e teste a varia\u00e7\u00e3o dos par\u00e2metros. Pode ser bastante divertido.

Essas adi\u00e7\u00f5es tornam o nosso endpoint mais flex\u00edvel e otimizado para lidar com diferentes cen\u00e1rios de uso.

"},{"location":"05/#testando-o-endpoint-get-users","title":"Testando o Endpoint GET /users","text":"

Com a mudan\u00e7a para o banco de dados real, nosso banco de dados de teste ser\u00e1 sempre resetado para cada teste. Portanto, n\u00e3o podemos mais executar o teste que t\u00ednhamos antes, pois n\u00e3o haver\u00e1 usu\u00e1rios no banco. Para verificar se o nosso endpoint est\u00e1 funcionando corretamente, criaremos um novo teste que solicita uma lista de usu\u00e1rios de um banco vazio:

tests/test_app.py
def test_read_users(client):\n    response = client.get('/users')\n    assert response.status_code == HTTPStatus.OK\n    assert response.json() == {'users': []}\n

Agora que temos nosso novo teste, podemos execut\u00e1-lo para verificar se o nosso endpoint GET est\u00e1 funcionando corretamente. Com esse novo teste, a fun\u00e7\u00e3o test_read_users deve passar.

$ Execu\u00e7\u00e3o no terminal!
task test\n\n# ...\n\ntests/test_app.py::test_create_user PASSED\ntests/test_app.py::test_read_users PASSED\ntests/test_app.py::test_update_user FAILED\n

Por\u00e9m, \u00e9 claro, queremos tamb\u00e9m testar o caso em que existem usu\u00e1rios no banco. Para isso, criaremos uma nova fixture que cria um usu\u00e1rio em nosso banco de dados de teste.

"},{"location":"05/#criando-uma-fixture-para-user","title":"Criando uma fixture para User","text":"

Para criar essa fixture, aproveitaremos a nossa fixture de sess\u00e3o do SQLAlchemy, e criar um novo usu\u00e1rio dentro dela:

tests/conftest.py
from fast_zero.models import User, table_registry\n\n# ...\n\n@pytest.fixture\ndef user(session):\n    user = User(username='Teste', email='teste@test.com', password='testtest')\n    session.add(user)\n    session.commit()\n    session.refresh(user)\n\n    return user\n

Com essa fixture, sempre que precisarmos de um usu\u00e1rio em nossos testes, podemos simplesmente passar user como um argumento para nossos testes, e o Pytest se encarregar\u00e1 de criar um novo usu\u00e1rio para n\u00f3s.

Agora podemos criar um novo teste para verificar se o nosso endpoint est\u00e1 retornando o usu\u00e1rio correto quando existe um usu\u00e1rio no banco:

tests/test_app.py
from fast_zero.schemas import UserPublic\n\n# ...\n\n\ndef test_read_users_with_users(client, user):\n    user_schema = UserPublic.model_validate(user).model_dump()\n    response = client.get('/users/')\n    assert response.json() == {'users': [user_schema]}\n

Agora podemos rodar o nosso teste novamente e verificar se ele est\u00e1 passando:

$ Execu\u00e7\u00e3o no terminal!
task test\n\n# ...\n\ntests/test_app.py::test_create_user PASSED\ntests/test_app.py::test_read_users PASSED\ntests/test_app.py::test_read_users_with_users FAILED\n

No entanto, mesmo que nosso c\u00f3digo pare\u00e7a correto, podemos encontrar um problema: o Pydantic n\u00e3o consegue converter diretamente nosso modelo SQLAlchemy para um modelo Pydantic. Resolveremos isso agora.

"},{"location":"05/#integrando-o-schema-ao-model","title":"Integrando o Schema ao Model","text":"

A integra\u00e7\u00e3o direta do ORM com o nosso esquema Pydantic n\u00e3o \u00e9 imediata e exige algumas modifica\u00e7\u00f5es. O Pydantic, por padr\u00e3o, n\u00e3o sabe como lidar com os modelos do SQLAlchemy, o que nos leva ao erro observado nos testes.

A solu\u00e7\u00e3o para esse problema passa por fazer uma altera\u00e7\u00e3o no esquema UserPublic que utilizamos, para que ele possa reconhecer e trabalhar com os modelos do SQLAlchemy. Isso permite que os objetos do SQLAlchemy sejam convertidos corretamente para os esquemas Pydantic.

Para resolver o problema de convers\u00e3o entre SQLAlchemy e Pydantic, precisamos atualizar o nosso esquema UserPublic para que ele possa reconhecer os modelos do SQLAlchemy. Para isso, adicionaremos a linha model_config = ConfigDict(from_attributes=True) ao nosso esquema:

fast_zero/schemas.py
from pydantic import BaseModel, ConfigDict, EmailStr\n\n# ...\n\nclass UserPublic(BaseModel):\n    id: int\n    username: str\n    email: EmailStr\n    model_config = ConfigDict(from_attributes=True)\n

Com essa mudan\u00e7a, nosso esquema Pydantic agora pode ser convertido a partir de um modelo SQLAlchemy. Agora podemos executar nosso teste novamente e verificar se ele est\u00e1 passando.

$ Execu\u00e7\u00e3o no terminal!
task test\n\n# ...\n\ntests/test_app.py::test_root_deve_retornar_ok_e_ola_mundo PASSED\ntests/test_app.py::test_create_user PASSED\ntests/test_app.py::test_read_users PASSED\ntests/test_app.py::test_read_users_with_users PASSED\ntests/test_app.py::test_update_user FAILED\n

Agora que temos nosso endpoint GET funcionando corretamente e testado, podemos seguir para o endpoint PUT, e continuar com o processo de atualiza\u00e7\u00e3o dos nossos endpoints.

"},{"location":"05/#modificando-o-endpoint-put-users","title":"Modificando o Endpoint PUT /users","text":"

Agora, modificaremos o endpoint de PUT para suportar o banco de dados, como fizemos com os endpoints POST e GET:

fast_zero/app.py
@app.put('/users/{user_id}', response_model=UserPublic)\ndef update_user(\n    user_id: int, user: UserSchema, session: Session = Depends(get_session)\n):\n\n    db_user = session.scalar(select(User).where(User.id == user_id))\n    if not db_user:\n        raise HTTPException(\n            status_code=HTTPStatus.NOT_FOUND, detail='User not found'\n        )\n\n    db_user.username = user.username\n    db_user.password = user.password\n    db_user.email = user.email\n    session.commit()\n    session.refresh(db_user)\n\n    return db_user\n

Semelhante ao que fizemos antes, estamos injetando a sess\u00e3o do SQLAlchemy em nosso endpoint e utilizando-a para buscar o usu\u00e1rio a ser atualizado. Se o usu\u00e1rio n\u00e3o for encontrado, retornamos um erro 404.

As linhas destacadas mostram que estamos atribuindo novos valores aos atributos username, password e email. Essa opera\u00e7\u00e3o, seguida por um commit, altera os valores existentes no banco. \u00c9 como se a atribui\u00e7\u00e3o nova, fosse uma atualiza\u00e7\u00e3o do dado da tabela.

Antes de partirmos para o pr\u00f3ximo passo, ao executar nosso linter, ele ir\u00e1 apontar um erro informando que importamos UserDB mas, nunca o usamos.

$ Execu\u00e7\u00e3o no terminal!
task lint\nfast_zero/app.py:9:40: F401 [*] `fast_zero.schemas.UserDB` imported but unused\nFound 1 error.\n[*] 1 fixable with the `--fix` option.\n--- fast_zero/app.py\n+++ fast_zero/app.py\n@@ -6,7 +6,7 @@\n\n from fast_zero.database import get_session\n from fast_zero.models import User\n-from fast_zero.schemas import Message, UserDB, UserList, UserPublic, UserSchema\n+from fast_zero.schemas import Message, UserList, UserPublic, UserSchema\n\n app = FastAPI()\n\n\nWould fix 1 error.\n

Isso ocorre porque a rota PUT era a \u00fanica que estava utilizando UserDB, e agora que modificamos esta rota, podemos remover UserDB dos nossos imports e tamb\u00e9m excluir sua defini\u00e7\u00e3o no arquivo schemas.py

Sobre o arquivo schemas.py

Caso fique em d\u00favida sobre o que remover, seu arquivo schemas.py deve estar parecido com isso, ap\u00f3s a remo\u00e7\u00e3o de UserDB:

schemas.py
from pydantic import BaseModel, ConfigDict, EmailStr\n\n\nclass Message(BaseModel):\n    message: str\n\nclass UserSchema(BaseModel):\n    username: str\n    email: EmailStr\n    password: str\n\n\nclass UserPublic(BaseModel):\n    id: int\n    username: str\n    email: EmailStr\n    model_config = ConfigDict(from_attributes=True)\n\n\nclass UserList(BaseModel):\n    users: list[UserPublic]\n
"},{"location":"05/#adicionando-o-teste-do-put","title":"Adicionando o teste do PUT","text":"

Tamb\u00e9m precisamos alterar o teste para o endpoint de PUT, para que exista um usu\u00e1rio na base para ser alterado:

tests/test_app.py
def test_update_user(client, user):\n    response = client.put(\n        '/users/1',\n        json={\n            'username': 'bob',\n            'email': 'bob@example.com',\n            'password': 'mynewpassword',\n        },\n    )\n    assert response.status_code == HTTPStatus.OK\n    assert response.json() == {\n        'username': 'bob',\n        'email': 'bob@example.com',\n        'id': 1,\n    }\n
"},{"location":"05/#modificando-o-endpoint-delete-users","title":"Modificando o Endpoint DELETE /users","text":"

Em seguida, modificamos o endpoint DELETE da mesma maneira:

fast_zero/app.py
@app.delete('/users/{user_id}', response_model=Message)\ndef delete_user(user_id: int, session: Session = Depends(get_session)):\n    db_user = session.scalar(select(User).where(User.id == user_id))\n\n    if not db_user:\n        raise HTTPException(\n            status_code=HTTPStatus.NOT_FOUND, detail='User not found'\n        )\n\n    session.delete(db_user)#(1)!\n    session.commit()\n\n    return {'message': 'User deleted'}\n
  1. O m\u00e9todo delete da session adiciona uma opera\u00e7\u00e3o de dele\u00e7\u00e3o na sess\u00e3o. Na sequ\u00eancia o m\u00e9todo commit tem que ser chamado para que a opera\u00e7\u00e3o seja performada.

Neste caso, estamos novamente usando a sess\u00e3o do SQLAlchemy para encontrar o usu\u00e1rio a ser deletado e, em seguida, exclu\u00edmos esse usu\u00e1rio do banco de dados.

"},{"location":"05/#adicionando-testes-para-delete","title":"Adicionando testes para DELETE","text":"

Assim como para o endpoint PUT, precisamos alterar o teste para o nosso endpoint DELETE, pois n\u00e3o existe um user na base:

tests/test_app.py
def test_delete_user(client, user):\n    response = client.delete('/users/1')\n    assert response.status_code == HTTPStatus.OK\n    assert response.json() == {'message': 'User deleted'}\n
"},{"location":"05/#cobertura-e-testes-nao-feitos","title":"Cobertura e testes n\u00e3o feitos","text":"

Com o banco de dados agora em funcionamento, podemos verificar a cobertura de c\u00f3digo do arquivo fast_zero/app.py. Se olharmos para a imagem abaixo, vemos que ainda h\u00e1 alguns casos que n\u00e3o testamos. Por exemplo, o que acontece quando tentamos atualizar ou excluir um usu\u00e1rio que n\u00e3o existe?

Esses tr\u00eas casos ficam como exerc\u00edcios para quem est\u00e1 acompanhando este curso.

Al\u00e9m disso, n\u00e3o devemos esquecer de remover a implementa\u00e7\u00e3o do banco de dados falso database = [] que usamos inicialmente e remover tamb\u00e9m as defini\u00e7\u00f5es de TestClient em test_app.py, pois tudo est\u00e1 usando as fixtures agora!

"},{"location":"05/#commit","title":"Commit","text":"

Agora que terminamos a atualiza\u00e7\u00e3o dos nossos endpoints, faremos o commit das nossas altera\u00e7\u00f5es. O processo \u00e9 o seguinte:

$ Execu\u00e7\u00e3o no terminal!
git add .\ngit commit -m \"Atualizando endpoints para usar o banco de dados real\"\ngit push\n

Com isso, terminamos a atualiza\u00e7\u00e3o dos nossos endpoints para usar o nosso banco de dados real.

"},{"location":"05/#exercicios","title":"Exerc\u00edcios","text":"
  1. Escrever um teste para o endpoint de POST (create_user) que contemple o cen\u00e1rio onde o username j\u00e1 foi registrado. Validando o erro 400;
  2. Escrever um teste para o endpoint de POST (create_user) que contemple o cen\u00e1rio onde o e-mail j\u00e1 foi registrado. Validando o erro 400;
  3. Atualizar os testes criados nos exerc\u00edcios 1 e 2 da aula 03 para suportarem o banco de dados;
  4. Implementar o banco de dados para o endpoint de listagem por id, criado no exerc\u00edcio 3 da aula 03.

Exerc\u00edcios resolvidos

"},{"location":"05/#conclusao","title":"Conclus\u00e3o","text":"

Parab\u00e9ns por chegar ao final desta aula! Voc\u00ea deu um passo significativo no desenvolvimento de nossa aplica\u00e7\u00e3o, substituindo a implementa\u00e7\u00e3o do banco de dados falso pela integra\u00e7\u00e3o com um banco de dados real usando SQLAlchemy. Tamb\u00e9m vimos como ajustar os nossos testes para considerar essa nova realidade.

Nesta aula, abordamos como modificar os endpoints para interagir com o banco de dados real e como utilizar a inje\u00e7\u00e3o de depend\u00eancias do FastAPI para gerenciar nossas sess\u00f5es do SQLAlchemy. Tamb\u00e9m discutimos a import\u00e2ncia dos testes para garantir que nossos endpoints est\u00e3o funcionando corretamente, e como as fixtures do Pytest podem nos auxiliar na prepara\u00e7\u00e3o do ambiente para esses testes.

Al\u00e9m disso, nos deparamos com situa\u00e7\u00f5es onde o Pydantic e o SQLAlchemy n\u00e3o interagem perfeitamente bem, e como solucionar esses casos.

No final desta aula, voc\u00ea deve estar confort\u00e1vel em integrar um banco de dados real a uma aplica\u00e7\u00e3o FastAPI, saber como escrever testes robustos que levem em considera\u00e7\u00e3o a intera\u00e7\u00e3o com o banco de dados, e estar ciente de poss\u00edveis desafios ao trabalhar com Pydantic e SQLAlchemy juntos.

Agora que a aula acabou, \u00e9 um bom momento para voc\u00ea relembrar alguns conceitos e fixar melhor o conte\u00fado respondendo ao question\u00e1rio referente a ela.

Quiz

"},{"location":"06/","title":"Autentica\u00e7\u00e3o e Autoriza\u00e7\u00e3o com JWT","text":""},{"location":"06/#autenticacao-e-autorizacao-com-jwt","title":"Autentica\u00e7\u00e3o e Autoriza\u00e7\u00e3o com JWT","text":"

Objetivos da Aula:

Caso prefira ver a aula em v\u00eddeo

Esse aula ainda n\u00e3o est\u00e1 dispon\u00edvel em formato de v\u00eddeo, somente em texto ou live!

Aula Slides C\u00f3digo Quiz Exerc\u00edcios

"},{"location":"06/#introducao","title":"Introdu\u00e7\u00e3o","text":"

Nesta aula, abordaremos dois aspectos cruciais de qualquer aplica\u00e7\u00e3o web: a autentica\u00e7\u00e3o e a autoriza\u00e7\u00e3o. At\u00e9 agora, nossos usu\u00e1rios podem criar, ler, atualizar e deletar suas contas, mas qualquer pessoa pode fazer essas a\u00e7\u00f5es. N\u00e3o queremos que qualquer usu\u00e1rio possa deletar ou modificar a conta de outro usu\u00e1rio. Para evitar isso, vamos implementar autentica\u00e7\u00e3o e autoriza\u00e7\u00e3o em nossa aplica\u00e7\u00e3o.

A autentica\u00e7\u00e3o \u00e9 o processo de verificar quem um usu\u00e1rio \u00e9, enquanto a autoriza\u00e7\u00e3o \u00e9 o processo de verificar o que ele tem permiss\u00e3o para fazer. Usaremos o JSON Web Token (JWT) para implementar a autentica\u00e7\u00e3o, e adicionaremos l\u00f3gica de autoriza\u00e7\u00e3o aos nossos endpoints.

Al\u00e9m disso, at\u00e9 agora, estamos armazenando as senhas dos usu\u00e1rios como texto puro no banco de dados, o que \u00e9 uma pr\u00e1tica insegura. Corrigiremos isso utilizando a biblioteca pwdlib para encriptar as senhas.

"},{"location":"06/#o-que-e-um-jwt","title":"O que \u00e9 um JWT","text":"

O JWT \u00e9 um padr\u00e3o (RFC 7519) que define uma maneira compacta e aut\u00f4noma de transmitir informa\u00e7\u00f5es entre as partes de maneira segura. Essas informa\u00e7\u00f5es s\u00e3o transmitidas como um objeto JSON que \u00e9 digitalmente assinado usando um segredo (com o algoritmo HMAC) ou um par de chaves p\u00fablica/privada usando RSA, ou ECDSA.

Um JWT consiste em tr\u00eas partes:

  1. Header: O cabe\u00e7alho do JWT consiste tipicamente em dois componentes: o tipo de token, que \u00e9 JWT neste caso, e o algoritmo de assinatura, como HMAC SHA256 ou RSA. Essas informa\u00e7\u00f5es s\u00e3o codificadas em Base64Url e formam a primeira parte do JWT.

    {\n   \"alg\": \"HS256\",\n   \"typ\": \"JWT\"\n}\n
  2. Payload: O payload de um JWT \u00e9 onde as reivindica\u00e7\u00f5es (em ingl\u00eas claims) s\u00e3o armazenadas. As reivindica\u00e7\u00f5es s\u00e3o informa\u00e7\u00f5es que queremos transmitir e que s\u00e3o relevantes para a intera\u00e7\u00e3o entre o cliente e o servidor. As reivindica\u00e7\u00f5es s\u00e3o codificadas em Base64Url e formam a segunda parte do JWT.

    {\n  \"sub\": \"teste@test.com\",\n  \"exp\": 1690258153\n}\n
  3. Signature: A assinatura \u00e9 utilizada para verificar que o remetente do JWT \u00e9 quem afirma ser e para garantir que a mensagem n\u00e3o foi alterada ao longo do caminho. Para criar a assinatura, voc\u00ea precisa codificar o cabe\u00e7alho, o payload, e um segredo utilizando o algoritmo especificado no cabe\u00e7alho. A assinatura \u00e9 a terceira parte do JWT. Uma assinatura de JWT pode ser criada como se segue:

    HMACSHA256(\n    base64UrlEncode(header) + \".\" +\n    base64UrlEncode(payload),\n nosso-segredo\n)\n

Essas tr\u00eas partes s\u00e3o separadas por pontos (.) e juntas formam um token JWT.

Formando a estrutura: HEADER.PAYLOAD.SIGNATURE que formam um token parecido com

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ0ZXN0ZUB0ZXN0LmNvbSIsImV4cCI6MTY5MDI1ODE1M30.Nx0P_ornVwJBH_LLLVrlJoh6RmJeXR-Nr7YJ_mlGY04\n

\u00c9 importante ressaltar que, apesar de a informa\u00e7\u00e3o em um JWT estar codificada, ela n\u00e3o est\u00e1 criptografada. Isso significa que qualquer pessoa com acesso ao token pode decodificar e ler as informa\u00e7\u00f5es nele. No entanto, sem o segredo usado para assinar o token, eles n\u00e3o podem alterar as informa\u00e7\u00f5es ou forjar um novo token. Portanto, n\u00e3o devemos incluir informa\u00e7\u00f5es sens\u00edveis ou confidenciais no payload do JWT.

Se quisermos ver o header, o payload e a assinatura contidas nesse token podemos acessar o debuger do jwt e checar quais as informa\u00e7\u00f5es que est\u00e3o nesse token:

"},{"location":"06/#claims","title":"Claims","text":"

As Claims do JWT s\u00e3o as informa\u00e7\u00f5es que ser\u00e3o adicionadas ao token via payload. Como:

{\n    \"sub\": \"teste@test.com\",\n    \"exp\": 1690258153\n}\n

Onde as chaves deste exemplo:

Em nossos exemplos iremos usar somente essas duas claims, mais existem muitas outras. Voc\u00ea pode ver a lista completa das claims aqui caso queira aprender mais.

"},{"location":"06/#como-funciona-o-jwt","title":"Como funciona o JWT","text":"

Em uma aplica\u00e7\u00e3o web, o processo de autentica\u00e7\u00e3o geralmente funciona da seguinte maneira:

  1. O usu\u00e1rio envia suas credenciais (e-mail e senha) para o servidor em um endpoint de gera\u00e7\u00e3o de token (/token por exemplo);
  2. O servidor verifica as credenciais e, se estiverem corretas, gera um token JWT e o envia de volta ao cliente;
  3. Nas solicita\u00e7\u00f5es subsequentes, o cliente deve incluir esse token no cabe\u00e7alho de autoriza\u00e7\u00e3o de suas solicita\u00e7\u00f5es. Como, por exemplo: Authorization: Bearer <token>;
  4. Quando o servidor recebe uma solicita\u00e7\u00e3o com um token JWT, ele pode verificar a assinatura e se o token \u00e9 v\u00e1lido e n\u00e3o expirou, ele processa a solicita\u00e7\u00e3o.
sequenceDiagram\n  participant Cliente as Cliente\n  participant Servidor as Servidor\n  Cliente->>Servidor: Envia credenciais (e-mail e senha)\n  Servidor->>Cliente: Verifica as credenciais\n  Servidor->>Cliente: Envia token JWT\n  Cliente->>Servidor: Envia solicita\u00e7\u00e3o com token JWT no cabe\u00e7alho de autoriza\u00e7\u00e3o\n  Servidor->>Cliente: Verifica o token JWT e processa a solicita\u00e7\u00e3o

Nos pr\u00f3ximos t\u00f3picos, vamos detalhar como podemos gerar e verificar tokens JWT em nossa aplica\u00e7\u00e3o FastAPI, bem como adicionar autentica\u00e7\u00e3o e autoriza\u00e7\u00e3o aos nossos endpoints.

"},{"location":"06/#gerando-tokens-jwt","title":"Gerando tokens JWT","text":"

Para gerar tokens JWT, precisamos de duas bibliotecas extras: pyjwt e pwdlib. A primeira ser\u00e1 usada para a gera\u00e7\u00e3o do token, enquanto a segunda ser\u00e1 usada para criptografar as senhas dos usu\u00e1rios. Para instal\u00e1-las, execute o seguinte comando no terminal:

$ Execu\u00e7\u00e3o no terminal!
poetry add pyjwt \"pwdlib[argon2]\"\n

Agora, criaremos uma fun\u00e7\u00e3o para gerar nossos tokens JWT. Criaremos um novo arquivo para gerenciar a seguran\u00e7a: security.py. Nesse arquivo iniciaremos a gera\u00e7\u00e3o dos tokens:

fast_zero/security.py
from datetime import datetime, timedelta\n\nfrom jwt import encode\nfrom pwdlib import PasswordHash\nfrom zoneinfo import ZoneInfo\n\nSECRET_KEY = 'your-secret-key'  # Isso \u00e9 provis\u00f3rio, vamos ajustar!\nALGORITHM = 'HS256'\nACCESS_TOKEN_EXPIRE_MINUTES = 30\npwd_context = PasswordHash.recommended()\n\n\ndef create_access_token(data: dict):\n    to_encode = data.copy()\n    expire = datetime.now(tz=ZoneInfo('UTC')) + timedelta(\n        minutes=ACCESS_TOKEN_EXPIRE_MINUTES\n    )\n    to_encode.update({'exp': expire})\n    encoded_jwt = encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)\n    return encoded_jwt\n

A fun\u00e7\u00e3o create_access_token \u00e9 respons\u00e1vel por criar um novo token JWT que ser\u00e1 usado para autenticar o usu\u00e1rio. Ela recebe um dicion\u00e1rio de dados, adiciona um tempo de expira\u00e7\u00e3o ao token (baseado na constante ACCESS_TOKEN_EXPIRE_MINUTES). Esses dados, em conjunto, formam o payload do JWT. Em seguida, usa a biblioteca pyjwt para codificar essas informa\u00e7\u00f5es em um token JWT, que \u00e9 ent\u00e3o retornado.

Note que a constante SECRET_KEY \u00e9 usada para assinar o token, e o algoritmo HS256 \u00e9 usado para a codifica\u00e7\u00e3o. Em um cen\u00e1rio de produ\u00e7\u00e3o, voc\u00ea deve manter a SECRET_KEY em um local seguro e n\u00e3o exp\u00f4-la em seu c\u00f3digo.

"},{"location":"06/#testando-a-geracao-de-tokens","title":"Testando a gera\u00e7\u00e3o de tokens","text":"

Embora esse c\u00f3digo ser\u00e1 coberto no futuro com a utiliza\u00e7\u00e3o do token, \u00e9 interessante criarmos um teste para essa fun\u00e7\u00e3o com uma finalidade puramente did\u00e1tica. De forma que vejamos os tokens gerados pelo pyjwt e interagirmos com ele.

Com isso criaremos um arquivo chamado tests/test_security.py para efetuar esse teste:

tests/test_security.py
from jwt import decode\n\nfrom fast_zero.security import SECRET_KEY, create_access_token\n\n\ndef test_jwt():\n    data = {'test': 'test'}\n    token = create_access_token(data)\n\n    decoded = decode(token, SECRET_KEY, algorithms=['HS256'])\n\n    assert decoded['test'] == data['test']\n    assert decoded['exp']  # Testa se o valor de exp foi adicionado ao token\n

Na pr\u00f3xima se\u00e7\u00e3o, veremos como podemos usar a biblioteca pwdlib para tratar as senhas dos usu\u00e1rios.

"},{"location":"06/#hashing-de-senhas","title":"Hashing de Senhas","text":"

Armazenar senhas em texto puro \u00e9 uma pr\u00e1tica de seguran\u00e7a extremamente perigosa. Em vez disso, \u00e9 uma pr\u00e1tica padr\u00e3o criptografar (\"hash\") as senhas antes de armazen\u00e1-las. Quando um usu\u00e1rio tenta se autenticar, a senha inserida \u00e9 criptografada novamente e comparada com a vers\u00e3o criptografada armazenada no banco de dados. Se as duas correspondem, o usu\u00e1rio \u00e9 autenticado.

Implementaremos essa funcionalidade usando a biblioteca pwdlib. Criaremos duas fun\u00e7\u00f5es: uma para criar o hash da senha e outra para verificar se uma senha inserida corresponde ao hash armazenado. Adicione o seguinte c\u00f3digo ao arquivo security.py:

fast_zero/security.py
def get_password_hash(password: str):\n    return pwd_context.hash(password)\n\n\ndef verify_password(plain_password: str, hashed_password: str):\n    return pwd_context.verify(plain_password, hashed_password)\n

A fun\u00e7\u00e3o get_password_hash recebe uma senha em texto puro como argumento e retorna uma vers\u00e3o criptografada dessa senha. A fun\u00e7\u00e3o verify_password recebe uma senha em texto puro e uma senha criptografada como argumentos, e verifica se a senha em texto puro, quando criptografada, corresponde \u00e0 senha criptografada. Ambas as fun\u00e7\u00f5es utilizam o objeto pwd_context, que definimos anteriormente usando a biblioteca pwdlib.

Agora, quando um usu\u00e1rio se registra em nossa aplica\u00e7\u00e3o, devemos usar a fun\u00e7\u00e3o get_password_hash para armazenar uma vers\u00e3o criptografada da senha. Quando um usu\u00e1rio tenta se autenticar, devemos usar a fun\u00e7\u00e3o verify_password para verificar se a senha inserida corresponde \u00e0 senha armazenada.

Na pr\u00f3xima se\u00e7\u00e3o, modificaremos nossos endpoints para fazer uso dessas fun\u00e7\u00f5es.

"},{"location":"06/#modificando-o-endpoint-de-post-para-encriptar-a-senha","title":"Modificando o endpoint de POST para encriptar a senha","text":"

Com as fun\u00e7\u00f5es de cria\u00e7\u00e3o de hash de senha e verifica\u00e7\u00e3o de senha em vigor, agora podemos atualizar nossos endpoints para usar essa nova funcionalidade de encripta\u00e7\u00e3o.

Primeiro, modificaremos a fun\u00e7\u00e3o create_user para criar um hash da senha antes de armazen\u00e1-la no banco de dados. Para fazer isso precisamos importar a fun\u00e7\u00e3o de gera\u00e7\u00e3o de hash get_password_hash e no momento da cria\u00e7\u00e3o do registro na tabela a senha deve ser passada com o hash gerado:

fast_zero/app.py
from fast_zero.security import get_password_hash\n\n# ...\n\n@app.post('/users/', status_code=HTTPStatus.CREATED, response_model=UserPublic)\ndef create_user(user: UserSchema, session: Session = Depends(get_session)):\n    db_user = session.scalar(\n        select(User).where(\n            (User.username == user.username) | (User.email == user.email)\n        )\n    )\n\n    if db_user:\n        if db_user.username == user.username:\n            raise HTTPException(\n                status_code=HTTPStatus.BAD_REQUEST,\n                detail='Username already exists',\n            )\n        elif db_user.email == user.email:\n            raise HTTPException(\n                status_code=HTTPStatus.BAD_REQUEST,\n                detail='Email already exists',\n            )\n\n    hashed_password = get_password_hash(user.password)\n\n    db_user = User(\n        email=user.email,\n        username=user.username,\n        password=hashed_password,\n    )\n\n    session.add(db_user)\n    session.commit()\n    session.refresh(db_user)\n\n    return db_user\n

Desta forma, a senha n\u00e3o ser\u00e1 mais criada em texto plano no objeto User. Fazendo com que caso exista algum problema relacionado a vazamento de dados, as senhas das pessoas nunca sejam expostas.

"},{"location":"06/#sobre-o-teste-da-post-users","title":"Sobre o teste da POST /users/","text":"

Por n\u00e3o validar o password, usando o retorno UserPublic, o teste j\u00e1 escrito deve passar normalmente:

$ Execu\u00e7\u00e3o no terminal!
task test\n\n# ...\n\ntests/test_app.py::test_root_deve_retornar_ok_e_ola_mundo PASSED\ntests/test_app.py::test_create_user PASSED\n
"},{"location":"06/#modificando-o-endpoint-de-atualizacao-de-usuarios","title":"Modificando o endpoint de atualiza\u00e7\u00e3o de usu\u00e1rios","text":"

\u00c9 igualmente importante modificar a fun\u00e7\u00e3o update_user para tamb\u00e9m criar um hash da senha antes de atualizar User no banco de dados. Caso contr\u00e1rio, a senha em texto puro seria armazenada no banco de dados no momento da atualiza\u00e7\u00e3o.

fast_zero/app.py
@app.put('/users/{user_id}', response_model=UserPublic)\ndef update_user(\n    user_id: int,\n    user: UserSchema,\n    session: Session = Depends(get_session),\n):\n    db_user = session.scalar(select(User).where(User.id == user_id))\n    if not db_user:\n        raise HTTPException(\n            status_code=HTTPStatus.NOT_FOUND, detail='User not found'\n        )\n\n    db_user.username = user.username\n    db_user.password = get_password_hash(user.password)\n    db_user.email = user.email\n    session.commit()\n    session.refresh(db_user)\n    return db_user\n

Assim, a atualiza\u00e7\u00e3o de um User, via m\u00e9todo PUT, tamb\u00e9m criar\u00e1 o hash da senha no momento da atualiza\u00e7\u00e3o. Pois, nesse caso em espec\u00edfico, existe a possibilidade de alterar qualquer coluna da tabela, inclusive o campo password.

"},{"location":"06/#sobre-os-testes-da-put-usersuser_id","title":"Sobre os testes da PUT /users/{user_id}","text":"

Assim como no teste da rota de cria\u00e7\u00e3o, os testes tamb\u00e9m passam normalmente por n\u00e3o validarem o campo password.

$ Execu\u00e7\u00e3o no terminal!
task test\n\n# ...\n\ntests/test_app.py::test_root_deve_retornar_ok_e_ola_mundo PASSED\ntests/test_app.py::test_create_user PASSED\ntests/test_app.py::test_read_users PASSED\ntests/test_app.py::test_read_users_with_users PASSED\ntests/test_app.py::test_update_user PASSED\n
"},{"location":"06/#criando-um-endpoint-de-geracao-do-token","title":"Criando um endpoint de gera\u00e7\u00e3o do token","text":"

Antes de criar o endpoint, precisamos criar um schema para o nosso token. Em um contexto JWT, access_token \u00e9 o pr\u00f3prio token que representa a sess\u00e3o do usu\u00e1rio e cont\u00e9m informa\u00e7\u00f5es sobre o usu\u00e1rio, enquanto token_type \u00e9 um tipo de autentica\u00e7\u00e3o que ser\u00e1 inclu\u00eddo no cabe\u00e7alho de autoriza\u00e7\u00e3o de cada solicita\u00e7\u00e3o. Em geral, o token_type para JWT \u00e9 \"bearer\".

fast_zero/schemas.py
class Token(BaseModel):\n    access_token: str\n    token_type: str\n
"},{"location":"06/#utilizando-oauth2passwordrequestform","title":"Utilizando OAuth2PasswordRequestForm","text":"

A classe OAuth2PasswordRequestForm \u00e9 uma classe especial do FastAPI que gera automaticamente um formul\u00e1rio para solicitar o username (email neste caso) e a senha. Este formul\u00e1rio ser\u00e1 apresentado automaticamente no Swagger UI e Redoc, facilitando a realiza\u00e7\u00e3o de testes de autentica\u00e7\u00e3o.

Para usar os formul\u00e1rios no FastAPI, precisamos instalar o python-multipart:

$ Execu\u00e7\u00e3o no terminal!
poetry add python-multipart\n
"},{"location":"06/#criando-um-endpoint-de-geracao-do-token_1","title":"Criando um endpoint de gera\u00e7\u00e3o do token","text":"

Agora criaremos o endpoint que ir\u00e1 autenticar o usu\u00e1rio e fornecer um token de acesso JWT. Este endpoint ir\u00e1 receber as informa\u00e7\u00f5es de login do usu\u00e1rio, verificar se as credenciais s\u00e3o v\u00e1lidas e, em caso afirmativo, retornar um token de acesso JWT.

fast_zero/app.py
from fastapi.security import OAuth2PasswordRequestForm\nfrom fast_zero.schemas import Message, Token, UserList, UserPublic, UserSchema\nfrom fast_zero.security import (\n    create_access_token,\n    get_password_hash,\n    verify_password,\n)\n\n# ...\n\n@app.post('/token', response_model=Token)\ndef login_for_access_token(\n    form_data: OAuth2PasswordRequestForm = Depends(),\n    session: Session = Depends(get_session),\n):\n    user = session.scalar(select(User).where(User.email == form_data.username))\n\n    if not user:\n        raise HTTPException(\n            status_code=HTTPStatus.BAD_REQUEST,\n            detail='Incorrect email or password'\n        )\n\n    if not verify_password(form_data.password, user.password):\n        raise HTTPException(\n            status_code=HTTPStatus.BAD_REQUEST,\n            detail='Incorrect email or password'\n        )\n\n    access_token = create_access_token(data={'sub': user.email})\n\n    return {'access_token': access_token, 'token_type': 'bearer'}\n

Esse endpoint recebe os dados do formul\u00e1rio atrav\u00e9s do form_data (que s\u00e3o injetados automaticamente gra\u00e7as ao Depends()) e tenta recuperar um usu\u00e1rio com o email fornecido. Se o usu\u00e1rio n\u00e3o for encontrado ou a senha n\u00e3o corresponder ao hash armazenado no banco de dados, uma exce\u00e7\u00e3o \u00e9 lan\u00e7ada. Caso contr\u00e1rio, um token de acesso \u00e9 criado usando o create_access_token() que criamos anteriormente e retornado como uma resposta.

"},{"location":"06/#testando-token","title":"Testando /token","text":"

Agora escreveremos um teste para verificar se o nosso novo endpoint est\u00e1 funcionando corretamente.

tests/test_app.py
def test_get_token(client, user):\n    response = client.post(\n        '/token',\n        data={'username': user.email, 'password': user.password},\n    )\n    token = response.json()\n\n    assert response.status_code == HTTPStatus.OK\n    assert 'access_token' in token\n    assert 'token_type' in token\n

Nesse teste, n\u00f3s enviamos uma requisi\u00e7\u00e3o POST para o endpoint \"/token\" com um username e uma senha v\u00e1lidos. Ent\u00e3o, n\u00f3s verificamos que a resposta cont\u00e9m um \"access_token\" e um \"token_type\", que s\u00e3o os campos que esperamos de um JWT v\u00e1lido.

No entanto, h\u00e1 um problema. Agora que a senha est\u00e1 sendo criptografada, nosso teste falhar\u00e1:

$ Execu\u00e7\u00e3o no terminal!
task test\n\n# ...\ntests/test_app.py::test_get_token FAILED\n

Para corrigir isso, precisamos garantir que a senha esteja sendo criptografada na fixture antes de ser salva:

tests/conftest.py
from fast_zero.security import get_password_hash\n\n# ...\n\n@pytest.fixture\ndef user(session):\n    user = User(\n        username='Teste',\n        email='teste@test.com',\n        password=get_password_hash('testtest'),\n    )\n    session.add(user)\n    session.commit()\n    session.refresh(user)\n\n    return user\n

Rodaremos o teste novamente. No entanto, ainda teremos um problema. Agora s\u00f3 temos a vers\u00e3o criptografada da senha, que n\u00e3o \u00e9 \u00fatil para fazer o login, j\u00e1 que o login exige a senha em texto puro:

$ Execu\u00e7\u00e3o no terminal!
task test\n\n# ...\ntests/test_app.py::test_get_token FAILED\n

Para resolver isso, faremos uma modifica\u00e7\u00e3o no objeto user (um monkey patch) para adicionar a senha em texto puro:

tests/conftest.py
@pytest.fixture\ndef user(session):\n    password = 'testtest'\n    user = User(\n        username='Teste',\n        email='teste@test.com',\n        password=get_password_hash(password),\n    )\n    session.add(user)\n    session.commit()\n    session.refresh(user)\n\n    user.clean_password = 'testtest'\n\n    return user\n

Monkey patching \u00e9 uma t\u00e9cnica em que modificamos ou estendemos o c\u00f3digo em tempo de execu\u00e7\u00e3o. Neste caso, estamos adicionando um novo atributo clean_password ao objeto user para armazenar a senha em texto puro.

Agora, podemos alterar o teste para usar clean_password:

tests/test_app.py
def test_get_token(client, user):\n    response = client.post(\n        '/token',\n        data={'username': user.email, 'password': user.clean_password},\n    )\n    token = response.json()\n\n    assert response.status_code == HTTPStatus.OK\n    assert 'access_token' in token\n    assert 'token_type' in token\n

E agora todos os testes devem passar normalmente:

$ Execu\u00e7\u00e3o no terminal!
task test\n\n# ...\n\ntests/test_app.py::test_root_deve_retornar_ok_e_ola_mundo PASSED\ntests/test_app.py::test_create_user PASSED\ntests/test_app.py::test_read_users PASSED\ntests/test_app.py::test_read_users_with_users PASSED\ntests/test_app.py::test_update_user PASSED\ntests/test_app.py::test_delete_user PASSED\ntests/test_app.py::test_get_token PASSED\ntests/test_db.py::test_create_user PASSED\n

Isso conclui a parte de autentica\u00e7\u00e3o de nossa API. No pr\u00f3ximo passo, implementaremos a autoriza\u00e7\u00e3o nos endpoints.

"},{"location":"06/#protegendo-os-endpoints","title":"Protegendo os Endpoints","text":"

Agora que temos uma forma de autenticar nossos usu\u00e1rios e emitir tokens JWT, \u00e9 hora de usar essa infraestrutura para proteger nossos endpoints. Neste passo, adicionaremos autentica\u00e7\u00e3o aos endpoints PUT e DELETE.

Para garantir que as informa\u00e7\u00f5es do usu\u00e1rio sejam extra\u00eddas corretamente do token JWT, precisamos de um schema especial, o TokenData. Esse schema ser\u00e1 utilizado para tipificar os dados extra\u00eddos do token JWT e garantir que temos um campo username que ser\u00e1 usado para identificar o usu\u00e1rio.

fast_zero/schemas.py
class TokenData(BaseModel):\n    username: str | None = None\n

Nesse ponto, criaremos uma a fun\u00e7\u00e3o get_current_user que ser\u00e1 respons\u00e1vel por extrair o token JWT do header Authorization da requisi\u00e7\u00e3o, decodificar esse token, extrair as informa\u00e7\u00f5es do usu\u00e1rio e obter finalmente o usu\u00e1rio do banco de dados. Se qualquer um desses passos falhar, uma exce\u00e7\u00e3o ser\u00e1 lan\u00e7ada e a requisi\u00e7\u00e3o ser\u00e1 negada. A adicionaremos ao security.py:

fast_zero/security.py
from datetime import datetime, timedelta\nfrom http import HTTPStatus\n\nfrom fastapi import Depends, HTTPException\nfrom fastapi.security import OAuth2PasswordBearer\nfrom jwt import DecodeError, decode, encode\nfrom pwdlib import PasswordHash\nfrom sqlalchemy import select\nfrom sqlalchemy.orm import Session\n\nfrom fast_zero.database import get_session\nfrom fast_zero.models import User\nfrom fast_zero.schemas import TokenData\n\n# ...\n\noauth2_scheme = OAuth2PasswordBearer(tokenUrl='token')\n\ndef get_current_user(\n    session: Session = Depends(get_session),\n    token: str = Depends(oauth2_scheme), #(1)!\n):\n    credentials_exception = HTTPException(  #(2)!\n        status_code=HTTPStatus.UNAUTHORIZED,\n        detail='Could not validate credentials',\n        headers={'WWW-Authenticate': 'Bearer'},\n    )\n\n    try:\n        payload = decode(token, SECRET_KEY, algorithms=[ALGORITHM])\n        username: str = payload.get('sub')\n        if not username:\n            raise credentials_exception  #(3)!\n        token_data = TokenData(username=username)\n    except DecodeError:\n        raise credentials_exception  #(4)!\n\n    user = session.scalar(\n        select(User).where(User.email == token_data.username)\n    )\n\n    if not user:\n        raise credentials_exception  #(5)!\n\n    return user\n
  1. A inje\u00e7\u00e3o de oauth2_scheme garante que um token foi enviado, caso n\u00e3o tenha sido enviado ele redirecionar\u00e1 a tokenUrl do objeto OAuth2PasswordBearer.
  2. Como essa opera\u00e7\u00e3o pode apresentar erros em diversos momentos, foi atribu\u00eddo um \u00fanico erro a vari\u00e1vel credentials_exception.
  3. Nessa primeira valida\u00e7\u00e3o \u00e9 checado se ap\u00f3s o decode do token a chave username est\u00e1 presente. Caso n\u00e3o esteja, o erro ser\u00e1 levantado.
  4. Nessa valida\u00e7\u00e3o \u00e9 testada se o token \u00e9 um token JWT v\u00e1lido. Caso n\u00e3o seja, o erro tamb\u00e9m ser\u00e1 levantado.
  5. Nossa \u00faltima valida\u00e7\u00e3o checa, ap\u00f3s de garantir que existe username, se ele est\u00e1 presente em nossa base de dados. Caso n\u00e3o, o erro ser\u00e1 levantado.

Aqui, a fun\u00e7\u00e3o get_current_user \u00e9 definida como ass\u00edncrona, indicando que ela pode realizar opera\u00e7\u00f5es de IO (como consultar um banco de dados) de forma n\u00e3o bloqueante. Esta fun\u00e7\u00e3o aceita dois argumentos: session e token. O session \u00e9 obtido atrav\u00e9s da fun\u00e7\u00e3o get_session (n\u00e3o mostrada aqui), que deve retornar uma sess\u00e3o de banco de dados ativa. O token \u00e9 obtido do header de autoriza\u00e7\u00e3o da requisi\u00e7\u00e3o, que \u00e9 esperado ser do tipo Bearer (indicado pelo esquema OAuth2).

A vari\u00e1vel credentials_exception \u00e9 definida como uma exce\u00e7\u00e3o HTTP que ser\u00e1 lan\u00e7ada sempre que houver um problema com as credenciais fornecidas pelo usu\u00e1rio. O status 401 indica que a autentica\u00e7\u00e3o falhou e a mensagem \"Could not validate credentials\" \u00e9 retornada ao cliente. Al\u00e9m disso, um cabe\u00e7alho 'WWW-Authenticate' \u00e9 inclu\u00eddo na resposta, indicando que o cliente deve fornecer autentica\u00e7\u00e3o.

No bloco try, tentamos decodificar o token JWT usando a chave secreta e o algoritmo especificado. O token decodificado \u00e9 armazenado na vari\u00e1vel payload. Extra\u00edmos o campo 'sub' (normalmente usado para armazenar o identificador do usu\u00e1rio no token JWT) e verificamos se ele existe. Se n\u00e3o, lan\u00e7amos a exce\u00e7\u00e3o credentials_exception. Em seguida, criamos um objeto TokenData com o username.

Por fim, realizamos uma consulta ao banco de dados para encontrar o usu\u00e1rio com o e-mail correspondente ao username contido no token. session.scalar \u00e9 usado para retornar a primeira coluna do primeiro resultado da consulta. Se nenhum usu\u00e1rio for encontrado, lan\u00e7amos a exce\u00e7\u00e3o credentials_exception. Se um usu\u00e1rio for encontrado, retornamos esse usu\u00e1rio.

"},{"location":"06/#aplicacao-da-protecao-ao-endpoint","title":"Aplica\u00e7\u00e3o da prote\u00e7\u00e3o ao endpoint","text":"

Primeiro, aplicaremos a autentica\u00e7\u00e3o no endpoint PUT. Se o user_id da rota n\u00e3o corresponder ao id do usu\u00e1rio autenticado, retornaremos um erro 400. Se tudo estiver correto, o usu\u00e1rio ser\u00e1 atualizado normalmente.

fast_zero/app.py
from fast_zero.security import (\n    create_access_token,\n    get_current_user,\n    get_password_hash,\n    verify_password,\n)\n\n# ...\n\n@app.put('/users/{user_id}', response_model=UserPublic)\ndef update_user(\n    user_id: int,\n    user: UserSchema,\n    session: Session = Depends(get_session),\n    current_user: User = Depends(get_current_user),\n):\n    if current_user.id != user_id:\n        raise HTTPException(\n            status_code=HTTPStatus.FORBIDDEN, detail='Not enough permissions'\n        )\n\n    current_user.username = user.username\n    current_user.password = get_password_hash(user.password)\n    current_user.email = user.email\n    session.commit()\n    session.refresh(current_user)\n\n    return current_user\n

Com isso, podemos remover a query feita no endpoint para encontrar o User, pois ela j\u00e1 est\u00e1 sendo feita no get_current_user, simplificando ainda mais nosso endpoint.

Agora, aplicaremos a autentica\u00e7\u00e3o no endpoint DELETE. Semelhante ao PUT, se o user_id da rota n\u00e3o corresponder ao id do usu\u00e1rio autenticado, retornaremos um erro 400. Se tudo estiver correto, o usu\u00e1rio ser\u00e1 deletado.

fast_zero/app.py
@app.delete('/users/{user_id}', response_model=Message)\ndef delete_user(\n    user_id: int,\n    session: Session = Depends(get_session),\n    current_user: User = Depends(get_current_user),\n):\n    if current_user.id != user_id:\n        raise HTTPException(\n            status_code=HTTPStatus.FORBIDDEN, detail='Not enough permissions'\n        )\n\n    session.delete(current_user)\n    session.commit()\n\n    return {'message': 'User deleted'}\n

Com essa nova depend\u00eancia, o FastAPI automaticamente garantir\u00e1 que um token de autentica\u00e7\u00e3o v\u00e1lido seja fornecido antes de permitir o acesso a esses endpoints. Se o token n\u00e3o for v\u00e1lido, ou se o usu\u00e1rio tentar modificar ou deletar um usu\u00e1rio diferente, um erro ser\u00e1 retornado.

"},{"location":"06/#atualizando-os-testes","title":"Atualizando os Testes","text":"

Os testes precisam ser atualizados para refletir essas mudan\u00e7as. Primeiro, precisamos criar uma nova fixture que gere um token para um usu\u00e1rio de teste.

tests/conftest.py
@pytest.fixture\ndef token(client, user):\n    response = client.post(\n        '/token',\n        data={'username': user.email, 'password': user.clean_password},\n    )\n    return response.json()['access_token']\n

Agora, podemos atualizar os testes para o endpoint PUT e DELETE para incluir a autentica\u00e7\u00e3o.

tests/test_app.py
def test_update_user(client, user, token):\n    response = client.put(\n        f'/users/{user.id}',\n        headers={'Authorization': f'Bearer {token}'},\n        json={\n            'username': 'bob',\n            'email': 'bob@example.com',\n            'password': 'mynewpassword',\n        },\n    )\n    assert response.status_code == HTTPStatus.OK\n    assert response.json() == {\n        'username': 'bob',\n        'email': 'bob@example.com',\n        'id': user.id,\n    }\n\n\ndef test_delete_user(client, user, token):\n    response = client.delete(\n        f'/users/{user.id}',\n        headers={'Authorization': f'Bearer {token}'},\n    )\n    assert response.status_code == HTTPStatus.OK\n    assert response.json() == {'message': 'User deleted'}\n

Finalmente, podemos rodar todos os testes para garantir que tudo esteja funcionando corretamente.

$ Execu\u00e7\u00e3o no terminal!
task test\n\n# ...\n\ntests/test_app.py::test_root_deve_retornar_ok_e_ola_mundo PASSED\ntests/test_app.py::test_create_user PASSED\ntests/test_app.py::test_read_users PASSED\ntests/test_app.py::test_read_users_with_users PASSED\ntests/test_app.py::test_update_user PASSED\ntests/test_app.py::test_delete_user PASSED\ntests/test_app.py::test_get_token PASSED\ntests/test_db.py::test_create_user PASSED\n

Com essas altera\u00e7\u00f5es, nossos endpoints agora est\u00e3o seguramente protegidos pela autentica\u00e7\u00e3o. Apenas os usu\u00e1rios autenticados podem alterar ou deletar seus pr\u00f3prios dados. Isso traz uma camada adicional de seguran\u00e7a e integridade para o nosso aplicativo.

"},{"location":"06/#checagem-de-tokens-invalidos","title":"Checagem de tokens inv\u00e1lidos","text":"

Uma coisa que pode ter passado em branco durante a fase de testes \u00e9 a valida\u00e7\u00e3o do token JWT. Ele pode estar em um formato inv\u00e1lido, \u00e0s vezes por erro do cliente, outras por algum problema de seguran\u00e7a. \u00c9 importante validarmos como a aplica\u00e7\u00e3o vai reagir a um token inv\u00e1lido.

Durante a constru\u00e7\u00e3o da fun\u00e7\u00e3o get_current_user, criamos um fluxo para esse erro:

Parte do c\u00f3digo da fun\u00e7\u00e3o `get_current_user` em security.py
credentials_exception = HTTPException(\n    status_code=HTTPStatus.UNAUTHORIZED,\n    detail='Could not validate credentials',\n    headers={'WWW-Authenticate': 'Bearer'},\n)\n\ntry:\n    payload = decode(token, SECRET_KEY, algorithms=[ALGORITHM])\n    # C\u00f3digo para caso o token seja decodificado\nexcept DecodeError:\n    raise credentials_exception\n

Caso n\u00e3o seja poss\u00edvel decodificar o token, por algum motivo, \u00e9 importante validarmos se o retorno ser\u00e1 um 401 UNAUTHORIZED. Para isso, seria importante criar um teste que verifica essa camada.

Poder\u00edamos fazer de duas formas, chamando diretamente a fun\u00e7\u00e3o get_current_user passando um token inv\u00e1lido, ou fazer a valida\u00e7\u00e3o diretamente via a requisi\u00e7\u00e3o de algu\u00e9m que dependa de um token v\u00e1lido. Optarei pela segunda op\u00e7\u00e3o, por ser mais pr\u00f3xima de uma requisi\u00e7\u00e3o real.

tests/test_security.py
from http import HTTPStatus\n\n# ...\n\ndef test_jwt_invalid_token(client):\n    response = client.delete(\n        '/users/1', headers={'Authorization': 'Bearer token-invalido'}\n    )\n\n    assert response.status_code == HTTPStatus.UNAUTHORIZED\n    assert response.json() == {'detail': 'Could not validate credentials'}\n

Usamos um assert para validar se o c\u00f3digo \u00e9 mesmo 401 e outro para validar nossa mensagem de erro. Assim, temos uma garantia que, caso algu\u00e9m envie um token inv\u00e1lido, a resposta seguir\u00e1 como esperado.

Antes de finalizar, ideal seria executarmos os testes:

$ Execu\u00e7\u00e3o no terminal!
task test\n\n# ...\n\ntests/test_app.py::test_get_token PASSED\ntests/test_db.py::test_create_user PASSED\ntests/test_security.py::test_jwt_invalid_token PASSED\n
"},{"location":"06/#exercicios","title":"Exerc\u00edcios","text":"
  1. Fa\u00e7a um teste para cobrir o cen\u00e1rio que levanta exception credentials_exception na autentica\u00e7\u00e3o caso o User n\u00e3o seja encontrado. Ao olhar a cobertura de security.py voc\u00ea vai notar que esse contexto n\u00e3o est\u00e1 coberto.

  2. Reveja os testes criados at\u00e9 a aula 5 e veja se eles ainda fazem sentido (testes envolvendo 400)

"},{"location":"06/#commit","title":"Commit","text":"

Depois de finalizar a prote\u00e7\u00e3o dos endpoints e atualizar os testes, \u00e9 hora de fazer commit das altera\u00e7\u00f5es. N\u00e3o se esque\u00e7a de revisar as altera\u00e7\u00f5es antes de fazer o commit.

$ Execu\u00e7\u00e3o no terminal!
git status\ngit add .\ngit commit -m \"Protege os endpoints PUT e DELETE com autentica\u00e7\u00e3o\"\n
"},{"location":"06/#conclusao","title":"Conclus\u00e3o","text":"

Nesta aula, demos um passo importante para aumentar a seguran\u00e7a da nossa API. Implementamos a autentica\u00e7\u00e3o e a autoriza\u00e7\u00e3o para os endpoints PUT e DELETE, garantindo que apenas usu\u00e1rios autenticados possam alterar ou excluir seus pr\u00f3prios dados. Tamb\u00e9m atualizamos os testes para incluir a autentica\u00e7\u00e3o. Na pr\u00f3xima aula, continuaremos a expandir a funcionalidade da nossa API. At\u00e9 l\u00e1!

Agora que a aula acabou, \u00e9 um bom momento para voc\u00ea relembrar alguns conceitos e fixar melhor o conte\u00fado respondendo ao question\u00e1rio referente a ela.

Quiz

"},{"location":"07/","title":"Refatorando a Estrutura do Projeto","text":""},{"location":"07/#refatorando-a-estrutura-do-projeto","title":"Refatorando a Estrutura do Projeto","text":"

Objetivos da Aula:

Caso prefira ver a aula em v\u00eddeo

Esse aula ainda n\u00e3o est\u00e1 dispon\u00edvel em formato de v\u00eddeo, somente em texto ou live!

Aula Slides C\u00f3digo Quiz

Ao longo da evolu\u00e7\u00e3o de um projeto, \u00e9 natural que sua estrutura inicial necessite de ajustes para manter a legibilidade, a facilidade de manuten\u00e7\u00e3o e a organiza\u00e7\u00e3o do c\u00f3digo. Nesta aula, faremos exatamente isso em nosso projeto FastAPI: refatoraremos partes dele para melhorar sua estrutura e, em seguida, ampliar a cobertura de nossos testes para garantir que todos os cen\u00e1rios poss\u00edveis sejam tratados corretamente. Vamos come\u00e7ar!

"},{"location":"07/#criando-routers","title":"Criando Routers","text":"

O FastAPI oferece uma ferramenta poderosa conhecida como routers, que facilita a organiza\u00e7\u00e3o e agrupamento de diferentes rotas em uma aplica\u00e7\u00e3o. Pense em um router como um \"subaplicativo\" do FastAPI que pode ser integrado em uma aplica\u00e7\u00e3o principal. Isso n\u00e3o s\u00f3 mant\u00e9m o c\u00f3digo organizado e leg\u00edvel, mas tamb\u00e9m se mostra especialmente \u00fatil \u00e0 medida que a aplica\u00e7\u00e3o se expande e novas rotas s\u00e3o adicionadas.

Esse tipo de organiza\u00e7\u00e3o nos oferece diversos benef\u00edcios:

  1. Organiza\u00e7\u00e3o e Legibilidade: Routers ajudam a manter o c\u00f3digo organizado e leg\u00edvel, o que \u00e9 crucial \u00e0 medida que a aplica\u00e7\u00e3o se expande.
  2. Separa\u00e7\u00e3o de Preocupa\u00e7\u00f5es: Alinhado ao princ\u00edpio de SoC, os routers facilitam o entendimento e teste do c\u00f3digo.
  3. Escalabilidade: A estrutura\u00e7\u00e3o com routers permite adicionar novas rotas e funcionalidades de maneira eficiente conforme o projeto cresce.
"},{"location":"07/#estruturacao-inicial","title":"Estrutura\u00e7\u00e3o Inicial","text":"

Criaremos inicialmente uma nova estrutura de diret\u00f3rios chamada routers dentro do seu projeto fast_zero. Aqui, teremos subaplicativos dedicados a fun\u00e7\u00f5es espec\u00edficas, como gerenciamento de usu\u00e1rios e autentica\u00e7\u00e3o.

\u251c\u2500\u2500 fast_zero\n\u2502  \u251c\u2500\u2500 app.py\n\u2502  \u251c\u2500\u2500 database.py\n\u2502  \u251c\u2500\u2500 models.py\n\u2502  \u251c\u2500\u2500 routers\n\u2502  \u2502  \u251c\u2500\u2500 auth.py\n\u2502  \u2502  \u2514\u2500\u2500 users.py\n

Esta organiza\u00e7\u00e3o facilita a expans\u00e3o do seu projeto e a manuten\u00e7\u00e3o de uma estrutura clara.

"},{"location":"07/#implementando-um-router-para-usuarios","title":"Implementando um Router para Usu\u00e1rios","text":"

No arquivo fast_zero/routers/users.py, implementaremos o recurso APIRouter do FastAPI, a ferramenta chave para criar nosso subaplicativo. O par\u00e2metro prefix que passamos ajuda a agrupar todos os endpoints relacionados aos usu\u00e1rios sob um mesmo teto.

fast_zero/routers/users.py
from fastapi import APIRouter\n\nrouter = APIRouter(prefix='/users', tags=['users'])\n

Com essa simples configura\u00e7\u00e3o, estamos prontos para definir rotas espec\u00edficas para usu\u00e1rios neste router, em vez de sobrecarregar o aplicativo principal. Utilizamos @router ao inv\u00e9s de @app para definir estas rotas. O uso da tag 'users' contribui para a organiza\u00e7\u00e3o e documenta\u00e7\u00e3o autom\u00e1tica no swagger.

Desta forma podemos migrar todos os nossos imports e nossas fun\u00e7\u00f5es de endpoints para o arquivo fast_zero/routers/users.py e os removendo de fast_zero/app.py. Fazendo com que todos esses endpoints estejam no mesmo contexto e isolados da aplica\u00e7\u00e3o principal:

fast_zero/routers/users.py
from http import HTTPStatus\n\nfrom fastapi import APIRouter, Depends, HTTPException\nfrom sqlalchemy import select\nfrom sqlalchemy.orm import Session\n\nfrom fast_zero.database import get_session\nfrom fast_zero.models import User\nfrom fast_zero.schemas import Message, UserList, UserPublic, UserSchema\nfrom fast_zero.security import (\n    get_current_user,\n    get_password_hash,\n)\n\nrouter = APIRouter(prefix='/users', tags=['users'])\n\n@router.post('/', status_code=HTTPStatus.CREATED, response_model=UserPublic)\n# ...\n@router.get('/', response_model=UserList)\n# ...\n@router.put('/{user_id}', response_model=UserPublic)\n# ...\n@router.delete('/{user_id}', response_model=Message)\n# ...\n

Com o prefixo definido no router, os paths dos endpoints se tornam mais simples e diretos. Ao inv\u00e9s de '/users/{user_id}', por exemplo, usamos apenas '/{user_id}'.

Exemplo do arquivo fast_zero/routers/users.py completo fast_zero/routers/users.py
from http import HTTPStatus\n\nfrom fastapi import APIRouter, Depends, HTTPException\nfrom sqlalchemy import select\nfrom sqlalchemy.orm import Session\n\nfrom fast_zero.database import get_session\nfrom fast_zero.models import User\nfrom fast_zero.schemas import Message, UserList, UserPublic, UserSchema\nfrom fast_zero.security import (\n    get_current_user,\n    get_password_hash,\n)\n\nrouter = APIRouter(prefix='/users', tags=['users'])\n\n\n@router.post('/', status_code=HTTPStatus.CREATED, response_model=UserPublic)\ndef create_user(user: UserSchema, session: Session = Depends(get_session)):\n    db_user = session.scalar(select(User).where(User.email == user.email))\n    if db_user:\n        raise HTTPException(\n            status_code=HTTPStatus.BAD_REQUEST,\n            detail='Email already registered'\n        )\n\n    hashed_password = get_password_hash(user.password)\n\n    db_user = User(\n        email=user.email,\n        username=user.username,\n        password=hashed_password,\n    )\n    session.add(db_user)\n    session.commit()\n    session.refresh(db_user)\n    return db_user\n\n\n@router.get('/', response_model=UserList)\ndef read_users(\n    skip: int = 0, limit: int = 100, session: Session = Depends(get_session)\n):\n    users = session.scalars(select(User).offset(skip).limit(limit)).all()\n    return {'users': users}\n\n\n@router.put('/{user_id}', response_model=UserPublic)\ndef update_user(\n    user_id: int,\n    user: UserSchema,\n    session: Session = Depends(get_session),\n    current_user: User = Depends(get_current_user),\n):\n    if current_user.id != user_id:\n        raise HTTPException(\n            status_code=HTTPStatus.FORBIDDEN, detail='Not enough permissions'\n        )\n\n    current_user.username = user.username\n    current_user.password = get_password_hash(user.password)\n    current_user.email = user.email\n    session.commit()\n    session.refresh(current_user)\n\n    return current_user\n\n\n@router.delete('/{user_id}', response_model=Message)\ndef delete_user(\n    user_id: int,\n    session: Session = Depends(get_session),\n    current_user: User = Depends(get_current_user),\n):\n    if current_user.id != user_id:\n        raise HTTPException(\n            status_code=HTTPStatus.FORBIDDEN, detail='Not enough permissions'\n        )\n\n    session.delete(current_user)\n    session.commit()\n\n    return {'message': 'User deleted'}\n

Por termos criados as tags, isso reflete na organiza\u00e7\u00e3o do swagger

"},{"location":"07/#criando-um-router-para-auth","title":"Criando um router para Auth","text":"

No momento, temos rotas para / e /token ainda no arquivo fast_zero/app.py. Daremos um passo adiante e criar um router separado para lidar com a autentica\u00e7\u00e3o. Desta forma, conseguiremos manter nosso arquivo principal (app.py) mais limpo e focado em sua responsabilidade principal que \u00e9 iniciar nossa aplica\u00e7\u00e3o.

O router para autentica\u00e7\u00e3o ser\u00e1 criado no arquivo fast_zero/routers/auth.py. Veja como fazer:

fast_zero/routers/auth.py
from http import HTTPStatus\n\nfrom fastapi import APIRouter, Depends, HTTPException\nfrom fastapi.security import OAuth2PasswordRequestForm\nfrom sqlalchemy import select\nfrom sqlalchemy.orm import Session\n\nfrom fast_zero.database import get_session\nfrom fast_zero.models import User\nfrom fast_zero.schemas import Token\nfrom fast_zero.security import create_access_token, verify_password\n\nrouter = APIRouter(prefix='/auth', tags=['auth'])\n\n\n@router.post('/token', response_model=Token)\ndef login_for_access_token(\n    form_data: OAuth2PasswordRequestForm = Depends(),\n    session: Session = Depends(get_session),\n):\n    user = session.scalar(select(User).where(User.email == form_data.username))\n\n    if not user:\n        raise HTTPException(\n            status_code=HTTPStatus.BAD_REQUEST,\n            detail='Incorrect email or password'\n        )\n\n    if not verify_password(form_data.password, user.password):\n        raise HTTPException(\n            status_code=HTTPStatus.BAD_REQUEST,\n            detail='Incorrect email or password'\n        )\n\n    access_token = create_access_token(data={'sub': user.email})\n\n    return {'access_token': access_token, 'token_type': 'bearer'}\n

Neste bloco de c\u00f3digo, n\u00f3s criamos um novo router que lidar\u00e1 exclusivamente com a rota de obten\u00e7\u00e3o de token (/token). O endpoint login_for_access_token \u00e9 definido exatamente da mesma maneira que antes, mas agora como parte deste router de autentica\u00e7\u00e3o.

"},{"location":"07/#alteracao-da-validacao-de-token","title":"Altera\u00e7\u00e3o da valida\u00e7\u00e3o de token","text":"

\u00c9 crucial abordar um aspecto relacionado \u00e0 modifica\u00e7\u00e3o do router: o uso do par\u00e2metro prefix. Ao introduzir o prefixo, o endere\u00e7o do endpoint /token, respons\u00e1vel pela valida\u00e7\u00e3o do bearer token JWT, \u00e9 alterado para /auth/token. Esse caminho est\u00e1 explicitamente definido no OAuth2PasswordBearer dentro de security.py, resultando em uma refer\u00eancia ao caminho antigo /token, anterior \u00e0 cria\u00e7\u00e3o do router.

Esse problema fica evidente ao clicar no bot\u00e3o Authorize no Swagger:

Percebe-se que o caminho para a autoriza\u00e7\u00e3o est\u00e1 incorreto. Como consequ\u00eancia, ao tentar autenticar atrav\u00e9s do Swagger, nos deparamos com um erro na interface:

No entanto, o erro n\u00e3o \u00e9 suficientemente descritivo para identificarmos a origem do problema, retornando apenas uma mensagem gen\u00e9rica de Auth Error. Para compreender melhor o que ocorreu, \u00e9 necess\u00e1rio verificar o log produzido pelo uvicorn no terminal:

Erro mostrado no terminal
task serve\n# ...\nINFO:     127.0.0.1:40132 - \"POST /token HTTP/1.1\" 404 Not Found\n

A solu\u00e7\u00e3o para este problema \u00e9 relativamente simples. Precisamos ajustar o par\u00e2metro tokenUrl na OAuth2PasswordBearer para refletir as mudan\u00e7as feitas no router, direcionando para /auth/token. Faremos isso no arquivo security.py:

security.py
oauth2_scheme = OAuth2PasswordBearer(tokenUrl='auth/token')\n

Ap\u00f3s essa altera\u00e7\u00e3o, ao utilizar o Swagger, a autoriza\u00e7\u00e3o ser\u00e1 direcionada corretamente para o endpoint apropriado.

"},{"location":"07/#alteracao-no-teste-do-token","title":"Altera\u00e7\u00e3o no teste do token","text":"

Essa altera\u00e7\u00e3o far\u00e1 com que o teste referente a cria\u00e7\u00e3o do token tamb\u00e9m falhe. Pois ele procurar\u00e1 pelo endpoint /token. Devemos fazer a altera\u00e7\u00e3o para o novo caminho, que com a cria\u00e7\u00e3o de router, adiciona o prefixo /auth. Ficando assim:

tests/test_app.py
def test_get_token(client, user):\n    response = client.post(\n        '/auth/token',#(1)!\n        data={'username': user.email, 'password': user.clean_password},\n    )\n    token = response.json()\n\n    assert response.status_code == HTTPStatus.OK\n    assert 'access_token' in token\n    assert 'token_type' in token\n
  1. A \u00fanica altera\u00e7\u00e3o \u00e9 mesmo o endpoint!

Desta forma o teste espec\u00edfico do token poder\u00e1 passar corretamente. Mas, existem testes que dependem do token criado pela fixture.

"},{"location":"07/#alteracao-na-fixture-de-token","title":"Altera\u00e7\u00e3o na fixture de token","text":"

A altera\u00e7\u00e3o da fixture de token \u00e9 igual que fizemos em /tests/test_auth.py, precisamos somente corrigir o novo endere\u00e7o do router no arquivo /tests/conftest.py:

/tests/conftest.py
@pytest.fixture\ndef token(client, user):\n    response = client.post(\n        '/auth/token',\n        data={'username': user.email, 'password': user.clean_password},\n    )\n    return response.json()['access_token']\n

Fazendo assim com que os testes que dependem dessa fixture passem a funcionar.

Contudo, essas modifica\u00e7\u00f5es ainda n\u00e3o podem ser executadas, pois precisamos plugar os roteadores no aplicativo antes de executar.

"},{"location":"07/#plugando-as-rotas-em-app","title":"Plugando as rotas em app","text":"

O FastAPI oferece uma maneira f\u00e1cil e direta de incluir routers em nossa aplica\u00e7\u00e3o principal. Isso nos permite organizar nossos endpoints de maneira eficiente e manter nosso arquivo app.py focado apenas em suas responsabilidades principais.

Para incluir os routers em nossa aplica\u00e7\u00e3o principal, precisamos import\u00e1-los e usar a fun\u00e7\u00e3o include_router(). Aqui est\u00e1 como o nosso arquivo app.py fica depois de incluir os routers:

fast_zero/fast_zero/app.py
from http import HTTPStatus\n\nfrom fastapi import FastAPI\n\nfrom fast_zero.routers import auth, users\nfrom fast_zero.schemas import Message\n\napp = FastAPI()\n\napp.include_router(users.router)\napp.include_router(auth.router)\n\n\n@app.get('/', status_code=HTTPStatus.OK, response_model=Message)\ndef read_root():\n    return {'message': 'Ol\u00e1 Mundo!'}\n

Como voc\u00ea pode ver, nosso arquivo app.py \u00e9 muito mais simples agora. Ele agora delega as rotas para os respectivos routers, mantendo o foco em iniciar nossa aplica\u00e7\u00e3o FastAPI.

"},{"location":"07/#executando-os-testes","title":"Executando os testes","text":"

Ap\u00f3s refatorar nosso c\u00f3digo, \u00e9 crucial verificar se tudo continua funcionando como esperado. Para isso, executamos nossos testes novamente.

$ Execu\u00e7\u00e3o no terminal!
task test\n\n# ...\n\ntests/test_app.py::test_root_deve_retornar_ok_e_ola_mundo PASSED\ntests/test_app.py::test_create_user PASSED\ntests/test_app.py::test_read_users PASSED\ntests/test_app.py::test_read_users_with_users PASSED\ntests/test_app.py::test_update_user PASSED\ntests/test_app.py::test_delete_user PASSED\ntests/test_app.py::test_get_token PASSED\ntests/test_db.py::test_create_user PASSED\n

Como voc\u00ea pode ver, todos os testes passaram. Isso significa que as altera\u00e7\u00f5es que fizemos no nosso c\u00f3digo n\u00e3o afetaram o funcionamento do nosso aplicativo. O router manteve todos os endpoints nas mesmas rotas, garantindo a continuidade do comportamento esperado.

Agora, para melhor alinhar nossos testes com a nova estrutura do nosso c\u00f3digo, devemos reorganizar os arquivos de teste de acordo. Ou seja, tamb\u00e9m devemos criar arquivos de teste espec\u00edficos para cada router, em vez de manter todos os testes no arquivo tests/test_app.py. Essa estrutura facilitar\u00e1 a manuten\u00e7\u00e3o e compreens\u00e3o dos testes \u00e0 medida que nossa aplica\u00e7\u00e3o cresce.

"},{"location":"07/#reestruturando-os-arquivos-de-testes","title":"Reestruturando os arquivos de testes","text":"

Para acompanhar a nova estrutura routers, podemos desacoplar os testes do m\u00f3dulo test/test_app.py e criar arquivos de teste espec\u00edficos para cada um dos dom\u00ednios:

Vamos adaptar os testes para se encaixarem nessa nova estrutura.

"},{"location":"07/#ajustando-os-testes-para-auth","title":"Ajustando os testes para Auth","text":"

Come\u00e7aremos criando o arquivo /tests/test_auth.py. Esse arquivo ser\u00e1 respons\u00e1vel por testar todas as funcionalidades relacionadas \u00e0 autentica\u00e7\u00e3o do usu\u00e1rio.

/tests/test_auth.py
from http import HTTPStatus\n\n\ndef test_get_token(client, user):\n    response = client.post(\n        '/auth/token',\n        data={'username': user.email, 'password': user.clean_password},\n    )\n    token = response.json()\n\n    assert response.status_code == HTTPStatus.OK\n    assert 'access_token' in token\n    assert 'token_type' in token\n

\u00c9 importante notar que com a cria\u00e7\u00e3o do router usando prefix='/auth' devemos alterar o endpoint onde o request \u00e9 feito de '/token' para '/auth/token'. Fazendo com que a requisi\u00e7\u00e3o seja encaminhada para o lugar certo.

"},{"location":"07/#ajustando-os-testes-para-user","title":"Ajustando os testes para User","text":"

Em seguida, moveremos os testes relacionados ao dom\u00ednio do usu\u00e1rio para o arquivo /tests/test_users.py.

/tests/test_users.py
from http import HTTPStatus\n\nfrom fast_zero.schemas import UserPublic\n\n\ndef test_create_user(client):\n    response = client.post(\n        '/users/',\n        json={\n            'username': 'alice',\n            'email': 'alice@example.com',\n            'password': 'secret',\n        },\n    )\n    assert response.status_code == HTTPStatus.CREATED\n    assert response.json() == {\n        'username': 'alice',\n        'email': 'alice@example.com',\n        'id': 1,\n    }\n\n\ndef test_read_users(client):\n    response = client.get('/users')\n    assert response.status_code == HTTPStatus.OK\n    assert response.json() == {'users': []}\n\n\ndef test_read_users_with_users(client, user):\n    user_schema = UserPublic.model_validate(user).model_dump()\n    response = client.get('/users/')\n    assert response.json() == {'users': [user_schema]}\n\n\ndef test_update_user(client, user, token):\n    response = client.put(\n        f'/users/{user.id}',\n        headers={'Authorization': f'Bearer {token}'},\n        json={\n            'username': 'bob',\n            'email': 'bob@example.com',\n            'password': 'mynewpassword',\n        },\n    )\n    assert response.status_code == HTTPStatus.OK\n    assert response.json() == {\n        'username': 'bob',\n        'email': 'bob@example.com',\n        'id': 1,\n    }\n\n\ndef test_delete_user(client, user, token):\n    response = client.delete(\n        f'/users/{user.id}',\n        headers={'Authorization': f'Bearer {token}'},\n    )\n\n    assert response.status_code == HTTPStatus.OK\n    assert response.json() == {'message': 'User deleted'}\n

Para a constru\u00e7\u00e3o desse arquivo, nenhum teste foi modificado. Eles foram somente movidos para o dom\u00ednio espec\u00edfico do router. Importante, por\u00e9m, notar que alguns destes testes usam a fixture token para checar a autoriza\u00e7\u00e3o, como o endpoint do token foi alterado, devemos alterar a fixture de token para que esses testes continuem passando.

"},{"location":"07/#executando-os-testes_1","title":"Executando os testes","text":"

Ap\u00f3s essa reestrutura\u00e7\u00e3o, \u00e9 importante garantir que tudo continua funcionando corretamente. Executaremos os testes novamente para confirmar isso.

$ Execu\u00e7\u00e3o no terminal!
task test\n\n# ...\n\ntests/test_app.py::test_root_deve_retornar_ok_e_ola_mundo PASSED\ntests/test_auth.py::test_get_token PASSED\ntests/test_db.py::test_create_user PASSED\ntests/test_users.py::test_create_user PASSED\ntests/test_users.py::test_read_users PASSED\ntests/test_users.py::test_read_users_with_users PASSED\ntests/test_users.py::test_update_user PASSED\ntests/test_users.py::test_delete_user PASSED\n

Como podemos ver, todos os testes continuam passando com sucesso, mesmo ap\u00f3s terem sido movidos para arquivos diferentes. Isso \u00e9 uma confirma\u00e7\u00e3o de que nossa reestrutura\u00e7\u00e3o foi bem-sucedida e que nossa aplica\u00e7\u00e3o continua funcionando como esperado.

"},{"location":"07/#refinando-a-definicao-de-rotas-com-annotated","title":"Refinando a Defini\u00e7\u00e3o de Rotas com Annotated","text":"

O FastAPI suporta um recurso fascinante da biblioteca nativa typing, conhecido como Annotated. Esse recurso prova ser especialmente \u00fatil quando buscamos simplificar a utiliza\u00e7\u00e3o de depend\u00eancias.

Ao definir uma anota\u00e7\u00e3o de tipo, seguimos a seguinte formata\u00e7\u00e3o: nome_do_argumento: Tipo = Depends(o_que_dependemos). Em todos os endpoints, acrescentamos a inje\u00e7\u00e3o de depend\u00eancia da sess\u00e3o da seguinte forma:

session: Session = Depends(get_session)\n

O tipo Annotated nos permite combinar um tipo e os metadados associados a ele em uma \u00fanica defini\u00e7\u00e3o. Atrav\u00e9s da aplica\u00e7\u00e3o do FastAPI, podemos utilizar o Depends no campo dos metadados. Isso nos permite encapsular o tipo da vari\u00e1vel e o Depends em uma \u00fanica entidade, facilitando a defini\u00e7\u00e3o dos endpoints.

Veja o exemplo a seguir:

fast_zero/routers/users.py
from typing import Annotated\n\nSession = Annotated[Session, Depends(get_session)]\nCurrentUser = Annotated[User, Depends(get_current_user)]\n

Desse modo, conseguimos refinar a defini\u00e7\u00e3o dos endpoints para que se tornem mais concisos, sem alterar seu funcionamento:

fast_zero/routers/users.py
@router.post('/', status_code=HTTPStatus.CREATED, response_model=UserPublic)\ndef create_user(user: UserSchema, session: Session):\n# ...\n\n@router.get('/', response_model=UserList)\ndef read_users(session: Session, skip: int = 0, limit: int = 100):\n# ...\n\n@router.put('/{user_id}', response_model=UserPublic)\ndef update_user(\n    user_id: int,\n    user: UserSchema,\n    session: Session,\n    current_user: CurrentUser\n):\n# ...\n\n@router.delete('/{user_id}', response_model=Message)\ndef delete_user(user_id: int, session: Session, current_user: CurrentUser):\n# ...\n

Da mesma forma, podemos otimizar o roteador de autentica\u00e7\u00e3o:

fast_zero/routers/auth.py
from typing import Annotated\n\n# ...\n\nOAuth2Form = Annotated[OAuth2PasswordRequestForm, Depends()]\nSession = Annotated[Session, Depends(get_session)]\n\n@router.post('/token', response_model=Token)\ndef login_for_access_token(form_data: OAuth2Form, session: Session):\n#...\n

Atrav\u00e9s do uso de tipos Annotated, conseguimos reutilizar os mesmos consistentemente, reduzindo a repeti\u00e7\u00e3o de c\u00f3digo e aumentando a efici\u00eancia do nosso trabalho.

"},{"location":"07/#movendo-as-constantes-para-variaveis-de-ambiente","title":"Movendo as constantes para vari\u00e1veis de ambiente","text":"

Conforme mencionamos na aula sobre os 12 fatores, \u00e9 uma boa pr\u00e1tica manter as constantes que podem mudar dependendo do ambiente em vari\u00e1veis de ambiente. Isso torna o seu projeto mais seguro e modular, pois voc\u00ea pode alterar essas constantes sem ter que modificar o c\u00f3digo-fonte.

Por exemplo, temos estas constantes em nosso m\u00f3dulo security.py:

SECRET_KEY = 'your-secret-key'  # Isso \u00e9 provis\u00f3rio, vamos ajustar!\nALGORITHM = 'HS256'\nACCESS_TOKEN_EXPIRE_MINUTES = 30\n

Estes valores n\u00e3o devem estar diretamente no c\u00f3digo-fonte, ent\u00e3o vamos mov\u00ea-los para nossas vari\u00e1veis de ambiente e represent\u00e1-los na nossa classe Settings.

"},{"location":"07/#adicionando-as-constantes-a-settings","title":"Adicionando as constantes a Settings","text":"

J\u00e1 temos uma classe ideal para fazer isso em fast_zero/settings.py. Alteraremos essa classe para incluir estas constantes.

fast_zero/settings.py
from pydantic_settings import BaseSettings, SettingsConfigDict\n\n\nclass Settings(BaseSettings):\n    model_config = SettingsConfigDict(\n        env_file='.env', env_file_encoding='utf-8'\n    )\n\n    DATABASE_URL: str\n    SECRET_KEY: str\n    ALGORITHM: str\n    ACCESS_TOKEN_EXPIRE_MINUTES: int\n

Agora, precisamos adicionar estes valores ao nosso arquivo .env.

.env
DATABASE_URL=\"sqlite:///database.db\"\nSECRET_KEY=\"your-secret-key\"\nALGORITHM=\"HS256\"\nACCESS_TOKEN_EXPIRE_MINUTES=30\n

Com isso, podemos alterar o nosso c\u00f3digo em fast_zero/security.py para ler as constantes a partir da classe Settings.

"},{"location":"07/#removendo-as-constantes-do-codigo","title":"Removendo as constantes do c\u00f3digo","text":"

Primeiramente, carregaremos as configura\u00e7\u00f5es da classe Settings no in\u00edcio do m\u00f3dulo security.py.

fast_zero/security.py
from fast_zero.settings import Settings\n\nsettings = Settings()\n

Com isso, todos os lugares onde as constantes eram usadas devem ser substitu\u00eddos por settings.CONSTANTE. Por exemplo, na fun\u00e7\u00e3o create_access_token, alteraremos para usar as constantes da classe Settings:

fast_zero/security.py
def create_access_token(data: dict):\n    to_encode = data.copy()\n    expire = datetime.utcnow() + timedelta(\n        minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES\n    )\n    to_encode.update({'exp': expire})\n    encoded_jwt = encode(\n        to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM\n    )\n    return encoded_jwt\n

Desta forma, eliminamos todas as constantes do c\u00f3digo-fonte e passamos a usar as configura\u00e7\u00f5es a partir da classe Settings. Isso torna nosso c\u00f3digo mais seguro, pois as constantes sens\u00edveis, como a chave secreta, est\u00e3o agora seguras em nosso arquivo .env, e nosso c\u00f3digo fica mais modular, pois podemos facilmente alterar estas constantes simplesmente mudando os valores no arquivo .env. Al\u00e9m disso, essa abordagem facilita o gerenciamento de diferentes ambientes (como desenvolvimento, teste e produ\u00e7\u00e3o) pois cada ambiente pode ter seu pr\u00f3prio arquivo .env com suas configura\u00e7\u00f5es espec\u00edficas.

Precisamos alterar o teste para usar as mesmas vari\u00e1veis de ambiente do c\u00f3digo:

/tests/test_security.py
from http import HTTPStatus\n\nfrom jwt import decode\n\nfrom fast_zero.security import create_access_token, settings\n\n\ndef test_jwt():\n    data = {'test': 'test'}\n    token = create_access_token(data)\n\n    decoded = decode(\n        token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM]\n    )\n\n    assert decoded['test'] == data['test']\n    assert decoded['exp']\n
"},{"location":"07/#testando-se-tudo-funciona","title":"Testando se tudo funciona","text":"

Depois de todas essas mudan\u00e7as, \u00e9 muito importante garantir que tudo ainda est\u00e1 funcionando corretamente. Para isso, executaremos todos os testes que temos at\u00e9 agora.

$ Execu\u00e7\u00e3o no terminal!
task test\n\n# ...\n\ntests/test_app.py::test_root_deve_retornar_ok_e_ola_mundo PASSED\ntests/test_auth.py::test_get_token PASSED\ntests/test_db.py::test_create_user PASSED\ntests/test_users.py::test_create_user PASSED\ntests/test_users.py::test_read_users PASSED\ntests/test_users.py::test_read_users_with_users PASSED\ntests/test_users.py::test_update_user PASSED\ntests/test_users.py::test_delete_user PASSED\n

Se tudo estiver certo, todos os testes devem passar. Lembre-se de que a refatora\u00e7\u00e3o n\u00e3o deve alterar a funcionalidade do nosso c\u00f3digo - apenas torn\u00e1-lo mais f\u00e1cil de ler e manter.

"},{"location":"07/#commit","title":"Commit","text":"

Para finalizar, criaremos um commit para registrar todas as altera\u00e7\u00f5es que fizemos na nossa aplica\u00e7\u00e3o. Como essa \u00e9 uma grande mudan\u00e7a que envolve reestruturar a forma como lidamos com as rotas e mover as constantes para vari\u00e1veis de ambiente, podemos usar uma mensagem de commit descritiva que explique todas as principais altera\u00e7\u00f5es:

$ Execu\u00e7\u00e3o no terminal!
git add .\ngit commit -m \"Refatorando estrutura do projeto: Criado routers para Users e Auth; movido constantes para vari\u00e1veis de ambiente.\"\n
"},{"location":"07/#exercicio","title":"Exerc\u00edcio","text":"

Migre os endpoints e testes criados nos exerc\u00edcios anteriores para os locais corretos na nova estrutura da aplica\u00e7\u00e3o.

"},{"location":"07/#conclusao","title":"Conclus\u00e3o","text":"

Nesta aula, vimos como refatorar a estrutura do nosso projeto FastAPI para torn\u00e1-lo mais manuten\u00edvel. Organizamos nosso c\u00f3digo em diferentes arquivos e usamos o sistema de roteadores do FastAPI para separar diferentes partes da nossa API. Tamb\u00e9m mudamos algumas constantes para o arquivo de configura\u00e7\u00e3o, tornando nosso c\u00f3digo mais seguro e flex\u00edvel. Finalmente, atualizamos nossos testes para refletir a nova estrutura do projeto.

Refatorar \u00e9 um processo cont\u00ednuo - sempre h\u00e1 espa\u00e7o para melhorias. No entanto, com a estrutura que estabelecemos aqui, estamos em uma boa posi\u00e7\u00e3o para continuar a expandir nossa API no futuro.

Na pr\u00f3xima aula, exploraremos mais sobre autentica\u00e7\u00e3o e como gerenciar tokens de acesso e de atualiza\u00e7\u00e3o em nossa API FastAPI.

Agora que a aula acabou, \u00e9 um bom momento para voc\u00ea relembrar alguns conceitos e fixar melhor o conte\u00fado respondendo ao question\u00e1rio referente a ela.

Quiz

"},{"location":"08/","title":"Tornando o sistema de autentica\u00e7\u00e3o robusto","text":""},{"location":"08/#tornando-o-sistema-de-autenticacao-robusto","title":"Tornando o sistema de autentica\u00e7\u00e3o robusto","text":"

Objetivos da Aula:

Caso prefira ver a aula em v\u00eddeo

Esse aula ainda n\u00e3o est\u00e1 dispon\u00edvel em formato de v\u00eddeo, somente em texto ou live!

Aula Slides C\u00f3digo Quiz

Em nossas aulas anteriores, abordamos os fundamentos de um sistema de autentica\u00e7\u00e3o, mas existem diversas maneiras de aprimor\u00e1-lo para que ele se torne mais robusto e seguro. Nessa aula, enfrentaremos perguntas importantes, como: Como lidar com falhas e erros? Como garantir a seguran\u00e7a do sistema mesmo em cen\u00e1rios desafiadores? Estas s\u00e3o algumas das quest\u00f5es-chave que exploraremos.

Come\u00e7aremos com uma an\u00e1lise detalhada dos testes de autentica\u00e7\u00e3o. At\u00e9 agora, concentramo-nos em cen\u00e1rios ideais, onde o usu\u00e1rio existe e as condi\u00e7\u00f5es s\u00e3o favor\u00e1veis. Contudo, \u00e9 essencial testar tamb\u00e9m as situa\u00e7\u00f5es adversas e compreender como o sistema responde a falhas. Vamos, portanto, aprender a realizar testes eficazes para esses casos negativos.

Depois, passaremos para a implementa\u00e7\u00e3o de um elemento crucial em qualquer sistema de autentica\u00e7\u00e3o: a renova\u00e7\u00e3o do token. Esse mecanismo \u00e9 imprescind\u00edvel para manter a sess\u00e3o do usu\u00e1rio ativa e segura, mesmo quando o token original expira.

"},{"location":"08/#testes-para-autenticacao","title":"Testes para autentica\u00e7\u00e3o","text":"

Antes de mergulharmos nos testes, conversaremos um pouco sobre por que eles s\u00e3o t\u00e3o importantes. Na programa\u00e7\u00e3o, \u00e9 f\u00e1cil cair na armadilha de pensar que, se algo funciona na maioria das vezes, ent\u00e3o est\u00e1 tudo bem. Mas a verdade \u00e9 que \u00e9 nos casos marginais que os bugs mais dif\u00edceis de encontrar e corrigir costumam se esconder.

Por exemplo, o que acontece se tentarmos autenticar um usu\u00e1rio que n\u00e3o existe? Ou se tentarmos autenticar com as credenciais erradas? Se n\u00e3o testarmos esses cen\u00e1rios, podemos acabar com um sistema que parece funcionar na superf\u00edcie, mas que, na verdade, est\u00e1 cheio de falhas de seguran\u00e7a.

No c\u00f3digo apresentado, se observarmos atentamente, vemos que o erro HTTPException(status_code=HTTPStatus.FORBIDDEN, detail='Not enough permissions') em users.py na rota /{user_id} n\u00e3o est\u00e1 sendo coberto por nossos testes. Essa exce\u00e7\u00e3o \u00e9 lan\u00e7ada quando um usu\u00e1rio n\u00e3o autenticado ou um usu\u00e1rio sem permiss\u00f5es adequadas tenta acessar, ou alterar um recurso que n\u00e3o deveria.

Essa lacuna em nossos testes representa um risco potencial, pois n\u00e3o estamos verificando como nosso sistema se comporta quando algu\u00e9m tenta, por exemplo, alterar os detalhes de um usu\u00e1rio sem ter permiss\u00f5es adequadas. Embora possamos assumir que nosso sistema se comportar\u00e1 corretamente, a falta de testes nos deixa sem uma confirma\u00e7\u00e3o concreta.

"},{"location":"08/#testando-a-alteracao-de-um-usuario-nao-autorizado","title":"Testando a altera\u00e7\u00e3o de um usu\u00e1rio n\u00e3o autorizado","text":"

Agora, escreveremos alguns testes para esses casos. Vamos come\u00e7ar com um cen\u00e1rio simples: o que acontece quando um usu\u00e1rio tenta alterar as informa\u00e7\u00f5es de outro usu\u00e1rio?

Para testar isso, criaremos um novo teste chamado test_update_user_with_wrong_user.

tests/test_users.py
def test_update_user_with_wrong_user(client, user, token):\n    response = client.put(\n        f'/users/{user.id}',\n        headers={'Authorization': f'Bearer {token}'},\n        json={\n            'username': 'bob',\n            'email': 'bob@example.com',\n            'password': 'mynewpassword',\n        },\n    )\n    assert response.status_code == HTTPStatus.FORBIDDEN\n    assert response.json() == {'detail': 'Not enough permissions'}\n
Este teste vai simular um usu\u00e1rio tentando alterar as informa\u00e7\u00f5es de outro usu\u00e1rio. Se nosso sistema estiver funcionando corretamente, ele dever\u00e1 rejeitar essa tentativa e retornar um erro."},{"location":"08/#criando-modelos-por-demanda-com-factory-boy","title":"Criando modelos por demanda com factory-boy","text":"

Embora o teste que escrevemos esteja tecnicamente correto, ele ainda n\u00e3o funcionar\u00e1 adequadamente porque, atualmente, s\u00f3 temos um usu\u00e1rio em nosso banco de dados de testes. Precisamos de uma maneira de criar m\u00faltiplos usu\u00e1rios de teste facilmente, e \u00e9 a\u00ed que entra o factory-boy.

O factory-boy \u00e9 uma biblioteca que nos permite criar objetos de modelo de teste de forma r\u00e1pida e f\u00e1cil. Com ele, podemos criar uma \"f\u00e1brica\" de usu\u00e1rios que produzir\u00e1 novos objetos de usu\u00e1rio sempre que precisarmos. Isso nos permite criar m\u00faltiplos usu\u00e1rios de teste com facilidade, o que \u00e9 perfeito para nosso cen\u00e1rio atual.

Para come\u00e7ar, precisamos instalar o factory-boy em nosso ambiente de desenvolvimento:

$ Execu\u00e7\u00e3o no terminal!
poetry add --group dev factory-boy\n

Ap\u00f3s instalar o factory-boy, podemos criar uma UserFactory. Esta f\u00e1brica ser\u00e1 respons\u00e1vel por criar novos objetos de usu\u00e1rio sempre que precisarmos de um para nossos testes. A estrutura da f\u00e1brica ser\u00e1 a seguinte:

tests/conftest.py
import factory\n\n# ...\n\nclass UserFactory(factory.Factory):\n    class Meta:\n        model = User\n\n    username = factory.Sequence(lambda n: f'test{n}')\n    email = factory.LazyAttribute(lambda obj: f'{obj.username}@test.com')\n    password = factory.LazyAttribute(lambda obj: f'{obj.username}@example.com')\n

Explicando linha a linha, esse c\u00f3digo faz o seguinte:

Essa f\u00e1brica pode ser usada em testes para criar inst\u00e2ncias de User com dados predefinidos, facilitando a escrita de testes que requerem a presen\u00e7a de usu\u00e1rios no banco de dados. Isso \u00e9 extremamente \u00fatil ao escrever testes que requerem o estado pr\u00e9-configurado do banco de dados e ajuda a tornar os testes mais leg\u00edveis e manuten\u00edveis.

A seguir, podemos usar essa nova f\u00e1brica para criar m\u00faltiplos usu\u00e1rios de teste. Para fazer isso, modificamos nossa fixture de usu\u00e1rio existente para usar a UserFactory. Assim, sempre que executarmos nossos testes, teremos usu\u00e1rios diferentes dispon\u00edveis.

tests/conftest.py
@pytest.fixture\ndef user(session):\n    password = 'testtest'\n    user = UserFactory(password=get_password_hash(password))\n\n    session.add(user)\n    session.commit()\n    session.refresh(user)\n\n    user.clean_password = 'testtest'\n\n    return user\n\n\n@pytest.fixture\ndef other_user(session):\n    password = 'testtest'\n    user = UserFactory(password=get_password_hash(password))\n\n    session.add(user)\n    session.commit()\n    session.refresh(user)\n\n    user.clean_password = 'testtest'\n\n    return user\n

A cria\u00e7\u00e3o de outra fixture chamada other_user \u00e9 crucial para simular o cen\u00e1rio de um usu\u00e1rio tentando acessar ou modificar as informa\u00e7\u00f5es de outro usu\u00e1rio no sistema. Ao criar duas fixtures diferentes, user e other_user, podemos efetivamente simular dois usu\u00e1rios diferentes em nossos testes. Isso nos permite avaliar como nosso sistema reage quando um usu\u00e1rio tenta realizar uma a\u00e7\u00e3o n\u00e3o autorizada, como alterar as informa\u00e7\u00f5es de outro usu\u00e1rio.

Um aspecto interessante no uso das f\u00e1bricas \u00e9 que, sempre que forem chamadas, elas retornar\u00e3o um novo User, pois estamos fixando apenas a senha. Dessa forma, cada chamada a essa f\u00e1brica de usu\u00e1rios retornar\u00e1 um User diferente, com base nos atributos \"lazy\" que usamos.

Com essa nova configura\u00e7\u00e3o, podemos finalmente testar o cen\u00e1rio de um usu\u00e1rio tentando alterar as informa\u00e7\u00f5es de outro usu\u00e1rio. E como voc\u00ea pode ver, nossos testes passaram com sucesso, o que indica que nosso sistema est\u00e1 lidando corretamente com essa situa\u00e7\u00e3o.

tests/test_users.py
def test_update_user_with_wrong_user(client, other_user, token):\n    response = client.put(\n        f'/users/{other_user.id}',\n        headers={'Authorization': f'Bearer {token}'},\n        json={\n            'username': 'bob',\n            'email': 'bob@example.com',\n            'password': 'mynewpassword',\n        },\n    )\n    assert response.status_code == HTTPStatus.FORBIDDEN\n    assert response.json() == {'detail': 'Not enough permissions'}\n

Neste caso, n\u00e3o estamos usando a fixture user porque queremos simular um cen\u00e1rio onde o usu\u00e1rio associado ao token (autenticado) est\u00e1 tentando realizar uma a\u00e7\u00e3o sobre outro usu\u00e1rio, representado pela fixture other_user. Ao usar a other_user, garantimos que o id do usu\u00e1rio que estamos tentando modificar ou deletar n\u00e3o seja o mesmo do usu\u00e1rio associado ao token, mas que ainda assim exista no banco de dados.

Para enfatizar, a fixture user est\u00e1 sendo usada para representar o usu\u00e1rio que est\u00e1 autenticado atrav\u00e9s do token. Se us\u00e1ssemos a mesma fixture user neste teste, o sistema consideraria que a a\u00e7\u00e3o est\u00e1 sendo realizada pelo pr\u00f3prio usu\u00e1rio, o que n\u00e3o corresponderia ao cen\u00e1rio que queremos testar. Al\u00e9m disso, \u00e9 importante entender que o escopo das fixtures implica que, quando chamadas no mesmo teste, elas devem retornar o mesmo valor. Portanto, usar a user e other_user permite uma simula\u00e7\u00e3o mais precisa do comportamento desejado.

Com o teste implementado, vamos execut\u00e1-lo para ver se nosso sistema est\u00e1 protegido contra essa a\u00e7\u00e3o indevida:

$ Execu\u00e7\u00e3o no terminal!
task test\n\n# ...\n\ntests/test_app.py::test_root_deve_retornar_ok_e_ola_mundo PASSED\ntests/test_auth.py::test_get_token PASSED\ntests/test_db.py::test_create_user PASSED\ntests/test_users.py::test_create_user PASSED\ntests/test_users.py::test_read_users PASSED\ntests/test_users.py::test_read_users_with_users PASSED\ntests/test_users.py::test_update_user PASSED\ntests/test_users.py::test_update_user_with_wrong_user PASSED\ntests/test_users.py::test_delete_user PASSED\n

Se todos os testes passaram com sucesso, isso indica que nosso sistema est\u00e1 se comportando como esperado, inclusive no caso de tentativas indevidas de deletar um usu\u00e1rio.

"},{"location":"08/#testando-o-delete-com-o-usuario-errado","title":"Testando o DELETE com o usu\u00e1rio errado","text":"

Continuando nossos testes, agora testaremos o que acontece quando tentamos deletar um usu\u00e1rio com um usu\u00e1rio errado.

Talvez voc\u00ea esteja se perguntando, por que precisamos fazer isso? Bem, lembre-se de que a seguran\u00e7a \u00e9 uma parte crucial de qualquer sistema de autentica\u00e7\u00e3o. Precisamos garantir que um usu\u00e1rio n\u00e3o possa deletar a conta de outro usu\u00e1rio - apenas a pr\u00f3pria conta. Portanto, \u00e9 importante que testemos esse cen\u00e1rio para garantir que nosso sistema est\u00e1 seguro.

Aqui est\u00e1 o teste que usaremos:

tests/test_users.py
def test_delete_user_wrong_user(client, other_user, token):\n    response = client.delete(\n        f'/users/{other_user.id}',\n        headers={'Authorization': f'Bearer {token}'},\n    )\n    assert response.status_code == HTTPStatus.FORBIDDEN\n    assert response.json() == {'detail': 'Not enough permissions'}\n

Como voc\u00ea pode ver, esse teste tenta deletar o user de um id diferente usando o token do user. Se nosso sistema estiver funcionando corretamente, ele dever\u00e1 rejeitar essa tentativa e retornar um status 400 com uma mensagem de erro indicando que o usu\u00e1rio n\u00e3o tem permiss\u00f5es suficientes para realizar essa a\u00e7\u00e3o.

Vamos executar esse teste agora e ver o que acontece:

$ Execu\u00e7\u00e3o no terminal!
task test\n\n# ...\n\ntests/test_users.py::test_delete_user_wrong_user PASSED\n

\u00d3timo, nosso teste passou! Isso significa que nosso sistema est\u00e1 corretamente impedindo um usu\u00e1rio de deletar a conta de outro usu\u00e1rio.

Agora que terminamos de testar a autoriza\u00e7\u00e3o, passaremos para o pr\u00f3ximo desafio: testar tokens expirados. Lembre-se, em um sistema de autentica\u00e7\u00e3o robusto, um token deve expirar ap\u00f3s um certo per\u00edodo de tempo por motivos de seguran\u00e7a. Portanto, \u00e9 importante que testemos o que acontece quando tentamos usar um token expirado. Veremos isso na pr\u00f3xima se\u00e7\u00e3o.

"},{"location":"08/#testando-a-expiracao-do-token","title":"Testando a expira\u00e7\u00e3o do token","text":"

Continuando com nossos testes de autentica\u00e7\u00e3o, a pr\u00f3xima coisa que precisamos testar \u00e9 a expira\u00e7\u00e3o do token. Tokens de autentica\u00e7\u00e3o s\u00e3o normalmente projetados para expirar ap\u00f3s um certo per\u00edodo de tempo por motivos de seguran\u00e7a. Isso evita que algu\u00e9m que tenha obtido um token possa us\u00e1-lo indefinidamente se ele for roubado ou perdido. Portanto, \u00e9 importante que verifiquemos que nosso sistema esteja tratando corretamente a expira\u00e7\u00e3o dos tokens.

Para realizar esse teste, usaremos uma biblioteca chamada freezegun. freezegun\u00e9 uma biblioteca Python que nos permite \"congelar\" o tempo em um ponto espec\u00edfico ou avan\u00e7\u00e1-lo conforme necess\u00e1rio durante os testes. Isso \u00e9 especialmente \u00fatil para testar funcionalidades sens\u00edveis ao tempo, como a expira\u00e7\u00e3o de tokens, sem ter que esperar em tempo real.

Primeiro, precisamos instalar a biblioteca:

poetry add --group dev freezegun\n

Agora criaremos nosso teste. Come\u00e7aremos pegando um token para um usu\u00e1rio, congelando o tempo, esperando pelo tempo de expira\u00e7\u00e3o do token e, em seguida, tentando usar o token para acessar um endpoint que requer autentica\u00e7\u00e3o.

Ao elaborarmos o teste, usaremos a funcionalidade de congelamento de tempo do freezegun. O objetivo \u00e9 simular a cria\u00e7\u00e3o de um token \u00e0s 12:00 e, em seguida, verificar sua expira\u00e7\u00e3o \u00e0s 12:31. Neste cen\u00e1rio, estamos utilizando o conceito de \"viajar no tempo\" para al\u00e9m do per\u00edodo de validade do token, garantindo que a tentativa subsequente de utiliz\u00e1-lo resultar\u00e1 em um erro de autentica\u00e7\u00e3o.

tests/test_auth.py
from freezegun import freeze_time\n\n# ...\n\ndef test_token_expired_after_time(client, user):\n    with freeze_time('2023-07-14 12:00:00'):\n        response = client.post(\n            '/auth/token',\n            data={'username': user.email, 'password': user.clean_password},\n        )\n        assert response.status_code == HTTPStatus.OK\n        token = response.json()['access_token']\n\n    with freeze_time('2023-07-14 12:31:00'):\n        response = client.put(\n            f'/users/{user.id}',\n            headers={'Authorization': f'Bearer {token}'},\n            json={\n                'username': 'wrongwrong',\n                'email': 'wrong@wrong.com',\n                'password': 'wrong',\n            },\n        )\n        assert response.status_code == HTTPStatus.UNAUTHORIZED\n        assert response.json() == {'detail': 'Could not validate credentials'}\n

Lembre-se de que configuramos nosso token para expirar ap\u00f3s 30 minutos. Portanto, n\u00f3s avan\u00e7amos o tempo em 31 minutos para garantir que o token tenha expirado.

Agora, executaremos nosso teste e ver o que acontece:

$ Execu\u00e7\u00e3o no terminal!
task test\n\n# ...\n\ntests/test_auth.py::test_token_expired_after_time FAILED\n

O motivo deste teste falhar \u00e9 que n\u00e3o temos uma condi\u00e7\u00e3o que fa\u00e7a a captura deste cen\u00e1rio. Em fast_zero/security.py, o \u00fanico caso de erro esperado \u00e9 em rela\u00e7\u00e3o \u00e0 dados inv\u00e1lidos durante o decode (DecodeError). Precisamos adicionar uma exception para quando o token estiver no formato correto, mas n\u00e3o estiver no prazo de validade. Quando seu tempo de uso j\u00e1 est\u00e1 expirado.

Para isso, podemos importar do pyjwt o objeto ExpiredSignatureError que \u00e9 a exce\u00e7\u00e3o levantada para a decodifica\u00e7\u00e3o de um c\u00f3digo expirado:

fast_zero/security.py
from datetime import datetime, timedelta\nfrom http import HTTPStatus\n\nfrom fastapi import Depends, HTTPException\nfrom fastapi.security import OAuth2PasswordBearer\nfrom jwt import DecodeError, ExpiredSignatureError, decode, encode\nfrom pwdlib import PasswordHash\nfrom sqlalchemy import select\nfrom sqlalchemy.orm import Session\n\n# ...\n\ndef get_current_user(\n    session: Session = Depends(get_session),\n    token: str = Depends(oauth2_scheme),\n):\n    credentials_exception = HTTPException(\n        status_code=HTTPStatus.UNAUTHORIZED,\n        detail='Could not validate credentials',\n        headers={'WWW-Authenticate': 'Bearer'},\n    )\n\n    try:\n        payload = decode(\n            token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM]\n        )\n        username: str = payload.get('sub')\n        if not username:\n            raise credentials_exception\n        token_data = TokenData(username=username)\n    except DecodeError:\n        raise credentials_exception\n    except ExpiredSignatureError:\n        raise credentials_exception\n\n    user = session.scalar(\n        select(User).where(User.email == token_data.username)\n    )\n\n    if not user:\n        raise credentials_exception\n\n    return user\n

Com essa simples altera\u00e7\u00e3o, temos uma sa\u00edda de erro para quando o token estiver inv\u00e1lido. Por quest\u00f5es de seguran\u00e7a, vamos manter a mesma mensagem usada antes. Dizendo que n\u00e3o conseguimos validar as credenciais.

Assim, vamos executar o teste novamente:

$ Execu\u00e7\u00e3o no terminal!
task test\n\n# ...\n\ntests/test_auth.py::test_token_expired_after_time PASSED\n

Temos a demonstra\u00e7\u00e3o de sucesso.

No entanto, ainda h\u00e1 uma coisa que precisamos implementar: a atualiza\u00e7\u00e3o de tokens. Atualmente, quando um token expira, o usu\u00e1rio teria que fazer login novamente para obter um novo token. Isso n\u00e3o \u00e9 uma \u00f3tima experi\u00eancia para o usu\u00e1rio. Em vez disso, gostar\u00edamos de oferecer a possibilidade de o usu\u00e1rio atualizar seu token quando ele estiver prestes a expirar. Veremos como fazer isso na pr\u00f3xima se\u00e7\u00e3o.

"},{"location":"08/#testando-o-usuario-nao-existente-e-senha-incorreta","title":"Testando o usu\u00e1rio n\u00e3o existente e senha incorreta","text":"

Na constru\u00e7\u00e3o de qualquer sistema de autentica\u00e7\u00e3o, \u00e9 crucial garantir que os casos de erro sejam tratados corretamente. Isso n\u00e3o s\u00f3 previne poss\u00edveis falhas de seguran\u00e7a, mas tamb\u00e9m permite fornecer feedback \u00fatil aos usu\u00e1rios.

Em nossa implementa\u00e7\u00e3o atual, temos duas situa\u00e7\u00f5es espec\u00edficas que devem retornar um erro: quando um usu\u00e1rio inexistente tenta fazer login e quando uma senha incorreta \u00e9 fornecida. Abordaremos esses casos de erro em nossos pr\u00f3ximos testes.

Embora possa parecer redundante testar esses casos j\u00e1 que ambos resultam no mesmo erro, \u00e9 importante verificar que ambos os cen\u00e1rios est\u00e3o corretamente tratados. Isso nos permitir\u00e1 manter a robustez do nosso sistema conforme ele evolui e muda ao longo do tempo.

"},{"location":"08/#testando-a-excecao-para-um-usuario-inexistente","title":"Testando a exce\u00e7\u00e3o para um usu\u00e1rio inexistente","text":"

Para este cen\u00e1rio, precisamos enviar um request para o endpoint de token com um e-mail que n\u00e3o existe no banco de dados. A resposta esperada \u00e9 um HTTP 400 com a mensagem de detalhe 'Incorrect email or password'.

tests/test_auth.py
def test_token_inexistent_user(client):\n    response = client.post(\n        '/auth/token',\n        data={'username': 'no_user@no_domain.com', 'password': 'testtest'},\n    )\n    assert response.status_code == HTTPStatus.BAD_REQUEST\n    assert response.json() == {'detail': 'Incorrect email or password'}\n
"},{"location":"08/#testando-a-excecao-para-uma-senha-incorreta","title":"Testando a exce\u00e7\u00e3o para uma senha incorreta","text":"

Aqui, precisamos enviar um request para o endpoint de token com uma senha incorreta para um usu\u00e1rio existente. A resposta esperada \u00e9 um HTTP 400 com a mensagem de detalhe 'Incorrect email or password'.

tests/test_auth.py
def test_token_wrong_password(client, user):\n    response = client.post(\n        '/auth/token',\n        data={'username': user.email, 'password': 'wrong_password'}\n    )\n    assert response.status_code == HTTPStatus.BAD_REQUEST\n    assert response.json() == {'detail': 'Incorrect email or password'}\n

Com esses testes, garantimos que nossas exce\u00e7\u00f5es est\u00e3o sendo lan\u00e7adas corretamente. Essa \u00e9 uma parte importante da constru\u00e7\u00e3o de um sistema de autentica\u00e7\u00e3o robusto, pois nos permite ter confian\u00e7a de que estamos tratando corretamente os casos de erro.

"},{"location":"08/#implementando-o-refresh-do-token","title":"Implementando o refresh do token","text":"

O processo de renova\u00e7\u00e3o de token \u00e9 uma parte essencial na implementa\u00e7\u00e3o de autentica\u00e7\u00e3o JWT. Em muitos sistemas, por raz\u00f5es de seguran\u00e7a, os tokens de acesso t\u00eam um tempo de vida relativamente curto. Isso significa que eles expiram ap\u00f3s um determinado per\u00edodo de tempo, e quando isso acontece, o cliente precisa obter um novo token para continuar acessando os recursos do servidor. Aqui \u00e9 onde o processo de renova\u00e7\u00e3o de token entra: permite que um cliente obtenha um novo token de acesso sem a necessidade de autentica\u00e7\u00e3o completa (por exemplo, sem ter que fornecer novamente o nome de usu\u00e1rio e senha).

Agora implementaremos a fun\u00e7\u00e3o de renova\u00e7\u00e3o de token em nosso c\u00f3digo.

fast_zero/routes/auth.py
from fast_zero.security import (\n    create_access_token,\n    get_current_user,\n    verify_password,\n)\n\n# ...\n\n@router.post('/refresh_token', response_model=Token)\ndef refresh_access_token(\n    user: User = Depends(get_current_user),\n):\n    new_access_token = create_access_token(data={'sub': user.email})\n\n    return {'access_token': new_access_token, 'token_type': 'bearer'}\n

Implementaremos tamb\u00e9m um teste para verificar se a fun\u00e7\u00e3o de renova\u00e7\u00e3o de token est\u00e1 funcionando corretamente.

tests/test_auth.py
def test_refresh_token(client, user, token):\n    response = client.post(\n        '/auth/refresh_token',\n        headers={'Authorization': f'Bearer {token}'},\n    )\n\n    data = response.json()\n\n    assert response.status_code == HTTPStatus.OK\n    assert 'access_token' in data\n    assert 'token_type' in data\n    assert data['token_type'] == 'bearer'\n

Ainda \u00e9 importante garantir que nosso sistema trate corretamente os tokens expirados. Para isso, adicionaremos um teste que verifica se um token expirado n\u00e3o pode ser usado para renovar um token.

tests/test_auth.py
def test_token_expired_dont_refresh(client, user):\n    with freeze_time('2023-07-14 12:00:00'):\n        response = client.post(\n            '/auth/token',\n            data={'username': user.email, 'password': user.clean_password},\n        )\n        assert response.status_code == HTTPStatus.OK\n        token = response.json()['access_token']\n\n    with freeze_time('2023-07-14 12:31:00'):\n        response = client.post(\n            '/auth/refresh_token',\n            headers={'Authorization': f'Bearer {token}'},\n        )\n        assert response.status_code == HTTPStatus.UNAUTHORIZED\n        assert response.json() == {'detail': 'Could not validate credentials'}\n

Agora, se executarmos nossos testes, todos eles devem passar, incluindo os novos testes que acabamos de adicionar.

$ Execu\u00e7\u00e3o no terminal!
task test\n\n# ...\n\ntests/test_app.py::test_root_deve_retornar_ok_e_ola_mundo PASSED\ntests/test_auth.py::test_get_token PASSED\ntests/test_auth.py::test_token_inexistent_user PASSED\ntests/test_auth.py::test_token_wrong_password PASSED\ntests/test_auth.py::test_refresh_token PASSED\ntests/test_auth.py::test_token_expired_after_time PASSED\ntests/test_db.py::test_create_user PASSED\ntests/test_users.py::test_create_user PASSED\ntests/test_users.py::test_read_users PASSED\ntests/test_users.py::test_read_users_with_users PASSED\ntests/test_users.py::test_update_user PASSED\ntests/test_users.py::test_update_user_with_wrong_user PASSED\ntests/test_users.py::test_delete_user PASSED\ntests/test_users.py::test_delete_user_wrong_user PASSED\ntests/test_users.py::test_token_expired_dont_refresh PASSED\n

Com esses testes, podemos ter certeza de que cobrimos alguns casos importantes relacionados \u00e0 autentica\u00e7\u00e3o de usu\u00e1rios em nossa API.

"},{"location":"08/#commit","title":"Commit","text":"

Agora, faremos um commit com as altera\u00e7\u00f5es que fizemos.

$ Execu\u00e7\u00e3o no terminal!
git add .\ngit commit -m \"Implementando o refresh do token e testes de autoriza\u00e7\u00e3o\"\n
"},{"location":"08/#conclusao","title":"Conclus\u00e3o","text":"

Nesta aula, abordamos uma grande quantidade de t\u00f3picos cruciais para a constru\u00e7\u00e3o de uma aplica\u00e7\u00e3o web segura e robusta. Come\u00e7amos com a implementa\u00e7\u00e3o da funcionalidade de renova\u00e7\u00e3o do token JWT, uma pe\u00e7a fundamental na arquitetura de autentica\u00e7\u00e3o baseada em token. Este processo garante que os usu\u00e1rios possam continuar acessando a aplica\u00e7\u00e3o, mesmo ap\u00f3s o token inicial ter expirado, sem a necessidade de fornecer suas credenciais novamente.

Por\u00e9m, a implementa\u00e7\u00e3o do c\u00f3digo foi apenas a primeira parte do que fizemos. Uma parte significativa da nossa aula foi dedicada a testar de maneira exaustiva a nossa aplica\u00e7\u00e3o. Escrevemos testes para verificar o comportamento b\u00e1sico das nossas rotas de autentica\u00e7\u00e3o, mas n\u00e3o paramos por a\u00ed. Tamb\u00e9m consideramos v\u00e1rios casos de borda que podem surgir durante a autentica\u00e7\u00e3o de um usu\u00e1rio.

Testamos, por exemplo, o que acontece quando se tenta obter um token com credenciais incorretas. Verificamos o comportamento da nossa aplica\u00e7\u00e3o quando um token expirado \u00e9 utilizado. Esses testes nos ajudam a garantir que nossa aplica\u00e7\u00e3o se comporte de maneira adequada n\u00e3o apenas nas situa\u00e7\u00f5es mais comuns, mas tamb\u00e9m quando algo sai do esperado.

Al\u00e9m disso, ao implementar esses testes, n\u00f3s garantimos que futuras altera\u00e7\u00f5es no nosso c\u00f3digo n\u00e3o ir\u00e3o quebrar funcionalidades j\u00e1 existentes. Testes automatizados s\u00e3o uma parte fundamental de qualquer aplica\u00e7\u00e3o de alta qualidade, e o que fizemos nesta aula vai al\u00e9m do b\u00e1sico, mostrando como lidar com cen\u00e1rios complexos e realistas. Nos aproximando do ambiente de produ\u00e7\u00e3o.

Na pr\u00f3xima aula, utilizaremos a infraestrutura de autentica\u00e7\u00e3o que criamos aqui para permitir que os usu\u00e1rios criem, leiam, atualizem e deletem suas pr\u00f3prias listas de tarefas. Isso vai nos permitir explorar ainda mais as funcionalidades do FastAPI e do SQLAlchemy, al\u00e9m de continuar a expandir a nossa su\u00edte de testes.

Agora que a aula acabou, \u00e9 um bom momento para voc\u00ea relembrar alguns conceitos e fixar melhor o conte\u00fado respondendo ao question\u00e1rio referente a ela.

Quiz

"},{"location":"09/","title":"Criando Rotas CRUD para Gerenciamento de Tarefas em FastAPI","text":""},{"location":"09/#criando-rotas-crud-para-gerenciamento-de-tarefas-em-fastapi","title":"Criando Rotas CRUD para Gerenciamento de Tarefas em FastAPI","text":"

Objetivos da Aula:

Caso prefira ver a aula em v\u00eddeo

Esse aula ainda n\u00e3o est\u00e1 dispon\u00edvel em formato de v\u00eddeo, somente em texto ou live!

Aula Slides C\u00f3digo Quiz

Ap\u00f3s termos cumprido todos os passos essenciais para estabelecer um sistema eficiente de gerenciamento de usu\u00e1rios, estamos agora preparados para levar nossa aplica\u00e7\u00e3o a um novo momento, introduzindo um sistema de gerenciamento de tarefas, mais conhecido como todo list. Nesta nova etapa, garantiremos que somente o usu\u00e1rio que criou uma tarefa tenha o direito de acessar e editar tal tarefa, refor\u00e7ando a seguran\u00e7a e a privacidade dos dados. Para isso, desenvolveremos diversos endpoints e implementaremos as medidas de restri\u00e7\u00e3o e autentica\u00e7\u00e3o que aprimoramos na \u00faltima aula.

"},{"location":"09/#estrutura-inicial-do-codigo","title":"Estrutura inicial do c\u00f3digo","text":"

Primeiro, criaremos um novo arquivo chamado todos.py no diret\u00f3rio de routers:

fast_zero/routers/todos.py
from fastapi import APIRouter\n\nrouter = APIRouter(prefix='/todos', tags=['todos'])\n

Neste c\u00f3digo, criamos uma nova inst\u00e2ncia da classe APIRouter do FastAPI. Esta classe \u00e9 usada para definir as rotas de nossa aplica\u00e7\u00e3o. A inst\u00e2ncia router funcionar\u00e1 como um mini aplicativo FastAPI, que poder\u00e1 ter suas pr\u00f3prias rotas, modelos de resposta, etc.

A op\u00e7\u00e3o prefix no construtor do APIRouter \u00e9 usada para definir um prefixo comum para todas as rotas definidas neste roteador. Isso significa que todas as rotas que definirmos neste roteador come\u00e7ar\u00e3o com /todos. Usamos um prefixo aqui porque queremos agrupar todas as rotas relacionadas a tarefas em um lugar. Isso torna nossa aplica\u00e7\u00e3o mais organizada e f\u00e1cil de entender.

A op\u00e7\u00e3o tags \u00e9 usada para agrupar as rotas em se\u00e7\u00f5es no documento interativo de API gerado pelo FastAPI (como Swagger UI e ReDoc). Todas as rotas que definirmos neste roteador aparecer\u00e3o na se\u00e7\u00e3o \"todos\" da documenta\u00e7\u00e3o da API.

Ap\u00f3s definir o roteador, precisamos inclu\u00ed-lo em nossa aplica\u00e7\u00e3o principal. Atualizaremos o arquivo fast_zero/app.py para incluir as rotas de tarefas que criaremos:

fast_zero/app.py
from http import HTTPStatus\n\nfrom fastapi import FastAPI\n\nfrom fast_zero.routers import auth, todos, users\nfrom fast_zero.schemas import Message\n\napp = FastAPI()\n\napp.include_router(users.router)\napp.include_router(auth.router)\napp.include_router(todos.router)\n\n\n@app.get('/', status_code=HTTPStatus.OK, response_model=Message)\ndef read_root():\n    return {'message': 'Ol\u00e1 Mundo!'}\n

Neste c\u00f3digo, chamamos o m\u00e9todo include_router do FastAPI para cada roteador que definimos. Este m\u00e9todo adiciona todas as rotas do roteador \u00e0 nossa aplica\u00e7\u00e3o. Com isso, nossa aplica\u00e7\u00e3o agora ter\u00e1 todas as rotas definidas nos roteadores users, auth e todos.

"},{"location":"09/#implementacao-da-tabela-no-banco-de-dados","title":"Implementa\u00e7\u00e3o da tabela no Banco de dados","text":"

Agora, implementaremos a tabela 'Todos' no nosso banco de dados. Esta tabela estar\u00e1 diretamente relacionada \u00e0 tabela 'User', pois toda tarefa pertence a um usu\u00e1rio. Esta rela\u00e7\u00e3o \u00e9 crucial para garantir que s\u00f3 o usu\u00e1rio dono da tarefa possa acessar e modificar suas tarefas.

fast_zero/models.py
from datetime import datetime\nfrom enum import Enum\n\nfrom sqlalchemy import ForeignKey, func\nfrom sqlalchemy.orm import Mapped, mapped_column, registry, relationship\n\ntable_registry = registry()\n\n\nclass TodoState(str, Enum):\n    draft = 'draft'\n    todo = 'todo'\n    doing = 'doing'\n    done = 'done'\n    trash = 'trash'\n\n\n@table_registry.mapped_as_dataclass\nclass User:\n    __tablename__ = 'users'\n\n    id: Mapped[int] = mapped_column(init=False, primary_key=True)\n    username: Mapped[str] = mapped_column(unique=True)\n    password: Mapped[str]\n    email: Mapped[str] = mapped_column(unique=True)\n    created_at: Mapped[datetime] = mapped_column(\n        init=False, server_default=func.now()\n    )\n\n    todos: Mapped[list['Todo']] = relationship(\n        init=False, back_populates='user', cascade='all, delete-orphan'\n    )\n\n\n@table_registry.mapped_as_dataclass\nclass Todo:\n    __tablename__ = 'todos'\n\n    id: Mapped[int] = mapped_column(init=False, primary_key=True)\n    title: Mapped[str]\n    description: Mapped[str]\n    state: Mapped[TodoState]\n\n    user_id: Mapped[int] = mapped_column(ForeignKey('users.id'))\n\n    user: Mapped[User] = relationship(init=False, back_populates='todos')\n

Neste ponto, \u00e9 importante compreender o conceito de relationship em SQLAlchemy. A fun\u00e7\u00e3o relationship define como as duas tabelas ir\u00e3o interagir. O argumento back_populates permite uma associa\u00e7\u00e3o bidirecional entre as tabelas, ou seja, se tivermos um usu\u00e1rio, podemos acessar suas tarefas atrav\u00e9s do atributo 'todos', e se tivermos uma tarefa, podemos encontrar o usu\u00e1rio a que ela pertence atrav\u00e9s do atributo 'user'. O argumento cascade determina o que ocorre com as tarefas quando o usu\u00e1rio associado a elas \u00e9 deletado. Ao definir 'all, delete-orphan', estamos instruindo o SQLAlchemy a deletar todas as tarefas de um usu\u00e1rio quando este for deletado.

O uso do tipo Enum em state: Mapped[TodoState] \u00e9 outro ponto importante. Enum \u00e9 um tipo de dado especial que permite a cria\u00e7\u00e3o de um conjunto fixo de constantes. Neste caso, estamos utilizando para definir os poss\u00edveis estados de uma tarefa.

Estes conceitos podem parecer um pouco complexos agora, mas ficar\u00e3o mais claros quando come\u00e7armos a implementar os testes.

"},{"location":"09/#testando-as-novas-implementacoes-do-banco-de-dados","title":"Testando as novas implementa\u00e7\u00f5es do banco de dados","text":"

Embora tenhamos 100% de cobertura de c\u00f3digo, isso n\u00e3o garante que tudo esteja funcionando corretamente. S\u00f3 implementamos a estrutura do banco de dados, mas n\u00e3o testamos a l\u00f3gica de como as tabelas e as rela\u00e7\u00f5es funcionam na pr\u00e1tica.

Para isso, criamos um teste para verificar se a rela\u00e7\u00e3o entre tarefas e usu\u00e1rios est\u00e1 funcionando corretamente. Este teste cria uma nova tarefa para um usu\u00e1rio e verifica se essa tarefa aparece na lista de tarefas desse usu\u00e1rio.

tests/test_db.py
from fast_zero.models import Todo, User\n# ...\ndef test_create_todo(session, user: User):\n    todo = Todo(\n        title='Test Todo',\n        description='Test Desc',\n        state='draft',\n        user_id=user.id,\n    )\n\n    session.add(todo)\n    session.commit()\n    session.refresh(todo)\n\n    user = session.scalar(select(User).where(User.id == user.id))\n\n    assert todo in user.todos\n

Com isso, voc\u00ea pode executar os testes:

$ Execu\u00e7\u00e3o no terminal!
task test tests/test_db.py\n# ...\ntests/test_db.py::test_create_user_without_todos PASSED\ntests/test_db.py::test_create_todo PASSED\n

Isso mostra que os testes foram bem-sucedidos. Mesmo sem testes mais extensivos, agora come\u00e7aremos a criar os esquemas para esse modelo e, em seguida, os endpoints.

"},{"location":"09/#schemas-para-todos","title":"Schemas para Todos","text":"

Criaremos dois esquemas para nosso modelo de tarefas (todos): TodoSchema e TodoPublic.

fast_zero/schemas.py
from fast_zero.models import TodoState\n\n#...\n\nclass TodoSchema(BaseModel):\n    title: str\n    description: str\n    state: TodoState\n\n\nclass TodoPublic(TodoSchema):\n    id: int\n\n\nclass TodoList(BaseModel):\n    todos: list[TodoPublic]\n

TodoSchema ser\u00e1 usado para validar os dados de entrada quando uma nova tarefa \u00e9 criada e TodoPublic ser\u00e1 usado para validar os dados de sa\u00edda quando uma tarefa \u00e9 retornada em um endpoint.

"},{"location":"09/#endpoint-de-criacao","title":"Endpoint de cria\u00e7\u00e3o","text":"

Criamos o primeiro endpoint para a cria\u00e7\u00e3o de tarefas. Este \u00e9 um endpoint POST na rota '/todos'. \u00c9 importante destacar que, para criar uma tarefa, um usu\u00e1rio precisa estar autenticado e s\u00f3 esse usu\u00e1rio autenticado ser\u00e1 o propriet\u00e1rio da tarefa.

fast_zero/routers/todos.py
from typing import Annotated\n\nfrom fastapi import APIRouter, Depends\nfrom sqlalchemy.orm import Session\n\nfrom fast_zero.database import get_session\nfrom fast_zero.models import Todo, User\nfrom fast_zero.schemas import TodoPublic, TodoSchema\nfrom fast_zero.security import get_current_user\n\nrouter = APIRouter()\n\nSession = Annotated[Session, Depends(get_session)]\nCurrentUser = Annotated[User, Depends(get_current_user)]\n\nrouter = APIRouter(prefix='/todos', tags=['todos'])\n\n\n@router.post('/', response_model=TodoPublic)\ndef create_todo(\n    todo: TodoSchema,\n    user: CurrentUser,\n    session: Session,\n):\n    db_todo = Todo(\n        title=todo.title,\n        description=todo.description,\n        state=todo.state,\n        user_id=user.id,\n    )\n    session.add(db_todo)\n    session.commit()\n    session.refresh(db_todo)\n\n    return db_todo\n

Neste endpoint, fazemos uso da depend\u00eancia get_current_user que garante que somente usu\u00e1rios autenticados possam criar tarefas, protegendo assim nossa aplica\u00e7\u00e3o.

"},{"location":"09/#testando-o-endpoint-de-criacao","title":"Testando o endpoint de cria\u00e7\u00e3o","text":"

Para garantir que nosso endpoint est\u00e1 funcionando corretamente, criamos um teste para ele. Este teste verifica se o endpoint '/todos' est\u00e1 criando tarefas corretamente.

tests/test_todos.py
def test_create_todo(client, token):\n    response = client.post(\n        '/todos/',\n        headers={'Authorization': f'Bearer {token}'},\n        json={\n            'title': 'Test todo',\n            'description': 'Test todo description',\n            'state': 'draft',\n        },\n    )\n    assert response.json() == {\n        'id': 1,\n        'title': 'Test todo',\n        'description': 'Test todo description',\n        'state': 'draft',\n    }\n

No teste, fazemos uma requisi\u00e7\u00e3o POST para o endpoint '/todos' passando um token de autentica\u00e7\u00e3o v\u00e1lido e um JSON com os dados da tarefa a ser criada. Em seguida, verificamos se a resposta cont\u00e9m os dados corretos da tarefa criada.

Para executar este teste, voc\u00ea deve usar o comando abaixo no terminal:

$ Execu\u00e7\u00e3o no terminal!
task test tests/test_todos.py\n# ...\ntests/test_todos.py::test_create_todo PASSED\n

Com essa implementa\u00e7\u00e3o, os testes devem passar. Por\u00e9m, apesar do sucesso dos testes, nosso c\u00f3digo ainda n\u00e3o est\u00e1 completamente pronto. Ainda \u00e9 necess\u00e1rio criar uma migra\u00e7\u00e3o para a tabela de tarefas no banco de dados.

"},{"location":"09/#criando-a-migracao-da-nova-tabela","title":"Criando a migra\u00e7\u00e3o da nova tabela","text":"

Agora que temos nosso modelo de tarefas definido, precisamos criar uma migra\u00e7\u00e3o para adicionar a tabela de tarefas ao nosso banco de dados. Usamos o Alembic para criar e gerenciar nossas migra\u00e7\u00f5es.

$ Execu\u00e7\u00e3o no terminal!
alembic revision --autogenerate -m \"create todos table\"\n\n# ...\n\nGenerating /<caminho>/fast_zero/migrations/versions/de865434f506_create_todos_table.py\n

Este comando gera um arquivo de migra\u00e7\u00e3o, que se parece com o c\u00f3digo abaixo:

migrations/versions/de865434f506_create_todos_table.py
def upgrade() -> None:\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.create_table('todos',\n    sa.Column('id', sa.Integer(), nullable=False),\n    sa.Column('title', sa.String(), nullable=False),\n    sa.Column('description', sa.String(), nullable=False),\n    sa.Column('state', sa.Enum('draft', 'todo', 'doing', 'done', 'trash', name='todostate'), nullable=False),\n    sa.Column('user_id', sa.Integer(), nullable=False),\n    sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),\n    sa.PrimaryKeyConstraint('id')\n    )\n    # ### end Alembic commands ###\n\n\ndef downgrade() -> None:\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.drop_table('todos')\n    # ### end Alembic commands ###\n

Depois que a migra\u00e7\u00e3o for criada, precisamos aplic\u00e1-la ao nosso banco de dados. Execute o comando alembic upgrade head para aplicar a migra\u00e7\u00e3o.

$ Execu\u00e7\u00e3o no terminal!
alembic upgrade head\nINFO  [alembic.runtime.migration] Context impl SQLiteImpl.\nINFO  [alembic.runtime.migration] Will assume non-transactional DDL.\nINFO  [alembic.runtime.migration] Running upgrade e018397cecf4 -> de865434f506, create todos table\n

Agora que a migra\u00e7\u00e3o foi aplicada, nosso banco de dados deve ter uma nova tabela de tarefas. Para verificar, voc\u00ea pode abrir o banco de dados com o comando sqlite3 database.db e depois executar o comando .schema para ver o esquema do banco de dados.

$ Execu\u00e7\u00e3o no terminal!
sqlite3 database.db\n# ...\nsqlite> .schema\n# ...\nCREATE TABLE todos (\n    id INTEGER NOT NULL,\n    title VARCHAR NOT NULL,\n    description VARCHAR NOT NULL,\n    state VARCHAR(5) NOT NULL,\n    user_id INTEGER NOT NULL,\n    PRIMARY KEY (id),\n    FOREIGN KEY(user_id) REFERENCES users (id)\n);\n

Finalmente, agora que temos a tabela de tarefas em nosso banco de dados, podemos testar nosso endpoint de cria\u00e7\u00e3o de tarefas no Swagger. Para fazer isso, execute nosso servidor FastAPI e abra o Swagger no seu navegador.

"},{"location":"09/#endpoint-de-listagem","title":"Endpoint de listagem","text":"

Agora que criamos a nossa migra\u00e7\u00e3o e temos o endpoint de cria\u00e7\u00e3o de Todos, temos que criar nosso endpoint de listagem de tarefas. Ele deve listar todas as tarefas de acordo com o CurrentUser.

Algumas coisas adicionais e que podem ser importantes na hora de recuperar as tarefas \u00e9 fazer um filtro de busca. Em alguns momentos queremos buscar uma tarefa por t\u00edtulo, em outro momento por descri\u00e7\u00e3o, \u00e0s vezes s\u00f3 pelo estado. Por exemplo, somente tarefas conclu\u00eddas.

Para fazer isso, podemos contar com um recurso do FastAPI chamado Query. A Query permite que definamos par\u00e2metros espec\u00edficos na URL, que podem ser utilizados para filtrar os resultados retornados pelo endpoint. Isso \u00e9 feito atrav\u00e9s da inclus\u00e3o de par\u00e2metros como query strings na URL, interpretados pelo FastAPI para ajustar a consulta ao banco de dados.

Por exemplo, uma query string simples pode ser: todos/?title=\"batatinha\".

Uma caracter\u00edstica importante das queries do FastAPI \u00e9 que podemos juntar mais de um atributo em uma busca. Por exemplo, podemos buscar somente as tarefas a fazer que contenham no t\u00edtulo \"trabalho\". Dessa forma, temos um endpoint mais eficiente, j\u00e1 que podemos realizar buscas complexas e refinadas com uma \u00fanica chamada.

A combina\u00e7\u00e3o poderia ser algo como: todos/?title=\"batatinha\"&status=todo.

A combina\u00e7\u00e3o de diferentes par\u00e2metros de query n\u00e3o s\u00f3 torna o endpoint mais flex\u00edvel, mas tamb\u00e9m permite que os usu\u00e1rios obtenham os dados de que precisam de maneira mais r\u00e1pida e conveniente. Isso contribui para uma melhor experi\u00eancia do usu\u00e1rio e otimiza a intera\u00e7\u00e3o com o banco de dados.

O c\u00f3digo a seguir ilustra como o endpoint de listagem \u00e9 definido utilizando a Query:

fast_zero/routers/todos.py
from fastapi import APIRouter, Depends, Query\nfrom sqlalchemy import select\n# ...\nfrom fast_zero.schemas import TodoList, TodoPublic, TodoSchema\n\n# ...\n\n@router.get('/', response_model=TodoList)\ndef list_todos(  # noqa (1)\n    session: Session,\n    user: CurrentUser,\n    title: str = Query(None),\n    description: str = Query(None),\n    state: str = Query(None),\n    offset: int = Query(None),\n    limit: int = Query(None),\n):\n    query = select(Todo).where(Todo.user_id == user.id)\n\n    if title:\n        query = query.filter(Todo.title.contains(title))\n\n    if description:\n        query = query.filter(Todo.description.contains(description))\n\n    if state:\n        query = query.filter(Todo.state == state)\n\n    todos = session.scalars(query.offset(offset).limit(limit)).all()\n\n    return {'todos': todos}\n
  1. o c\u00f3digo noqa significa que queremos ignorar essa defini\u00e7\u00e3o durante a an\u00e1lise est\u00e1tica. N\u00e3o \u00e9 considerada uma boa pr\u00e1tica de c\u00f3digo ter uma fun\u00e7\u00e3o que recebe mais de 5 argumentos pela regra R0913 do pylint. Nesse caso, por\u00e9m, temos uma particularidade do FastAPI que recebe cada par\u00e2metro de url por um argumento de fun\u00e7\u00e3o. Como queremos ignorar a checagem somente nessa defini\u00e7\u00e3o, usamos o coment\u00e1rio # noqa

Essa abordagem equilibra a flexibilidade e a efici\u00eancia, tornando o endpoint capaz de atender a uma variedade de necessidades de neg\u00f3cio. Utilizando os recursos do FastAPI, conseguimos implementar uma solu\u00e7\u00e3o robusta e f\u00e1cil de manter, que ser\u00e1 testada posteriormente para garantir sua funcionalidade e integridade.

No c\u00f3digo acima, estamos utilizando filtros do SQLAlchemy, uma biblioteca ORM (Object-Relational Mapping) do Python, para adicionar condi\u00e7\u00f5es \u00e0 nossa consulta. Esses filtros correspondem aos par\u00e2metros que o usu\u00e1rio pode passar na URL.

Essas condi\u00e7\u00f5es s\u00e3o traduzidas em cl\u00e1usulas SQL pelo SQLAlchemy, permitindo que o banco de dados filtre os resultados de acordo com os crit\u00e9rios especificados pelo usu\u00e1rio. Essa integra\u00e7\u00e3o entre FastAPI e SQLAlchemy torna o processo de filtragem eficiente e a codifica\u00e7\u00e3o mais expressiva e clara.

"},{"location":"09/#criando-uma-factory-para-simplificar-os-testes","title":"Criando uma factory para simplificar os testes","text":"

Criar uma factory para o endpoint facilitaria os testes por diversas raz\u00f5es, especialmente quando se trata de testar o nosso endpoint de listagem que usa m\u00faltiplas queries. Primeiro, a factory ajuda a encapsular a l\u00f3gica de cria\u00e7\u00e3o dos objetos necess\u00e1rios para o teste, como no caso dos objetos Todo. Isso significa que voc\u00ea pode criar objetos consistentes e bem-formados sem ter que repetir o mesmo c\u00f3digo em v\u00e1rios testes.

Com a complexidade das queries que nosso endpoint permite, precisamos cobrir todos os usos poss\u00edveis dessas queries. A factory vai nos ajudar a criar muitos casos de testes de forma pr\u00e1tica e eficiente, j\u00e1 que podemos gerar diferentes combina\u00e7\u00f5es de t\u00edtulos, descri\u00e7\u00f5es, estados, entre outros atributos, simulando diversas situa\u00e7\u00f5es de uso.

Al\u00e9m disso, ao utilizar bibliotecas como o factory, \u00e9 poss\u00edvel gerar dados aleat\u00f3rios e v\u00e1lidos, o que pode ajudar a garantir que os testes sejam abrangentes e testem o endpoint em uma variedade de condi\u00e7\u00f5es. Ao simplificar o processo de configura\u00e7\u00e3o dos testes, voc\u00ea pode economizar tempo e esfor\u00e7o, permitindo que a equipe se concentre mais na l\u00f3gica do teste.

tests/test_todos.py
import factory.fuzzy\n\nfrom fast_zero.models import Todo, TodoState\n\n# ...\n\nclass TodoFactory(factory.Factory):\n    class Meta:\n        model = Todo\n\n    title = factory.Faker('text')\n    description = factory.Faker('text')\n    state = factory.fuzzy.FuzzyChoice(TodoState)\n    user_id = 1\n

A fixture acima pode ser usada em diversos testes, reduzindo a duplica\u00e7\u00e3o de c\u00f3digo e melhorando a manuten\u00e7\u00e3o. Por exemplo, em um teste que precisa criar v\u00e1rios objetos Todo, voc\u00ea pode simplesmente usar a TodoFactory para criar esses objetos com uma \u00fanica linha de c\u00f3digo. A factory j\u00e1 cont\u00e9m a l\u00f3gica necess\u00e1ria para criar um objeto v\u00e1lido, e voc\u00ea pode facilmente sobrescrever qualquer um dos atributos, se necess\u00e1rio, para o caso de teste espec\u00edfico.

A utiliza\u00e7\u00e3o de f\u00e1bricas tamb\u00e9m promove uma melhor separa\u00e7\u00e3o entre a l\u00f3gica de cria\u00e7\u00e3o do objeto e a l\u00f3gica do teste, tornando os testes mais leg\u00edveis e f\u00e1ceis de seguir. Com a TodoFactory, somos capazes de simular e testar diversos cen\u00e1rios de busca e filtragem, garantindo que nosso endpoint de listagem funcione corretamente em todas as situa\u00e7\u00f5es poss\u00edveis, aumentando assim a robustez e confiabilidade de nosso sistema.

"},{"location":"09/#testes-para-esse-endpoint","title":"Testes para esse endpoint","text":"

Ao trabalhar com o endpoint de listagem de tarefas, temos v\u00e1rias varia\u00e7\u00f5es de query strings que precisam ser testadas. Cada uma dessas varia\u00e7\u00f5es representa um caso de uso diferente, e queremos garantir que o sistema funcione corretamente em todos eles. Separaremos os testes em pequenos blocos e explicar cada um deles.

"},{"location":"09/#testando-a-listagem-de-todos","title":"Testando a Listagem de Todos","text":"

Primeiro, criaremos um teste b\u00e1sico que verifica se o endpoint est\u00e1 listando todos os objetos Todo.

tests/test_todos.py
def test_list_todos_should_return_5_todos(session, client, user, token):\n    expected_todos = 5\n    session.bulk_save_objects(TodoFactory.create_batch(5, user_id=user.id))\n    session.commit()\n\n    response = client.get(\n        '/todos/',\n        headers={'Authorization': f'Bearer {token}'},\n    )\n\n    assert len(response.json()['todos']) == expected_todos\n

Este teste valida que todos os 5 objetos Todo s\u00e3o retornados pelo endpoint.

"},{"location":"09/#testando-a-paginacao","title":"Testando a Pagina\u00e7\u00e3o","text":"

Em seguida, testaremos a pagina\u00e7\u00e3o para garantir que o offset e o limite estejam funcionando corretamente.

tests/test_todos.py
def test_list_todos_pagination_should_return_2_todos(\n    session, user, client, token\n):\n    expected_todos = 2\n    session.bulk_save_objects(TodoFactory.create_batch(5, user_id=user.id))\n    session.commit()\n\n    response = client.get(\n        '/todos/?offset=1&limit=2',\n        headers={'Authorization': f'Bearer {token}'},\n    )\n\n    assert len(response.json()['todos']) == expected_todos\n

Este teste verifica que, quando aplicado o offset de 1 e o limite de 2, apenas 2 objetos Todo s\u00e3o retornados.

"},{"location":"09/#testando-o-filtro-por-titulo","title":"Testando o Filtro por T\u00edtulo","text":"

Tamb\u00e9m queremos verificar se a filtragem por t\u00edtulo est\u00e1 funcionando conforme esperado.

tests/test_todos.py
def test_list_todos_filter_title_should_return_5_todos(\n    session, user, client, token\n):\n    expected_todos = 5\n    session.bulk_save_objects(\n        TodoFactory.create_batch(5, user_id=user.id, title='Test todo 1')\n    )\n    session.commit()\n\n    response = client.get(\n        '/todos/?title=Test todo 1',\n        headers={'Authorization': f'Bearer {token}'},\n    )\n\n    assert len(response.json()['todos']) == expected_todos\n

Este teste garante que quando o filtro de t\u00edtulo \u00e9 aplicado, apenas as tarefas com o t\u00edtulo correspondente s\u00e3o retornadas.

"},{"location":"09/#testando-o-filtro-por-descricao","title":"Testando o Filtro por Descri\u00e7\u00e3o","text":"

Da mesma forma, queremos testar o filtro de descri\u00e7\u00e3o.

tests/test_todos.py
def test_list_todos_filter_description_should_return_5_todos(\n    session, user, client, token\n):\n    expected_todos = 5\n    session.bulk_save_objects(\n        TodoFactory.create_batch(5, user_id=user.id, description='description')\n    )\n    session.commit()\n\n    response = client.get(\n        '/todos/?description=desc',\n        headers={'Authorization': f'Bearer {token}'},\n    )\n\n    assert len(response.json()['todos']) == expected_todos\n

Este teste verifica que, quando filtramos pela descri\u00e7\u00e3o, apenas as tarefas com a descri\u00e7\u00e3o correspondente s\u00e3o retornadas.

"},{"location":"09/#testando-o-filtro-por-estado","title":"Testando o Filtro por Estado","text":"

Finalmente, precisamos testar o filtro de estado.

tests/test_todos.py
def test_list_todos_filter_state_should_return_5_todos(\n    session, user, client, token\n):\n    expected_todos = 5\n    session.bulk_save_objects(\n        TodoFactory.create_batch(5, user_id=user.id, state=TodoState.draft)\n    )\n    session.commit()\n\n    response = client.get(\n        '/todos/?state=draft',\n        headers={'Authorization': f'Bearer {token}'},\n    )\n\n    assert len(response.json()['todos']) == expected_todos\n

Este teste garante que quando filtramos pelo estado, apenas as tarefas com o estado correspondente s\u00e3o retornadas.

"},{"location":"09/#testando-a-combinacao-de-filtros-de-estado-titulo-e-descricao","title":"Testando a Combina\u00e7\u00e3o de Filtros de Estado, T\u00edtulo e Descri\u00e7\u00e3o","text":"

Em nosso conjunto de testes, tamb\u00e9m \u00e9 importante verificar se o endpoint \u00e9 capaz de lidar com m\u00faltiplos par\u00e2metros de consulta simultaneamente. Para isso, criaremos um teste que combine os filtros de estado, t\u00edtulo e descri\u00e7\u00e3o. Isso assegurar\u00e1 que, quando esses par\u00e2metros s\u00e3o usados juntos, o endpoint retornar\u00e1 apenas as tarefas que correspondem a todas essas condi\u00e7\u00f5es.

Este teste \u00e9 vital para garantir que os usu\u00e1rios podem realizar buscas complexas usando v\u00e1rios crit\u00e9rios ao mesmo tempo, e que o endpoint ir\u00e1 retornar os resultados esperados.

A seguir, apresento o c\u00f3digo do teste:

tests/test_todos.py
def test_list_todos_filter_combined_should_return_5_todos(\n    session, user, client, token\n):\n    expected_todos = 5\n    session.bulk_save_objects(\n        TodoFactory.create_batch(\n            5,\n            user_id=user.id,\n            title='Test todo combined',\n            description='combined description',\n            state=TodoState.done,\n        )\n    )\n\n    session.bulk_save_objects(\n        TodoFactory.create_batch(\n            3,\n            user_id=user.id,\n            title='Other title',\n            description='other description',\n            state=TodoState.todo,\n        )\n    )\n    session.commit()\n\n    response = client.get(\n        '/todos/?title=Test todo combined&description=combined&state=done',\n        headers={'Authorization': f'Bearer {token}'},\n    )\n\n    assert len(response.json()['todos']) == expected_todos\n

Com esses testes, cobrimos todas as poss\u00edveis varia\u00e7\u00f5es de query strings para o nosso endpoint, garantindo que ele funciona corretamente em todas essas situa\u00e7\u00f5es. A abordagem modular para escrever esses testes facilita a leitura e a manuten\u00e7\u00e3o, al\u00e9m de permitir uma cobertura de teste abrangente e robusta.

"},{"location":"09/#executando-os-testes","title":"Executando os testes","text":"

\u00c9 importante n\u00e3o esquecermos de executar os testes para ver se tudo corre bem:

task test tests/test_todos.py\n# ...\ntests/test_todos.py::test_create_todo PASSED\ntests/test_todos.py::test_list_todos PASSED\ntests/test_todos.py::test_list_todos_pagination PASSED\ntests/test_todos.py::test_list_todos_filter_title PASSED\ntests/test_todos.py::test_list_todos_filter_description PASSED\ntests/test_todos.py::test_list_todos_filter_state PASSED\ntests/test_todos.py::test_list_todos_filter_combined PASSED\n
"},{"location":"09/#endpoint-de-alteracao","title":"Endpoint de Altera\u00e7\u00e3o","text":"

Para fazer a altera\u00e7\u00e3o de uma tarefa, precisamos de um modelo onde tudo seja opcional, j\u00e1 que poder\u00edamos querer atualizar apenas um ou alguns campos da tarefa. Criaremos o esquema TodoUpdate, no qual todos os campos s\u00e3o opcionais:

fast_zero/schemas.py
class TodoUpdate(BaseModel):\n    title: str | None = None\n    description: str | None = None\n    state: TodoState | None = None\n

Para podermos alterar somente os valores que recebemos no modelo, temos que fazer um dump somente dos valores que recebemos e os atualizar no objeto que pegamos da base de dados:

fast_zero/routers/todos.py
from http import HTTPStatus\nfrom typing import Annotated\n\nfrom fastapi import APIRouter, Depends, HTTPException, Query\n# ...\nfrom fast_zero.schemas import TodoList, TodoPublic, TodoSchema, TodoUpdate\n\n# ...\n\n@router.patch('/{todo_id}', response_model=TodoPublic)\ndef patch_todo(\n    todo_id: int, session: Session, user: CurrentUser, todo: TodoUpdate\n):\n    db_todo = session.scalar(\n        select(Todo).where(Todo.user_id == user.id, Todo.id == todo_id)\n    )\n\n    if not db_todo:\n        raise HTTPException(\n            status_code=HTTPStatus.NOT_FOUND, detail='Task not found.'\n        )\n\n    for key, value in todo.model_dump(exclude_unset=True).items():\n        setattr(db_todo, key, value)\n\n    session.add(db_todo)\n    session.commit()\n    session.refresh(db_todo)\n\n    return db_todo\n

A linha for key, value in todo.model_dump(exclude_unset=True).items(): est\u00e1 iterando atrav\u00e9s de todos os campos definidos na inst\u00e2ncia todo do modelo de atualiza\u00e7\u00e3o. A fun\u00e7\u00e3o model_dump \u00e9 um m\u00e9todo que vem do modelo BaseModel do Pydantic e permite exportar o modelo para um dicion\u00e1rio.

O par\u00e2metro exclude_unset=True \u00e9 importante aqui, pois significa que apenas os campos que foram explicitamente definidos (ou seja, aqueles que foram inclu\u00eddos na solicita\u00e7\u00e3o PATCH) ser\u00e3o inclu\u00eddos no dicion\u00e1rio resultante. Isso permite que voc\u00ea atualize apenas os campos que foram fornecidos na solicita\u00e7\u00e3o, deixando os outros inalterados.

Ap\u00f3s obter a chave e o valor de cada campo definido, a linha setattr(db_todo, key, value) \u00e9 usada para atualizar o objeto db_todo que representa a tarefa no banco de dados. A fun\u00e7\u00e3o setattr \u00e9 uma fun\u00e7\u00e3o embutida do Python que permite definir o valor de um atributo em um objeto. Neste caso, ele est\u00e1 definindo o atributo com o nome igual \u00e0 chave (ou seja, o nome do campo) no objeto db_todo com o valor correspondente.

Dessa forma, garantimos que somente os campos enviados ao schema sejam atualizados no objeto.

"},{"location":"09/#testes-para-o-endpoint-de-alteracao","title":"Testes para o Endpoint de Altera\u00e7\u00e3o","text":"

Os testes aqui incluem o caso de atualiza\u00e7\u00e3o bem-sucedida e o caso de erro quando a tarefa n\u00e3o \u00e9 encontrada:

fast_zero/tests/test_todos.py
from http import HTTPStatus\n\n# ...\n\ndef test_patch_todo_error(client, token):\n    response = client.patch(\n        '/todos/10',\n        json={},\n        headers={'Authorization': f'Bearer {token}'},\n    )\n    assert response.status_code == HTTPStatus.NOT_FOUND\n    assert response.json() == {'detail': 'Task not found.'}\n\n\ndef test_patch_todo(session, client, user, token):\n    todo = TodoFactory(user_id=user.id)\n\n    session.add(todo)\n    session.commit()\n\n    response = client.patch(\n        f'/todos/{todo.id}',\n        json={'title': 'teste!'},\n        headers={'Authorization': f'Bearer {token}'},\n    )\n    assert response.status_code == HTTPStatus.OK\n    assert response.json()['title'] == 'teste!'\n

Agora precisamos executar os testes para ver se est\u00e1 tudo correto:

$ Execu\u00e7\u00e3o no terminal!
task test tests/test_todos.py\n\n# ...\n\ntests/test_todos.py::test_create_todo PASSED\ntests/test_todos.py::test_list_todos PASSED\ntests/test_todos.py::test_list_todos_pagination PASSED\ntests/test_todos.py::test_list_todos_filter_title PASSED\ntests/test_todos.py::test_list_todos_filter_description PASSED\ntests/test_todos.py::test_list_todos_filter_state PASSED\ntests/test_todos.py::test_list_todos_filter_combined PASSED\ntests/test_todos.py::test_patch_todo_error PASSED\ntests/test_todos.py::test_patch_todo PASSED\n

Com tudo funcionando, podemos partir para o nosso endpoint de DELETE.

"},{"location":"09/#endpoint-de-delecao","title":"Endpoint de Dele\u00e7\u00e3o","text":"

A rota para deletar uma tarefa \u00e9 simples e direta. Caso o todo exista, deletaremos ele com a sesion caso n\u00e3o, retornamos 404:

fast_zero/routers/todos.py
from fast_zero.schemas import (\n    Message,\n    TodoList,\n    TodoPublic,\n    TodoSchema,\n    TodoUpdate,\n)\n\n# ...\n\n\n@router.delete('/{todo_id}', response_model=Message)\ndef delete_todo(todo_id: int, session: Session, user: CurrentUser):\n    todo = session.scalar(\n        select(Todo).where(Todo.user_id == user.id, Todo.id == todo_id)\n    )\n\n    if not todo:\n        raise HTTPException(\n            status_code=HTTPStatus.NOT_FOUND, detail='Task not found.'\n        )\n\n    session.delete(todo)\n    session.commit()\n\n    return {'message': 'Task has been deleted successfully.'}\n
"},{"location":"09/#testes-para-o-endpoint-de-delecao","title":"Testes para o Endpoint de Dele\u00e7\u00e3o","text":"

Esses testes verificam tanto a remo\u00e7\u00e3o bem-sucedida quanto o caso de erro quando a tarefa n\u00e3o \u00e9 encontrada:

fast_zero/tests/test_todos.py
def test_delete_todo(session, client, user, token):\n    todo = TodoFactory(user_id=user.id)\n\n    session.add(todo)\n    session.commit()\n\n    response = client.delete(\n        f'/todos/{todo.id}', headers={'Authorization': f'Bearer {token}'}\n    )\n\n    assert response.status_code == HTTPStatus.OK\n    assert response.json() == {\n        'message': 'Task has been deleted successfully.'\n    }\n\n\ndef test_delete_todo_error(client, token):\n    response = client.delete(\n        f'/todos/{10}', headers={'Authorization': f'Bearer {token}'}\n    )\n\n    assert response.status_code == HTTPStatus.NOT_FOUND\n    assert response.json() == {'detail': 'Task not found.'}\n

Por fim, precisamos executar os testes para ver se est\u00e1 tudo correto:

$ Execu\u00e7\u00e3o no terminal!
task test tests/test_todos.py\n\n# ...\n\ntests/test_todos.py::test_create_todo PASSED\ntests/test_todos.py::test_list_todos PASSED\ntests/test_todos.py::test_list_todos_pagination PASSED\ntests/test_todos.py::test_list_todos_filter_title PASSED\ntests/test_todos.py::test_list_todos_filter_description PASSED\ntests/test_todos.py::test_list_todos_filter_state PASSED\ntests/test_todos.py::test_list_todos_filter_combined PASSED\ntests/test_todos.py::test_delete_todo PASSED\ntests/test_todos.py::test_delete_todo_error PASSED\ntests/test_todos.py::test_patch_todo_error PASSED\ntests/test_todos.py::test_patch_todo PASSED\n
"},{"location":"09/#commit","title":"Commit","text":"

Agora que voc\u00ea finalizou a implementa\u00e7\u00e3o desses endpoints, \u00e9 um bom momento para fazer um commit das suas mudan\u00e7as. Para isso, voc\u00ea pode seguir os seguintes passos:

  1. Adicione as mudan\u00e7as para a stage area: git add .
  2. Commit as mudan\u00e7as: git commit -m \"Implementado os endpoints de tarefas\"
"},{"location":"09/#exercicios","title":"Exerc\u00edcios","text":"
  1. Adicione os campos created_at e updated_at na tabela Todo

    • Eles devem ser init=False
    • Deve usar func.now() para cria\u00e7\u00e3o
    • O campo updated_at deve ter onupdate
  2. Criar uma migra\u00e7\u00e3o para que os novos campos sejam versionados e tamb\u00e9m aplicar a migra\u00e7\u00e3o

  3. Adicionar os campos created_at e updated_at no schema de sa\u00edda dos endpoints. Para que esse valores sejam retornados na API.
"},{"location":"09/#conclusao","title":"Conclus\u00e3o","text":"

Nesta aula exploramos os aspectos essenciais para construir uma API completa e funcional para gerenciar tarefas, integrando-se ao sistema de autentica\u00e7\u00e3o que j\u00e1 t\u00ednhamos desenvolvido.

Iniciamos criando a estrutura de banco de dados para as tarefas, incluindo tabelas e migra\u00e7\u00f5es, e em seguida definimos os schemas necess\u00e1rios. A partir da\u00ed, trabalhamos na cria\u00e7\u00e3o dos endpoints para as opera\u00e7\u00f5es CRUD: cria\u00e7\u00e3o, leitura (listagem com filtragem), atualiza\u00e7\u00e3o (edi\u00e7\u00e3o) e exclus\u00e3o (dele\u00e7\u00e3o).

Em cada est\u00e1gio, focamos na qualidade e na robustez, utilizando testes rigorosos para assegurar que os endpoints se comportassem conforme esperado. Exploramos tamb\u00e9m t\u00e9cnicas espec\u00edficas como atualiza\u00e7\u00e3o parcial e filtragem avan\u00e7ada, tornando a API flex\u00edvel e poderosa.

O resultado foi um sistema integrado de gerenciamento de tarefas, ou um \"todo list\", ligado aos usu\u00e1rios e \u00e0 autentica\u00e7\u00e3o que j\u00e1 hav\u00edamos implementado. Esta aula refor\u00e7ou a import\u00e2ncia de um design cuidadoso e uma implementa\u00e7\u00e3o criteriosa, ilustrando como a FastAPI pode ser usada para criar APIs eficientes e profissionais.

Agora que a nossa aplica\u00e7\u00e3o est\u00e1 crescendo e ganhando mais funcionalidades, na pr\u00f3xima aula, mergulharemos no mundo da dockeriza\u00e7\u00e3o. Aprenderemos a colocar a nossa aplica\u00e7\u00e3o dentro de um container Docker, facilitando o deploy e o escalonamento. Este \u00e9 um passo vital no desenvolvimento moderno de aplica\u00e7\u00f5es e estou ansioso para gui\u00e1-lo atrav\u00e9s dele. At\u00e9 l\u00e1!

Agora que a aula acabou, \u00e9 um bom momento para voc\u00ea relembrar alguns conceitos e fixar melhor o conte\u00fado respondendo ao question\u00e1rio referente a ela.

Quiz

"},{"location":"10/","title":"Dockerizando a nossa aplica\u00e7\u00e3o e introduzindo o PostgreSQL","text":""},{"location":"10/#dockerizando-a-nossa-aplicacao-e-introduzindo-o-postgresql","title":"Dockerizando a nossa aplica\u00e7\u00e3o e introduzindo o PostgreSQL","text":"

Objetivos da aula:

Caso prefira ver a aula em v\u00eddeo

Esse aula ainda n\u00e3o est\u00e1 dispon\u00edvel em formato de v\u00eddeo, somente em texto ou live!

Aula Slides C\u00f3digo Quiz

Ap\u00f3s a implementa\u00e7\u00e3o do nosso gerenciador de tarefas na aula anterior, temos uma primeira vers\u00e3o est\u00e1vel da nossa aplica\u00e7\u00e3o. Nesta aula, al\u00e9m de aprendermos a \"dockerizar\" nossa aplica\u00e7\u00e3o FastAPI, tamb\u00e9m abordaremos a migra\u00e7\u00e3o do banco de dados SQLite para o PostgreSQL.

"},{"location":"10/#o-docker-e-a-nossa-aplicacao","title":"O Docker e a nossa aplica\u00e7\u00e3o","text":"

Docker \u00e9 uma plataforma aberta que permite automatizar o processo de implanta\u00e7\u00e3o, escalonamento e opera\u00e7\u00e3o de aplica\u00e7\u00f5es dentro de cont\u00eaineres. Ele serve para \"empacotar\" uma aplica\u00e7\u00e3o e suas depend\u00eancias em um cont\u00eainer virtual que pode ser executado em qualquer sistema operacional que suporte Docker. Isso facilita a implanta\u00e7\u00e3o, o desenvolvimento e o compartilhamento de aplica\u00e7\u00f5es, al\u00e9m de proporcionar um ambiente isolado e consistente.

Caso n\u00e3o tenha o docker instalado na sua m\u00e1quina

A instala\u00e7\u00e3o do Docker varia entre sistemas operacionais. Por esse motivo, acredito que n\u00e3o cabe cobrir a instala\u00e7\u00e3o do docker nesse material.

WindowsLinuxMacOS X

A instala\u00e7\u00e3o no windows varia com a forma em que voc\u00ea administra o seu sistema. Ela pode se basear em WSL2 ou no Hyper-V.

Os passos para ambos os tipos de instala\u00e7\u00e3o podem ser encontrados na documenta\u00e7\u00e3o oficial do docker: link.

A instala\u00e7\u00e3o no linux variar\u00e1 de acordo com a sua distribui\u00e7\u00e3o. As distribui\u00e7\u00f5es mais tradicionais (baseadas em Debian e RHEL podem ser encontradas na documenta\u00e7\u00e3o oficial do docker: link.

Outras distribui\u00e7\u00f5es devem ter o pacote do docker dispon\u00edvel em seus reposit\u00f3rios. Como distro baseadas em Archlinux.

A instala\u00e7\u00e3o no MacOS, depender\u00e1 da arquitetura do seu computador. Se voc\u00ea usa Intel ou Silicon.

Os passos para ambos os tipos de instala\u00e7\u00e3o podem ser encontrados na documenta\u00e7\u00e3o oficial do docker: link.

"},{"location":"10/#criando-nosso-dockerfile","title":"Criando nosso Dockerfile","text":"

Para criar um container Docker, escrevemos uma lista de passos de como construir o ambiente para execu\u00e7\u00e3o da nossa aplica\u00e7\u00e3o em um arquivo chamado Dockerfile. Ele define o ambiente de execu\u00e7\u00e3o, os comandos necess\u00e1rios para preparar o ambiente e o comando a ser executado quando um cont\u00eainer \u00e9 iniciado a partir da imagem.

Uma das coisas interessantes sobre Docker \u00e9 que existe um Hub de containers prontos onde a comunidade hospeda imagens \"prontas\", que podemos usar como ponto de partida. Por exemplo, a comunidade de python mant\u00e9m um grupo de imagens com o ambiente python pronto para uso. Podemos partir dessa imagem com o python j\u00e1 instalado adicionar os passos para que nossa aplica\u00e7\u00e3o seja executada.

Aqui est\u00e1 um exemplo de Dockerfile para executar nossa aplica\u00e7\u00e3o:

Vers\u00e3o 3.11Vers\u00e3o 3.12
FROM python:3.11-slim\nENV POETRY_VIRTUALENVS_CREATE=false\n\nWORKDIR app/\nCOPY . .\n\nRUN pip install poetry\n\nRUN poetry config installer.max-workers 10\nRUN poetry install --no-interaction --no-ansi\n\nEXPOSE 8000\nCMD poetry run uvicorn --host 0.0.0.0 fast_zero.app:app\n

Aqui est\u00e1 o que cada linha faz:

  1. FROM python:3.11-slim: define a imagem base para nosso cont\u00eainer. Estamos usando a vers\u00e3o slim da imagem do Python 3.11, que tem tudo que precisamos para rodar nossa aplica\u00e7\u00e3o.
  2. ENV POETRY_VIRTUALENVS_CREATE=false: define uma vari\u00e1vel de ambiente que diz ao Poetry para n\u00e3o criar um ambiente virtual. (O container j\u00e1 \u00e9 um ambiente isolado)
  3. RUN pip install poetry: instala o Poetry, nosso gerenciador de pacotes.
  4. WORKDIR app/: define o diret\u00f3rio em que executaremos os comandos a seguir.
  5. COPY . .: copia todos os arquivos do diret\u00f3rio atual para o cont\u00eainer.
  6. RUN poetry config installer.max-workers 10: configura o Poetry para usar at\u00e9 10 workers ao instalar pacotes.
  7. RUN poetry install --no-interaction --no-ansi: instala as depend\u00eancias do nosso projeto sem intera\u00e7\u00e3o e sem cores no output.
  8. EXPOSE 8000: informa ao Docker que o cont\u00eainer escutar\u00e1 na porta 8000.
  9. CMD poetry run uvicorn --host 0.0.0.0 fast_zero.app:app: define o comando que ser\u00e1 executado quando o cont\u00eainer for iniciado.
FROM python:3.12-slim\nENV POETRY_VIRTUALENVS_CREATE=false\n\nWORKDIR app/\nCOPY . .\n\nRUN pip install poetry\n\nRUN poetry config installer.max-workers 10\nRUN poetry install --no-interaction --no-ansi\n\nEXPOSE 8000\nCMD poetry run uvicorn --host 0.0.0.0 fast_zero.app:app\n

Aqui est\u00e1 o que cada linha faz:

  1. FROM python:3.12-slim: define a imagem base para nosso cont\u00eainer. Estamos usando a vers\u00e3o slim da imagem do Python 3.12, que tem tudo que precisamos para rodar nossa aplica\u00e7\u00e3o.
  2. ENV POETRY_VIRTUALENVS_CREATE=false: define uma vari\u00e1vel de ambiente que diz ao Poetry para n\u00e3o criar um ambiente virtual. (O container j\u00e1 \u00e9 um ambiente isolado)
  3. RUN pip install poetry: instala o Poetry, nosso gerenciador de pacotes.
  4. WORKDIR app/: define o diret\u00f3rio em que executaremos os comandos a seguir.
  5. COPY . .: copia todos os arquivos do diret\u00f3rio atual para o cont\u00eainer.
  6. RUN poetry config installer.max-workers 10: configura o Poetry para usar at\u00e9 10 workers ao instalar pacotes.
  7. RUN poetry install --no-interaction --no-ansi: instala as depend\u00eancias do nosso projeto sem intera\u00e7\u00e3o e sem cores no output.
  8. EXPOSE 8000: informa ao Docker que o cont\u00eainer escutar\u00e1 na porta 8000.
  9. CMD poetry run uvicorn --host 0.0.0.0 fast_zero.app:app: define o comando que ser\u00e1 executado quando o cont\u00eainer for iniciado.

Vamos entender melhor esse \u00faltimo comando:

"},{"location":"10/#criando-a-imagem","title":"Criando a imagem","text":"

Para criar uma imagem Docker a partir do Dockerfile, usamos o comando docker build. O comando a seguir cria uma imagem chamada \"fast_zero\":

$ Execu\u00e7\u00e3o no terminal!
docker build -t \"fast_zero\" .\n
Voc\u00ea usa Mac com Silicon?

Pode haver alguma incompatibilidade em alguma biblioteca durante o build. Pois nem todos os pacotes est\u00e3o dispon\u00edveis para Silicon no pypi. Arquitetura aarch64.

Caso encontre algum problema, durante o build voc\u00ea pode especificar a plataforma para amd64. Que \u00e9 a arquitetura em que o curso foi escrito:

$ Execu\u00e7\u00e3o no terminal!
docker build --platform linux/amd64 -t \"fast_zero\" .\n

Mais informa\u00e7\u00f5es nessa issue. Obrigado @K-dash por notificar

Este comando l\u00ea o Dockerfile no diret\u00f3rio atual (indicado pelo .) e cria uma imagem com a tag \"fast_zero\", (indicada pelo -t).

Ent\u00e3o verificaremos se a imagem foi criada com sucesso usando o comando:

$ Execu\u00e7\u00e3o no terminal!
docker images\n

Este comando lista todas as imagens Docker dispon\u00edveis no seu sistema.

"},{"location":"10/#executando-o-container","title":"Executando o container","text":"

Para executar o cont\u00eainer, usamos o comando docker run. Especificamos o nome do cont\u00eainer com a flag --name, indicamos a imagem que queremos executar e a tag que queremos usar <nome_da_imagem>:<tag>. A flag -p serve para mapear a porta do host para a porta do cont\u00eainer <porta_do_host>:<porta_do_cont\u00eainer>. Portanto, teremos o seguinte comando:

$ Execu\u00e7\u00e3o no terminal!
docker run -it --name fastzeroapp -p 8000:8000 fast_zero:latest\n

Este comando iniciar\u00e1 nossa aplica\u00e7\u00e3o em um cont\u00eainer Docker, que estar\u00e1 escutando na porta 8000. Para testar se tudo est\u00e1 funcionando corretamente, voc\u00ea pode acessar http://localhost:8000 em um navegador ou usar um comando como:

$ Execu\u00e7\u00e3o no terminal!
curl http://localhost:8000\n
Caso voc\u00ea fique preso no terminal

Caso voc\u00ea tenha a aplica\u00e7\u00e3o travada no terminal e n\u00e3o consiga sair, voc\u00ea pode teclar Ctrl+C para parar a execu\u00e7\u00e3o do container.

"},{"location":"10/#gerenciando-containers-docker","title":"Gerenciando Containers docker","text":"

Quando voc\u00ea trabalha com Docker, \u00e9 importante saber como gerenciar os cont\u00eaineres. Aqui est\u00e3o algumas opera\u00e7\u00f5es b\u00e1sicas para gerenci\u00e1-los:

  1. Rodar um cont\u00eainer em background: se voc\u00ea deseja executar o cont\u00eainer em segundo plano para que n\u00e3o ocupe o terminal, pode usar a op\u00e7\u00e3o -d:

    $ Execu\u00e7\u00e3o no terminal!
    docker run -d --name fastzeroapp -p 8000:8000 fast_zero:latest\n
  2. Parar um cont\u00eainer: quando voc\u00ea \"para\" um cont\u00eainer, est\u00e1 essencialmente interrompendo a execu\u00e7\u00e3o do processo principal do cont\u00eainer. Isso significa que o cont\u00eainer n\u00e3o est\u00e1 mais ativo, mas ainda existe no sistema, com seus dados associados e configura\u00e7\u00e3o. Isso permite que voc\u00ea reinicie o cont\u00eainer posteriormente, se desejar.

    $ Execu\u00e7\u00e3o no terminal!
    docker stop fastzeroapp\n
  3. Remover um cont\u00eainer: ao \"remover\" um cont\u00eainer, voc\u00ea est\u00e1 excluindo o cont\u00eainer do sistema. Isso significa que todos os dados associados ao cont\u00eainer s\u00e3o apagados. Uma vez que um cont\u00eainer \u00e9 removido, voc\u00ea n\u00e3o pode reinici\u00e1-lo; no entanto, voc\u00ea pode sempre criar um novo cont\u00eainer a partir da mesma imagem.

    $ Execu\u00e7\u00e3o no terminal!
    docker rm fastzeroapp\n

Ambos os comandos (stop e rm) usam o nome do cont\u00eainer que definimos anteriormente com a flag --name. \u00c9 uma boa pr\u00e1tica manter a gest\u00e3o dos seus cont\u00eaineres, principalmente durante o desenvolvimento, para evitar um uso excessivo de recursos ou conflitos de nomes e portas.

"},{"location":"10/#introduzindo-o-postgresql","title":"Introduzindo o postgreSQL","text":"

O PostgreSQL \u00e9 um Sistema de Gerenciamento de Banco de Dados Objeto-Relacional (ORDBMS) poderoso e de c\u00f3digo aberto. Ele \u00e9 amplamente utilizado em produ\u00e7\u00e3o em muitos projetos devido \u00e0 sua robustez, escalabilidade e conjunto de recursos extensos.

Mudar para um banco de dados como PostgreSQL tem v\u00e1rios benef\u00edcios:

Al\u00e9m disso, SQLite tem algumas limita\u00e7\u00f5es que podem torn\u00e1-lo inadequado para produ\u00e7\u00e3o em alguns casos. Por exemplo, ele n\u00e3o suporta alta concorr\u00eancia e pode ter problemas de performance com grandes volumes de dados.

Nota

Embora para o escopo da nossa aplica\u00e7\u00e3o e os objetivos de aprendizado o SQLite pudesse ser suficiente, \u00e9 sempre bom nos prepararmos para cen\u00e1rios de produ\u00e7\u00e3o real. A ado\u00e7\u00e3o de PostgreSQL nos d\u00e1 uma pr\u00e9via das pr\u00e1ticas do mundo real e garante que nossa aplica\u00e7\u00e3o possa escalar sem grandes modifica\u00e7\u00f5es de infraestrutura.

"},{"location":"10/#como-executar-o-postgres","title":"Como executar o postgres?","text":"

Embora o PostgreSQL seja poderoso, sua instala\u00e7\u00e3o direta em uma m\u00e1quina real pode ser desafiadora e pode resultar em configura\u00e7\u00f5es diferentes entre os ambientes de desenvolvimento. Felizmente, podemos utilizar o Docker para resolver esse problema. No Docker Hub, est\u00e3o dispon\u00edveis imagens pr\u00e9-constru\u00eddas do PostgreSQL, permitindo-nos executar o PostgreSQL com um \u00fanico comando. Confira a imagem oficial do PostgreSQL.

Para executar um cont\u00eainer do PostgreSQL, use o seguinte comando:

$ Execu\u00e7\u00e3o no terminal!
docker run -d \\\n    --name app_database \\\n    -e POSTGRES_USER=app_user \\\n    -e POSTGRES_DB=app_db \\\n    -e POSTGRES_PASSWORD=app_password \\\n    -p 5432:5432 \\\n    postgres\n
"},{"location":"10/#explicando-as-flags-e-configuracoes","title":"Explicando as Flags e Configura\u00e7\u00f5es","text":"

Esta flag \u00e9 usada para definir vari\u00e1veis de ambiente no cont\u00eainer. No contexto do PostgreSQL, essas vari\u00e1veis s\u00e3o essenciais. Elas configuram o nome de usu\u00e1rio, nome do banco de dados, e senha durante a primeira execu\u00e7\u00e3o do cont\u00eainer. Sem elas, o PostgreSQL pode n\u00e3o iniciar da forma esperada. \u00c9 uma forma pr\u00e1tica de configurar o PostgreSQL sem interagir manualmente ou criar arquivos de configura\u00e7\u00e3o.

O PostgreSQL, por padr\u00e3o, escuta por conex\u00f5es na porta 5432. Mapeando esta porta do cont\u00eainer para a mesma porta no host (usando -p), fazemos com que o PostgreSQL seja acess\u00edvel nesta porta na m\u00e1quina anfitri\u00e3, permitindo que outras aplica\u00e7\u00f5es se conectem a ele.

Sobre as vari\u00e1veis

Os valores acima (app_user, app_db, e app_password) s\u00e3o padr\u00f5es gen\u00e9ricos para facilitar a inicializa\u00e7\u00e3o do PostgreSQL em um ambiente de desenvolvimento. No entanto, \u00e9 altamente recomend\u00e1vel que voc\u00ea altere esses valores, especialmente app_password, para garantir a seguran\u00e7a do seu banco de dados.

"},{"location":"10/#volumes-e-persistencia-de-dados","title":"Volumes e Persist\u00eancia de Dados","text":"

Para garantir a persist\u00eancia dos dados entre execu\u00e7\u00f5es do cont\u00eainer, utilizamos volumes. Um volume mapeia um diret\u00f3rio do sistema host para um diret\u00f3rio no cont\u00eainer. Isso \u00e9 crucial para bancos de dados, pois sem um volume, ao remover o cont\u00eainer, todos os dados armazenados dentro dele se perderiam.

No PostgreSQL, o diret\u00f3rio padr\u00e3o para armazenamento de dados \u00e9 /var/lib/postgresql/data. Mapeamos esse diret\u00f3rio para um volume (neste caso \"pgdata\") em nossa m\u00e1quina host para garantir a persist\u00eancia dos dados:

$ Execu\u00e7\u00e3o no terminal!
docker run -d \\\n    --name app_database \\\n    -e POSTGRES_USER=app_user \\\n    -e POSTGRES_DB=app_db \\\n    -e POSTGRES_PASSWORD=app_password \\\n    -v pgdata:/var/lib/postgresql/data \\\n    -p 5432:5432 \\\n    postgres\n

O par\u00e2metro do volume \u00e9 passado ao cont\u00eainer usando o par\u00e2metro -v Dessa forma, os dados do banco continuar\u00e3o existindo, mesmo que o cont\u00eainer seja reiniciado ou removido.

"},{"location":"10/#adicionando-o-suporte-ao-postgresql-na-nossa-aplicacao","title":"Adicionando o suporte ao PostgreSQL na nossa aplica\u00e7\u00e3o","text":"

Para que o SQLAlchemy suporte o PostgreSQL, precisamos instalar uma depend\u00eancia chamada psycopg. Este \u00e9 o adaptador PostgreSQL para Python e \u00e9 crucial para fazer a comunica\u00e7\u00e3o.

Para instalar essa depend\u00eancia, utilize o seguinte comando:

$ Execu\u00e7\u00e3o no terminal!
poetry add \"psycopg[binary]\"\n

Uma das vantagens do SQLAlchemy enquanto ORM \u00e9 a flexibilidade. Com apenas algumas altera\u00e7\u00f5es m\u00ednimas, como a atualiza\u00e7\u00e3o da string de conex\u00e3o, podemos facilmente transicionar para um banco de dados diferente. Assim, ap\u00f3s ajustar o arquivo .env com a string de conex\u00e3o do PostgreSQL, a aplica\u00e7\u00e3o dever\u00e1 operar normalmente, mas desta vez utilizando o PostgreSQL.

Para ajustar a conex\u00e3o com o PostgreSQL, modifique seu arquivo .env para incluir a seguinte string de conex\u00e3o:

.env
DATABASE_URL=\"postgresql+psycopg://app_user:app_password@localhost:5432/app_db\"\n

Caso tenha alterado as vari\u00e1veis de ambiente do cont\u00eainer

Se voc\u00ea alterou app_user, app_password ou app_db ao inicializar o cont\u00eainer PostgreSQL, garanta que esses valores sejam refletidos na string de conex\u00e3o acima. A palavra localhost indica que o banco de dados PostgreSQL est\u00e1 sendo executado na mesma m\u00e1quina que sua aplica\u00e7\u00e3o. Se o banco de dados estiver em uma m\u00e1quina diferente, substitua localhost pelo endere\u00e7o IP correspondente e, se necess\u00e1rio, ajuste a porta 5432.

Para que a instala\u00e7\u00e3o do psycopg esteja na imagem docker, precisamos fazer um novo build. Para que a nova vers\u00e3o do pyproject.toml seja copiada e os novos pacotes sejam instalados:

$ Execu\u00e7\u00e3o no terminal!
docker rm fastzeroapp #(1)!\ndocker build -t \"fast_zero\" #(2)!\ndocker run -it --name fastzeroapp -p 8000:8000 fast_zero:latest #(3)!\n
  1. Remove a vers\u00e3o antiga
  2. Refaz o build
  3. Executa novamente
"},{"location":"10/#executando-as-migracoes","title":"Executando as migra\u00e7\u00f5es","text":"

Migra\u00e7\u00f5es s\u00e3o como vers\u00f5es para seu banco de dados, permitindo que voc\u00ea atualize sua estrutura de forma ordenada e controlada. Sempre que mudamos de banco de dados, ou at\u00e9 mesmo quando alteramos sua estrutura, as migra\u00e7\u00f5es precisam ser executadas para garantir que a base de dados esteja em sincronia com nosso c\u00f3digo.

No contexto de cont\u00eaineres, rodar as migra\u00e7\u00f5es se torna ainda mais simples. Quando mudamos de banco de dados, como \u00e9 o caso de termos sa\u00eddo de um SQLite (por exemplo) para um PostgreSQL, as migra\u00e7\u00f5es s\u00e3o essenciais. O motivo \u00e9 simples: o novo banco de dados n\u00e3o ter\u00e1 a estrutura e os dados do antigo, a menos que migremos. As migra\u00e7\u00f5es ir\u00e3o garantir que o novo banco de dados tenha a mesma estrutura e rela\u00e7\u00f5es que o anterior.

Antes de executar o pr\u00f3ximo comando

Assegure-se de que ambos os cont\u00eaineres, tanto da aplica\u00e7\u00e3o quanto do banco de dados, estejam ativos. O cont\u00eainer do banco de dados deve estar rodando para que a aplica\u00e7\u00e3o possa se conectar a ele.

Assegure-se de que o cont\u00eainer da aplica\u00e7\u00e3o esteja ativo. Estamos usando a flag --network=host para que o cont\u00eainer use a rede do host. Isso pode ser essencial para evitar problemas de conex\u00e3o, j\u00e1 que n\u00e3o podemos prever como est\u00e1 configurada a rede do computador onde este comando ser\u00e1 executado.

execu\u00e7\u00e3o no terminal
docker run -d --network=host --name fastzeroapp -p 8000:8000 fast_zero:latest\n

Para aplicar migra\u00e7\u00f5es em um ambiente com cont\u00eaineres, frequentemente temos comandos espec\u00edficos associados ao servi\u00e7o. Vejamos como executar migra\u00e7\u00f5es usando o Docker:

$ Execu\u00e7\u00e3o no terminal!
docker exec -it fastzeroapp poetry run alembic upgrade head\n

O comando docker exec \u00e9 usado para invocar um comando espec\u00edfico dentro de um cont\u00eainer em execu\u00e7\u00e3o. A op\u00e7\u00e3o -it \u00e9 uma combina\u00e7\u00e3o de -i (interativo) e -t (pseudo-TTY), que juntas garantem um terminal interativo, permitindo a comunica\u00e7\u00e3o direta com o cont\u00eainer.

Ap\u00f3s executar as migra\u00e7\u00f5es, voc\u00ea pode verificar a cria\u00e7\u00e3o das tabelas utilizando um sistema de gerenciamento de banco de dados. A seguir, apresentamos um exemplo com o Beekeeper Studio:

Lembre-se: Embora as tabelas estejam agora criadas e estruturadas, o banco de dados ainda n\u00e3o cont\u00e9m os dados anteriormente presentes no SQLite ou em qualquer outro banco que voc\u00ea estivesse utilizando antes.

"},{"location":"10/#simplificando-nosso-fluxo-com-docker-compose","title":"Simplificando nosso fluxo com docker-compose","text":"

Docker Compose \u00e9 uma ferramenta que permite definir e gerenciar aplicativos multi-cont\u00eainer com Docker. \u00c9 como se voc\u00ea tivesse um maestro conduzindo uma orquestra: o maestro (ou Docker Compose) garante que todos os m\u00fasicos (ou cont\u00eaineres) toquem em harmonia. Definimos nossa aplica\u00e7\u00e3o e servi\u00e7os relacionados, como o PostgreSQL, em um arquivo compose.yaml e os gerenciamos juntos atrav\u00e9s de comandos simplificados.

Ao adotar o Docker Compose, facilitamos o desenvolvimento e a execu\u00e7\u00e3o da nossa aplica\u00e7\u00e3o com seus servi\u00e7os dependentes utilizando um \u00fanico comando.

"},{"location":"10/#criacao-do-composeyaml","title":"Cria\u00e7\u00e3o do compose.yaml","text":"compose.yaml
services:\n  fastzero_database:\n    image: postgres\n    volumes:\n      - pgdata:/var/lib/postgresql/data\n    environment:\n      POSTGRES_USER: app_user\n      POSTGRES_DB: app_db\n      POSTGRES_PASSWORD: app_password\n    ports:\n      - \"5432:5432\"\n\n  fastzero_app:\n    image: fastzero_app\n    build: .\n    ports:\n      - \"8000:8000\"\n    depends_on:\n      - fastzero_database\n    environment:\n      DATABASE_URL: postgresql+psycopg://app_user:app_password@fastzero_database:5432/app_db\n\nvolumes:\n  pgdata:\n

Explica\u00e7\u00e3o linha a linha:

  1. services:: define os servi\u00e7os (cont\u00eaineres) que ser\u00e3o gerenciados.

  2. fastzero_database:: define nosso servi\u00e7o de banco de dados PostgreSQL.

  3. image: postgres: usa a imagem oficial do PostgreSQL.

  4. volumes:: mapeia volumes para persist\u00eancia de dados.

  5. pgdata:/var/lib/postgresql/data: cria ou usa um volume chamado \"pgdata\" e o mapeia para o diret\u00f3rio /var/lib/postgresql/data no cont\u00eainer.

  6. environment:: define vari\u00e1veis de ambiente para o servi\u00e7o.

  7. fastzero_app:: define o servi\u00e7o para nossa aplica\u00e7\u00e3o.

  8. image: fastzero_app: usa a imagem Docker da nossa aplica\u00e7\u00e3o.

  9. build:: instru\u00e7\u00f5es para construir a imagem se n\u00e3o estiver dispon\u00edvel, procura pelo Dockerfile em ..

  10. ports:: mapeia portas do cont\u00eainer para o host.

  11. \"8000:8000\": mapeia a porta 8000 do cont\u00eainer para a porta 8000 do host.

  12. depends_on:: especifica que fastzero_app depende de fastzero_database. Isto garante que o banco de dados seja iniciado antes da aplica\u00e7\u00e3o.

  13. DATABASE_URL: ...: \u00e9 uma vari\u00e1vel de ambiente que nossa aplica\u00e7\u00e3o usar\u00e1 para se conectar ao banco de dados. Aqui, ele se conecta ao servi\u00e7o fastzero_database que definimos anteriormente.

  14. volumes: (n\u00edvel superior): define volumes que podem ser usados pelos servi\u00e7os.

  15. pgdata:: define um volume chamado \"pgdata\". Este volume \u00e9 usado para persistir os dados do PostgreSQL entre as execu\u00e7\u00f5es do cont\u00eainer.

Sobre o docker-compose

Para usar o Docker Compose, voc\u00ea precisa t\u00ea-lo instalado em seu sistema. Ele n\u00e3o est\u00e1 inclu\u00eddo na instala\u00e7\u00e3o padr\u00e3o do Docker, ent\u00e3o lembre-se de instal\u00e1-lo separadamente!

O guia oficial de instala\u00e7\u00e3o pode ser encontrado aqui

Com este arquivo compose.yaml, voc\u00ea pode iniciar ambos os servi\u00e7os (aplica\u00e7\u00e3o e banco de dados) simultaneamente usando:

docker-compose up\n

Para parar os servi\u00e7os e manter os dados seguros nos volumes definidos, use:

docker-compose down\n

Esses comandos simplificam o fluxo de trabalho e garantem que os servi\u00e7os iniciem corretamente e se comuniquem conforme o esperado.

Execu\u00e7\u00e3o em modo desanexado

Voc\u00ea pode iniciar os servi\u00e7os em segundo plano com a flag -d usando docker-compose up -d. Isso permite que os cont\u00eaineres rodem em segundo plano, liberando o terminal para outras tarefas.

"},{"location":"10/#rodando-as-migracoes-de-forma-automatica","title":"Rodando as migra\u00e7\u00f5es de forma autom\u00e1tica","text":"

Automatizar as migra\u00e7\u00f5es do banco de dados \u00e9 uma pr\u00e1tica recomendada para garantir que sua aplica\u00e7\u00e3o esteja sempre sincronizada com o estado mais atual do seu esquema de banco de dados. \u00c9 como preparar todos os ingredientes antes de come\u00e7ar a cozinhar: voc\u00ea garante que tudo o que \u00e9 necess\u00e1rio est\u00e1 pronto para ser usado.

Para automatizar as migra\u00e7\u00f5es em nossos cont\u00eaineres Docker, utilizamos um entrypoint. O entrypoint define o comando que ser\u00e1 executado quando o cont\u00eainer iniciar. Em outras palavras, \u00e9 o primeiro ponto de entrada de execu\u00e7\u00e3o do cont\u00eainer.

Por que usar o Entrypoint?

No Docker, o entrypoint permite que voc\u00ea configure um ambiente de cont\u00eainer que ser\u00e1 executado como um execut\u00e1vel. \u00c9 \u00fatil para preparar o ambiente, como realizar migra\u00e7\u00f5es de banco de dados, antes de iniciar a aplica\u00e7\u00e3o propriamente dita. Isso significa que qualquer comando definido no CMD do Dockerfile n\u00e3o ser\u00e1 executado automaticamente se um entrypoint estiver definido. Em vez disso, precisamos incluir explicitamente esse comando no script de entrypoint.

Implementando o Entrypoint

Criamos um script chamado entrypoint.sh que ir\u00e1 preparar nosso ambiente antes de a aplica\u00e7\u00e3o iniciar:

entrypoint.sh
#!/bin/sh\n\n# Executa as migra\u00e7\u00f5es do banco de dados\npoetry run alembic upgrade head\n\n# Inicia a aplica\u00e7\u00e3o\npoetry run uvicorn --host 0.0.0.0 --port 8000 fast_zero.app:app\n

Explica\u00e7\u00e3o Detalhada do Script:

Como Funciona na Pr\u00e1tica?

Quando o cont\u00eainer \u00e9 iniciado, o Docker executa o script de entrypoint, que por sua vez executa as migra\u00e7\u00f5es e s\u00f3 ent\u00e3o inicia a aplica\u00e7\u00e3o. Isso garante que o banco de dados esteja atualizado com as \u00faltimas migra\u00e7\u00f5es antes de qualquer intera\u00e7\u00e3o com a aplica\u00e7\u00e3o.

Visualizando o Processo:

Voc\u00ea pode pensar no entrypoint.sh como o ato de aquecer e verificar todos os instrumentos antes de uma apresenta\u00e7\u00e3o musical. Antes de a m\u00fasica come\u00e7ar, cada instrumento \u00e9 afinado e testado. Da mesma forma, nosso script assegura que o banco de dados est\u00e1 em harmonia com a aplica\u00e7\u00e3o antes de ela come\u00e7ar a receber requisi\u00e7\u00f5es.

Adicionando o Entrypoint ao Docker Compose:

Inclu\u00edmos o entrypoint no nosso servi\u00e7o no arquivo compose.yaml, garantindo que esteja apontando para o script correto:

compose.yaml
  fastzero_app:\n    image: fastzero_app\n    entrypoint: ./entrypoint.sh\n    build: .\n

Reconstruindo e Executando com Novas Configura\u00e7\u00f5es:

Para aplicar as altera\u00e7\u00f5es, reconstru\u00edmos e executamos os servi\u00e7os com a op\u00e7\u00e3o --build:

$ Execu\u00e7\u00e3o no terminal!
docker-compose up --build\n

Observando o Comportamento Esperado:

Quando o cont\u00eainer \u00e9 iniciado, voc\u00ea deve ver as migra\u00e7\u00f5es sendo aplicadas, seguidas pela inicializa\u00e7\u00e3o da aplica\u00e7\u00e3o:

$ Exemplo do resultado no terminal!
fastzero_app-1  | INFO  [alembic.runtime.migration] Context impl PostgresqlImpl.\nfastzero_app-1  | INFO  [alembic.runtime.migration] Will assume transactional DDL.\nfastzero_app-1  | INFO:     Started server process [10]\nfastzero_app-1  | INFO:     Waiting for application startup.\nfastzero_app-1  | INFO:     Application startup complete.\nfastzero_app-1  | INFO:     Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)\n

Este processo garante que as migra\u00e7\u00f5es do banco de dados s\u00e3o realizadas automaticamente, mantendo a base de dados alinhada com a aplica\u00e7\u00e3o e pronta para a\u00e7\u00e3o assim que o servidor Uvicorn entra em cena.

Nota de revis\u00e3o sobre vari\u00e1veis de ambiente

Utilizar vari\u00e1veis de ambiente definidas em um arquivo .env \u00e9 uma pr\u00e1tica recomendada para cen\u00e1rios de produ\u00e7\u00e3o devido \u00e0 seguran\u00e7a que oferece. No entanto, para manter a simplicidade e o foco nas funcionalidades do FastAPI neste curso, optamos por explicitar essas vari\u00e1veis no compose.yaml. Isso \u00e9 particularmente relevante, pois o Docker Compose \u00e9 utilizado apenas para o ambiente de desenvolvimento; no deploy para fly.io, o qual \u00e9 o nosso foco, o compose n\u00e3o ser\u00e1 utilizado em produ\u00e7\u00e3o.

Ainda assim, \u00e9 valioso mencionar como essa configura\u00e7\u00e3o mais segura seria realizada, especialmente para aqueles que planejam utilizar o Docker Compose em produ\u00e7\u00e3o.

Em ambientes de produ\u00e7\u00e3o com Docker Compose, \u00e9 uma boa pr\u00e1tica gerenciar vari\u00e1veis de ambiente sens\u00edveis, como credenciais, por meio de um arquivo .env. Isso previne a exposi\u00e7\u00e3o dessas informa\u00e7\u00f5es diretamente no arquivo compose.yaml, contribuindo para a seguran\u00e7a do projeto.

As vari\u00e1veis de ambiente podem ser definidas em nosso arquivo .env localizado na raiz do projeto:

.env
POSTGRES_USER=app_user\nPOSTGRES_DB=app_db\nPOSTGRES_PASSWORD=app_password\nDATABASE_URL=postgresql+psycopg://app_user:app_password@fastzero_database:5432/app_db\n

Para aplicar essas vari\u00e1veis, referencie o arquivo .env no compose.yaml:

compose.yaml
services:\n  fastzero_database:\n    image: postgres\n    env_file:\n      - .env\n    # Restante da configura\u00e7\u00e3o...\n\n  fastzero_app:\n    build: .\n    env_file:\n      - .env\n    # Restante da configura\u00e7\u00e3o...\n

Adotar essa abordagem evita a exposi\u00e7\u00e3o das vari\u00e1veis de ambiente no arquivo de configura\u00e7\u00e3o. Esta n\u00e3o foi a abordagem padr\u00e3o no curso devido \u00e0 complexidade adicional e \u00e0 inten\u00e7\u00e3o de evitar confus\u00f5es. Dependendo do ambiente estabelecido pela equipe de DevOps/SRE em um projeto real, essa gest\u00e3o pode variar entre vari\u00e1veis de ambiente, arquivos .env ou solu\u00e7\u00f5es mais avan\u00e7adas como Vault.

Se optar por utilizar um arquivo .env com as configura\u00e7\u00f5es do PostgreSQL, configure o Pydantic para ignorar vari\u00e1veis de ambiente que n\u00e3o s\u00e3o necess\u00e1rias, adicionando extra='ignore' a chamada de SettingsConfigDic:

fast_zero/settings.py
from pydantic_settings import BaseSettings, SettingsConfigDict\n\n\nclass Settings(BaseSettings):\n    model_config = SettingsConfigDict(\n        env_file='.env', env_file_encoding='utf-8', extra='ignore'\n    )\n\n    DATABASE_URL: str\n    SECRET_KEY: str\n    ALGORITHM: str\n    ACCESS_TOKEN_EXPIRE_MINUTES: int\n

Com essa configura\u00e7\u00e3o, o Pydantic ir\u00e1 ignorar quaisquer vari\u00e1veis no .env que n\u00e3o sejam explicitamente declaradas na classe Settings, evitando assim conflitos e erros inesperados.

Agradecimentos especiais a @vcwild e @williangl pelas revis\u00f5es valiosas nesta aula que me fizeram criar essa nota.

Boas pr\u00e1ticas de inicializa\u00e7\u00e3o do banco de dados

Como esse \u00e9 um caso pensado em estudo, possivelmente n\u00e3o haver\u00e1 problemas relacionados \u00e0 inicializa\u00e7\u00e3o. Em um ambiente de produ\u00e7\u00e3o, por\u00e9m, n\u00e3o existe a garantia de que o postgres est\u00e1 pronto para uso no momento em que o entrypoint for executado. Seria necess\u00e1rio que, antes da execu\u00e7\u00e3o da migra\u00e7\u00e3o, o container do banco de dados tivesse a inicializa\u00e7\u00e3o finalizada.

Isso \u00e9 feito usando o campo healthcheck do compose.yaml:

compose.yaml
services:\n  fastzero_database:\n  image: postgres\n  volumes:\n    - pgdata:/var/lib/postgresql/data\n  environment:\n      POSTGRES_USER: app_user\n      POSTGRES_DB: app_db\n      POSTGRES_PASSWORD: app_password\n  ports:\n      - \"5432:5432\"\n  healthcheck:\n      test: [\"CMD-SHELL\", \"pg_isready\"]\n      interval: 5s\n      timeout: 5s\n      retries: 10\n

Dessa forma, ele ir\u00e1 executar o comando pg_isready a cada 5 segundos por 10 vezes. pg_isready \u00e9 um utilit\u00e1rio do PostgreSQL que verifica se ele j\u00e1 est\u00e1 operando e pronto para receber conex\u00f5es. Desta forma, a inicializa\u00e7\u00e3o do container s\u00f3 termina quando o postgres estiver ouvindo conex\u00f5es.

Dica do @ayharano nessa issue.

"},{"location":"10/#testes-e-docker","title":"Testes e Docker","text":"

Uma das partes importantes dos testes \u00e9 tentar chegar o mais pr\u00f3ximo poss\u00edvel do ambiente de desenvolvimento. Contudo, nessa aula, introduzimos uma depend\u00eancia que vai al\u00e9m do python, o postgres.

Isso pode tornar o nosso c\u00f3digo mais complicado de testar, por existir um DoC. Um \"componente dependente\" para ser executado. Nesse caso, por\u00e9m, \u00e9 interno ao sqlalchemy. Para usar o psycopg, temos uma depend\u00eancia externa ao python, o banco de dados precisa estar sendo executado, caso contr\u00e1rio os testes falhar\u00e3o.

"},{"location":"10/#executando-testes-com-o-banco-de-dados-em-um-container","title":"Executando testes com o banco de dados em um container","text":"

Os testes contemplam um ciclo de feedback positivo, eles t\u00eam que ser executados de forma r\u00e1pida e eficiente. Adicionar o container do Postgres a nossa aplica\u00e7\u00e3o, torna o processo de testes um pouco mais complexo. Pois existe uma depend\u00eancia ao n\u00edvel de sistema para os testes serem executados.

Come\u00e7aremos com o contraexemplo. Vamos alterar o comportamento da fixture do banco de dados para usar o postgres:

tests/conftest.py
from fast_zero.app import app\nfrom fast_zero.database import get_session\nfrom fast_zero.models import table_registry\nfrom fast_zero.settings import Settings\nfrom fast_zero.security import get_password_hash\nfrom tests.factories import UserFactory\n\n\n@pytest.fixture\ndef session():\n    engine = create_engine(Settings().DATABASE_URL)\n    table_registry.metadata.create_all(engine)\n\n    with Session(engine) as session:\n        yield session\n        session.rollback()\n\n    table_registry.metadata.drop_all(engine)\n

Com essa modifica\u00e7\u00e3o, agora estamos apontando para o PostgreSQL, conforme definido nas configura\u00e7\u00f5es da nossa aplica\u00e7\u00e3o (Settings().DATABASE_URL). A transi\u00e7\u00e3o do SQLite para o PostgreSQL \u00e9 facilitada pela abstra\u00e7\u00e3o fornecida pelo SQLAlchemy, que nos permite mudar de um banco para outro sem problemas.

\u00c9 importante notar que essa flexibilidade se deve ao fato de n\u00e3o termos utilizado recursos espec\u00edficos do PostgreSQL que n\u00e3o s\u00e3o suportados pelo SQLite. Caso contr\u00e1rio, a mudan\u00e7a poderia n\u00e3o ser t\u00e3o direta.

Partindo desse exemplo, para os testes serem executados, o banco de dados precisaria estar de p\u00e9. O que nos cobraria um container em execu\u00e7\u00e3o para os testes poderem rodar.

Por exemplo:

$ Execu\u00e7\u00e3o no terminal!
task test\n

Isso originaria esse erro:

============================= test session starts ==============================\nplatform linux -- Python 3.12.3, pytest-8.2.1, pluggy-1.5.0 -- /home/dunossauro/git/fastapi-do-zero/codigo_das_aulas/10/.venv/bin/python\ncachedir: .pytest_cache\nrootdir: /home/dunossauro/git/fastapi-do-zero/codigo_das_aulas/10\nconfigfile: pyproject.toml\nplugins: anyio-4.4.0, cov-5.0.0, Faker-25.4.0\ncollecting ... collected 28 items\n\ntests/test_app.py::test_root_deve_retornar_ok_e_ola_mundo ERROR\n\n==================================== ERRORS ====================================\n___________ ERROR at setup of test_root_deve_retornar_ok_e_ola_mundo ___________\n\nself = <sqlalchemy.engine.base.Connection object at 0x7ad981fb7380>\nengine = Engine(postgresql+psycopg://app_user:***@localhost:5432/app_db)\nconnection = None, _has_events = None, _allow_revalidate = True\n_allow_autobegin = True\n\n# ...\n        if not rv:\n            assert last_ex\n>           raise last_ex.with_traceback(None)\nE           psycopg.OperationalError: connection failed: connection to server at \"127.0.0.1\", port 5432 failed: Connection refused\nE               Is the server running on that host and accepting TCP/IP connections?\n\n.venv/lib/python3.12/site-packages/psycopg/connection.py:748: OperationalError\n

Obtivemos o erro psycopg.OperationalError: connection failed: connection to server at \"127.0.0.1\", port 5432 failed: Connection refused. Ele diz que ouve uma falha na comunica\u00e7\u00e3o com o nosso host na porta 5432. O endere\u00e7o que colocamos no .env. Para que ele fique acess\u00edvel, temos que iniciar o container antes de executar os testes.

Para isso:

$ Execu\u00e7\u00e3o no terminal!
docker-compose up -d fastzero_database #(1)!\n
  1. Inicia somente o container no banco de dados via docker compose

E em seguida executar os testes:

$ Execu\u00e7\u00e3o no terminal!
task test\n

Agora, sucesso. O resultado \u00e9 exatamente o que esper\u00e1vamos:

All checks passed!\n========== test session starts ==========\nplatform linux -- Python 3.12.3, pytest-8.2.1, pluggy-1.5.0 -- /.../10/.venv/bin/python\ncachedir: .pytest_cache\nrootdir: /home/dunossauro/git/fastapi-do-zero/codigo_das_aulas/10\nconfigfile: pyproject.toml\nplugins: anyio-4.4.0, cov-5.0.0, Faker-25.4.0\ncollected 28 items\n\ntests/test_app.py::test_root_deve_retornar_ok_e_ola_mundo PASSED\ntests/test_auth.py::test_get_token PASSED\ntests/test_auth.py::test_token_expired_after_time PASSED\ntests/test_auth.py::test_token_inexistent_user PASSED\ntests/test_auth.py::test_token_wrong_password PASSED\ntests/test_auth.py::test_refresh_token PASSED\ntests/test_auth.py::test_token_expired_dont_refresh PASSED\ntests/test_db.py::test_create_user PASSED\ntests/test_db.py::test_create_todo PASSED\ntests/test_security.py::test_jwt PASSED\ntests/test_todos.py::test_create_todo PASSED\ntests/test_todos.py::test_list_todos_should_return_5_todos PASSED\ntests/test_todos.py::test_list_todos_pagination_should_return_2_todos PASSED\ntests/test_todos.py::test_list_todos_filter_title_should_return_5_todos PASSED\ntests/test_todos.py::test_list_todos_filter_description_should_return_5_todos PASSED\ntests/test_todos.py::test_list_todos_filter_state_should_return_5_todos PASSED\ntests/test_todos.py::test_list_todos_filter_combined_should_return_5_todos PASSED\ntests/test_todos.py::test_patch_todo_error PASSED\ntests/test_todos.py::test_patch_todo PASSED\ntests/test_todos.py::test_delete_todo PASSED\ntests/test_todos.py::test_delete_todo_error PASSED\ntests/test_users.py::test_create_user PASSED\ntests/test_users.py::test_read_users PASSED\ntests/test_users.py::test_read_users_with_users PASSED\ntests/test_users.py::test_update_user PASSED\ntests/test_users.py::test_delete_user PASSED\ntests/test_users.py::test_update_user_with_wrong_user PASSED\ntests/test_users.py::test_delete_user_wrong_user PASSED\n\n---------- coverage: platform linux, python 3.12.3-final-0 -----------\nName                         Stmts   Miss  Cover\n------------------------------------------------\nfast_zero/__init__.py            0      0   100%\nfast_zero/app.py                11      0   100%\nfast_zero/database.py            7      2    71%\nfast_zero/models.py             29      0   100%\nfast_zero/routers/auth.py       26      0   100%\nfast_zero/routers/todos.py      50      0   100%\nfast_zero/routers/users.py      47      4    91%\nfast_zero/schemas.py            35      0   100%\nfast_zero/security.py           42      3    93%\nfast_zero/settings.py            7      0   100%\n------------------------------------------------\nTOTAL                          254      9    96%\n\n\n========== 28 passed in 4.94s ==========\nWrote HTML report to htmlcov/index.html\n

Embora essa seja uma abordagem que funciona, ela \u00e9 trabalhosa e temos que garantir que o container sempre esteja de p\u00e9. E como garantir isso durante a execu\u00e7\u00e3o dos testes?

"},{"location":"10/#containers-de-testes","title":"Containers de testes","text":"

Uma forma de interessante de usar containers em testes, \u00e9 usar containers espec\u00edficos para testes. Em python temos uma biblioteca chamada testcontainers.

TestContainers \u00e9 uma biblioteca que fornece uma interface python para executarmos os containers diretamente no c\u00f3digo dos testes. Voc\u00ea importa o c\u00f3digo referente a um container e ele te retorna todas as configura\u00e7\u00f5es para que voc\u00ea possa usar durante os testes. Desta forma, podemos controlar o fluxo de inicializa\u00e7\u00e3o/finaliza\u00e7\u00e3o dos containers diretamente no c\u00f3digo.

A biblioteca TestContainers tem diversas op\u00e7\u00f5es de containers, principalmente de bancos de dados. Como MariaDB, MongoDB, InfluxDB, etc. Tamb\u00e9m temos a op\u00e7\u00e3o de iniciar o PostgreSQL. Para isso, vamos instalar o testcontainters:

$ Execu\u00e7\u00e3o no terminal!
poetry add --group dev testcontainers\n

Com o testcontainers instalado iremos alterar a fixture de conex\u00e3o com o banco de dados, para usar um container que ser\u00e1 gerenciado pela fixture:

tests/conftest.py
from testcontainers.postgres import PostgresContainer #(1)!\n\n# ...\n\n@pytest.fixture\ndef session():\n    with PostgresContainer('postgres:16', driver='psycopg') as postgres: #(2)!\n        engine = create_engine(postgres.get_connection_url()) #(3)!\n        table_registry.metadata.create_all(engine)\n\n        with Session(engine) as session:\n            yield session\n            session.rollback()\n\n        table_registry.metadata.drop_all(engine)\n
  1. Faz o import do PostgresContainer dos testcontainers. O que quer dizer que ela ser\u00e1 iniciada somente uma vez durante toda a sess\u00e3o de testes.
  2. Cria um container de postgres na vers\u00e3o 16. Usando o psycopg como driver.
  3. get_connection_url() pega a URI do container postgres criado pelo testcontainers.

Agora, todas \u00e0s vezes em que a fixture de session for usada nos testes. Ser\u00e1 iniciado um novo container postgres na vers\u00e3o 16. E as intera\u00e7\u00f5es com o banco ser\u00e3o feitas nesse container.

Tudo pronto para execu\u00e7\u00e3o dos testes:

$ Execu\u00e7\u00e3o no terminal!
task test\n

Os testes devem ser executados com sucesso, mas algumas mensagens estranhas podem come\u00e7ar a aparecer entre o nome dos testes. Algo como:

$ Parte da resposta do comando de testes
tests/test_users.py::test_delete_user_wrong_user Pulling image postgres:16\nContainer started: beff0853dde0\nWaiting for container <Container: beff0853dde0> with image postgres:16 to be ready ...\nWaiting for container <Container: beff0853dde0> with image postgres:16 to be ready ...\nPASSED\n# ...\n========= 28 passed in 80.92s (0:01:20) =========\n

A mensagem Pulling image postgres:16 est\u00e1 dizendo que o container do postgres est\u00e1 sendo baixado do hub. Logo em seguida temos a mensagem Container started: beff0853dde0. Que diz que o container com id beff0853dde0 foi iniciado. Ap\u00f3s essa mensagem vemos o Waiting for container, que diz que est\u00e1 aguardando o container estar pronto para operar durante os testes.

Uma coisa preocupante nessa execu\u00e7\u00e3o \u00e9 a mensagem final: 28 passed in 80.92s (0:01:20). Embora todos os testes tenham sido executados com sucesso, levaram 80 segundos para serem executados (isso na minha m\u00e1quina).

Isso faz com que o tempo de feedback dos testes seja alto. Quando isso acontece, tendemos a executar menos os testes, por conta da demora. Ent\u00e3o, temos que melhorar esse tempo.

"},{"location":"10/#fixtures-de-sessao","title":"Fixtures de sess\u00e3o","text":"

As fixtures do pytest, por padr\u00e3o, s\u00e3o executadas todas \u00e0s vezes em que uma fun\u00e7\u00e3o de teste recebe a fixture como argumento:

c\u00f3digo de exemplo
@pytest.fixture\ndef fixture_de_exemplo():\n    # arrange\n    yield\n    # teardown\n\ndef teste_de_exemplo(fixture_de_exemplo): ...\n

Antes de executar o teste_de_exemplo, ser\u00e1 executado o c\u00f3digo da fixture at\u00e9 a instru\u00e7\u00e3o yield ser executada. A prepara\u00e7\u00e3o para o teste (arrange). Quando a fun\u00e7\u00e3o de teste \u00e9 finalizada, o bloco ap\u00f3s o yield \u00e9 executado. Chamamos ele de \"teardown\", para desfazer o efeito do \"arrage\". A volta do ambiente como era antes do \"arrange\".

Dizemos que uma fixture \"tradicional\" tem o escopo de fun\u00e7\u00e3o. Pois ela \u00e9 iniciada e finalizada em todas as fun\u00e7\u00f5es de teste.

Contudo, existem outros escopos, que precisam ser expl\u00edcitos durante a declara\u00e7\u00e3o da fixture, pelo par\u00e2metro scope. Existem diversos escopos:

Para resolver o problema com a lentid\u00e3o dos testes, iremos criar uma fixture para iniciar o container do banco de dados com o escopo \"session\".

sequenceDiagram\n    PytestRunner-->>Fixture: Executa a fixture at\u00e9 o yield\n    PytestRunner->>Testes: Executa todos os testes\n    Testes-->>Testes: Executa um teste\n    PytestRunner-->>Fixture: Executa a fixture depois do yield

Dessa forma, a fixture \u00e9 inicializada antes de todos os testes, est\u00e1 dispon\u00edvel durante a execu\u00e7\u00e3o das fun\u00e7\u00f5es, sendo finalizada ap\u00f3s a execu\u00e7\u00e3o de todos os testes.

"},{"location":"10/#fixture-para-engine","title":"Fixture para engine","text":"

Para resolver o problema da lentid\u00e3o, vamos criar nova fixture para a engine no escopo session. Ela ficar\u00e1 respons\u00e1vel por iniciar o container (arrange), criar a conex\u00e3o persistente com o postgres (yield) e desfazer o container ap\u00f3s a execu\u00e7\u00e3o de todos os testes (teardown):

tests/conftest.py
@pytest.fixture(scope='session')#(1)!\ndef engine():\n    with PostgresContainer('postgres:16', driver='psycopg') as postgres:\n\n        _engine = create_engine(postgres.get_connection_url())\n\n        with _engine.begin():#(2)!\n            yield _engine\n
  1. fixture sendo definida com o escopo 'session'.
  2. Inicia a conex\u00e3o com o banco de dados. A Session originalmente inicia a conex\u00e3o e a fecha. Contudo, como vamos criar diversas sessions, \u00e9 interessante que o controle da conex\u00e3o seja gerenciado pela engine.

Desta forma, por consequ\u00eancia, n\u00e3o iremos mais definir a engine na fixture de session. Usaremos a fixture de engine, que ser\u00e1 criada somente uma vez durante toda a execu\u00e7\u00e3o dos testes:

tests/conftest.py
@pytest.fixture\ndef session(engine):#(1)!\n    table_registry.metadata.create_all(engine)\n\n    with Session(engine) as session:\n        yield session\n        session.rollback()\n\n    table_registry.metadata.drop_all(engine)\n
  1. engine agora \u00e9 definida pela fixture de engine.

Com isso, podemos executar os testes novamente e devemos ver uma diferen\u00e7a significativa de tempo:

$ Execu\u00e7\u00e3o no terminal!
task test\n\n# ...\n========= 28 passed in 7.96s =========\n

Com o container sendo iniciado somente uma vez, o tempo total de execu\u00e7\u00e3o dos testes caiu para 7.96s, em compara\u00e7\u00e3o com os 80 segundos que t\u00ednhamos antes. Um tempo de feedback aceit\u00e1vel para execu\u00e7\u00e3o de testes.

Desta forma temos uma separa\u00e7\u00e3o do nosso container postgres de desenvolvimento, do container usado pelos testes. Fazendo com que a execu\u00e7\u00e3o dos testes n\u00e3o remova os dados inseridos durante o desenvolvimento da aplica\u00e7\u00e3o.

"},{"location":"10/#commit","title":"Commit","text":"

Para finalizar, ap\u00f3s criar nosso arquivo Dockerfile e compose.yaml, executar os testes e construir nosso ambiente, podemos fazer o commit das altera\u00e7\u00f5es no Git:

  1. Adicionando todos os arquivos modificados nessa aula com git add .
  2. Fa\u00e7a o commit das altera\u00e7\u00f5es com git commit -m \"Dockerizando nossa aplica\u00e7\u00e3o e inserindo o PostgreSQL\"
  3. Envie as altera\u00e7\u00f5es para o reposit\u00f3rio remoto com git push
"},{"location":"10/#conclusao","title":"Conclus\u00e3o","text":"

Dockerizar nossa aplica\u00e7\u00e3o FastAPI, com o PostgreSQL, nos permite garantir consist\u00eancia em diferentes ambientes. A combina\u00e7\u00e3o de Docker e Docker Compose simplifica o processo de desenvolvimento e implanta\u00e7\u00e3o. Na pr\u00f3xima aula, aprenderemos como levar nossa aplica\u00e7\u00e3o para o pr\u00f3ximo n\u00edvel executando os testes de forma remota com a integra\u00e7\u00e3o cont\u00ednua do GitHub Actions.

Agora que a aula acabou, \u00e9 um bom momento para voc\u00ea relembrar alguns conceitos e fixar melhor o conte\u00fado respondendo ao question\u00e1rio referente a ela.

Quiz

"},{"location":"11/","title":"Automatizando os testes com Integra\u00e7\u00e3o Cont\u00ednua","text":""},{"location":"11/#automatizando-os-testes-com-integracao-continua-ci","title":"Automatizando os testes com Integra\u00e7\u00e3o Cont\u00ednua (CI)","text":"

Objetivos da aula:

Caso prefira ver a aula em v\u00eddeo

Esse aula ainda n\u00e3o est\u00e1 dispon\u00edvel em formato de v\u00eddeo, somente em texto ou live!

Aula Slides C\u00f3digo Quiz

Na aula anterior, preparamos nossa aplica\u00e7\u00e3o para execu\u00e7\u00e3o em containers Docker, um passo fundamental para replicar o ambiente de produ\u00e7\u00e3o. Agora, vamos garantir que nossa aplica\u00e7\u00e3o mantenha sua integridade a cada mudan\u00e7a, implementando Integra\u00e7\u00e3o Cont\u00ednua.

"},{"location":"11/#integracao-continua-ci","title":"Integra\u00e7\u00e3o Cont\u00ednua (CI)","text":"

Integra\u00e7\u00e3o Cont\u00ednua (CI) \u00e9 uma pr\u00e1tica de desenvolvimento que envolve a integra\u00e7\u00e3o regular do c\u00f3digo-fonte ao reposit\u00f3rio principal, acompanhada de testes automatizados para garantir a qualidade. O objetivo dessa pr\u00e1tica \u00e9 identificar e corrigir erros de forma precoce, facilitando o desenvolvimento cont\u00ednuo e colaborativo. Pois, caso algu\u00e9m esque\u00e7a de rodar os testes ou exista algum problema na integra\u00e7\u00e3o entre dois commits, ou em algum merge, isso seja detectado no momento em que a integra\u00e7\u00e3o cont\u00ednua \u00e9 executada.

"},{"location":"11/#github-actions","title":"GitHub Actions","text":"

Entre as ferramentas dispon\u00edveis para CI, o GitHub Actions \u00e9 um servi\u00e7o do GitHub que automatiza workflows dentro do seu reposit\u00f3rio. Voc\u00ea pode configurar o GitHub Actions para executar a\u00e7\u00f5es espec\u00edficas \u2014 como testes automatizados \u2014 cada vez que um novo c\u00f3digo \u00e9 commitado no reposit\u00f3rio.

"},{"location":"11/#exemplo-de-workflow","title":"Exemplo de workflow","text":"

Workflows no GitHub Actions come\u00e7am com a constru\u00e7\u00e3o de um ambiente (escolher um sistema operacional e instalar suas depend\u00eancias) e criar diversos passos (steps em ingl\u00eas) para executar todas as etapas que fazemos no nosso computador durante o desenvolvimento. \u00c9 uma forma de garantir que o sistema funciona em um ambiente controlado. Dessa forma, todas \u00e0s vezes que subimos o c\u00f3digo para o reposit\u00f3rio (damos push) esse ambiente e a sequ\u00eancia de passos ser\u00e1 executada.

Por exemplo, como nosso sistema usar\u00e1 um sistema operacional GNU/Linux, podemos selecionar uma distribui\u00e7\u00e3o como Ubuntu para executar todos os passos da execu\u00e7\u00e3o dos nossos testes. Isso inclui diversas etapas como preparar o banco de dados, ler as vari\u00e1veis de ambiente, instalar o python e o poetry, etc.

Antes de mergulharmos na configura\u00e7\u00e3o do YAML, vamos visualizar o processo de constru\u00e7\u00e3o do nosso ambiente de CI com um fluxograma. Este diagrama mostra os passos essenciais, desde a instala\u00e7\u00e3o do Python at\u00e9 a execu\u00e7\u00e3o dos testes, ajudando a entender a sequ\u00eancia de opera\u00e7\u00f5es no GitHub Actions.

flowchart LR\n    Push -- Inicia --> Ubuntu\n    Ubuntu -- Execute os --> Passos\n    Ubuntu --> Z[Configure as vari\u00e1veis de ambiente]\n    subgraph Passos\n      A[Instale a vers\u00e3o 3.11 do Python] --> B[Copie os arquivos do reposit\u00f3rio para o ambiente]\n      B --> C[Instale o Poetry]\n      C --> D[Instale as depend\u00eancia do projeto com Poetry]\n      D --> E[Poetry execute os testes do projeto]\n    end

Com o fluxograma em mente, nosso objetivo de aula \u00e9 traduzir esses passos para a configura\u00e7\u00e3o pr\u00e1tica no GitHub Actions. Agora que temos uma vis\u00e3o clara do que nosso workflow envolve, nos aprofundaremos em como transformar essa teoria em pr\u00e1tica.

"},{"location":"11/#configurando-o-workflow-de-ci","title":"Configurando o workflow de CI","text":"

As configura\u00e7\u00f5es dos workflows no GitHub Actions s\u00e3o definidas em um arquivo YAML localizado em um path especificado pelo github no reposit\u00f3rio .github/workflows/. Dentro desse diret\u00f3rio podemos criar quantos workflows quisermos. Iniciaremos nossa configura\u00e7\u00e3o com um \u00fanico arquivo que chamaremos de pipeline.yaml:

Vers\u00e3o 3.11Vers\u00e3o 3.12 .github/workflows/pipeline.yaml
name: Pipeline\non: [push, pull_request]\n\njobs:\n  test:\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: Instalar o python\n        uses: actions/setup-python@v5\n        with:\n          python-version: '3.11'\n
.github/workflows/pipeline.yaml
name: Pipeline\non: [push, pull_request]\n\njobs:\n  test:\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: Instalar o python\n        uses: actions/setup-python@v5\n        with:\n          python-version: '3.12'\n

Basicamente um arquivo de workflow precisa de tr\u00eas componentes essenciais para serem definidos:

Nesse bloco de c\u00f3digo definimos que toda vez em que um push ou um pull_request ocorrer no nosso reposit\u00f3rio o Pipeline ser\u00e1 executado. Esse workflow tem um job chamado test que roda na \u00faltima vers\u00e3o do Ubuntu runs-on: ubuntu-latest. Nesse job chamado test temos uma lista de passos para serem executados, os steps.

O \u00fanico step que definimos \u00e9 a instala\u00e7\u00e3o do Python na vers\u00e3o \"3.11\":

Vers\u00e3o 3.11Vers\u00e3o 3.12
    steps:\n      - name: Instalar o python\n        uses: actions/setup-python@v5\n        with:\n          python-version: '3.11'\n
    steps:\n      - name: Instalar o python\n        uses: actions/setup-python@v5\n        with:\n          python-version: '3.12'\n

Nesse momento, se executarmos um commit do arquivo .github/workflows/pipeline.yaml e um push em nosso reposit\u00f3rio, um workflow ser\u00e1 iniciado.

$ Execu\u00e7\u00e3o no terminal!
git add .\ngit commit -m \"Instala\u00e7\u00e3o do Python\"\ngit push\n

Nisso, podemos ir at\u00e9 a p\u00e1gina do nosso reposit\u00f3rio no github e clicar na aba Actions, isso exibir\u00e1 todas \u00e0s vezes que um workflow for executado. Se clicarmos no wokflow seremos levados a p\u00e1gina dos jobs executados e se clicarmos nos jobs, temos uma descri\u00e7\u00e3o dos steps executados:

Guia Actions ->Jobs executados ->Steps executados

Isso nos mostra que tudo que configuramos no arquivo pipelines.yaml foi executado pelo actions no momento que em executamos um push no git.

Agora que temos essa vis\u00e3o geral de como o Actions monta e executa workflows, podemos nos concentrar em construir o nosso ambiente.

"},{"location":"11/#construcao-do-nosso-ambiente-de-ci","title":"Constru\u00e7\u00e3o do nosso ambiente de CI","text":"

Para executar nossos testes no workflow, precisamos seguir alguns passos essenciais:

  1. Instalar o Python: fundamental para executar nosso c\u00f3digo e testes, por ser a linguagem base da nossa aplica\u00e7\u00e3o.
  2. Instalar o Poetry: pois toda a nossa aplica\u00e7\u00e3o e depend\u00eancias foram constru\u00eddas com ele.
  3. Instalar as depend\u00eancias do projeto: far\u00e3o com que todas as bibliotecas necess\u00e1rias estejam dispon\u00edveis para a execu\u00e7\u00e3o dos testes.
  4. Executar os testes: passo crucial para validar a integridade e funcionalidade do c\u00f3digo antes de integr\u00e1-lo ao reposit\u00f3rio principal.
flowchart LR\n   Python[\"1: Python instalado\"] --> Poetry[\"2: Poetry instalado\"]\n   Poetry --> Deps[\"3: Instalar as depend\u00eancias via Poetry\"]\n   Deps --> Testes[\"4: Executar os testes via Poetry\"]

Cada um desses passos contribui para estabelecer um ambiente de CI robusto e confi\u00e1vel, assegurando que cada mudan\u00e7a no c\u00f3digo seja validada automaticamente, mantendo a qualidade e a estabilidade da nossa aplica\u00e7\u00e3o.

Para isso, devemos criar um step para cada uma dessas a\u00e7\u00f5es no nosso job test. Desta:

Vers\u00e3o 3.11Vers\u00e3o 3.12 .github/workflows/pipeline.yaml
    steps:\n      - name: Instalar o python\n        uses: actions/setup-python@v5\n        with:\n          python-version: '3.11'\n\n      - name: Instalar o poetry\n        run: pipx install poetry\n\n      - name: Instalar depend\u00eancias\n        run: poetry install\n\n      - name: Executar testes\n        run: poetry run task test\n
.github/workflows/pipeline.yaml
    steps:\n      - name: Instalar o python\n        uses: actions/setup-python@v5\n        with:\n          python-version: '3.12'\n\n      - name: Instalar o poetry\n        run: pipx install poetry\n\n      - name: Instalar depend\u00eancias\n        run: poetry install\n\n      - name: Executar testes\n        run: poetry run task test\n

Para testar essa implementa\u00e7\u00e3o no Actions, temos que fazer um commit1, para executar o trigger do CI:

$ Execu\u00e7\u00e3o no terminal!
git add .\ngit commit -m \"Adicionando passos para executar os testes no CI\"\ngit push\n

Assim, podemos avaliar o impacto desses passos no nosso workflow:

Se analisarmos com calma o resultado, veremos que a execu\u00e7\u00e3o do nosso workflow apresenta um erro de execu\u00e7\u00e3o. O erro est\u00e1 descrito na linha 12: Poetry could not find a pyproject.toml file in <path> or its parents. Se traduzirmos de maneira literal, a linha nos disse Poetry n\u00e3o encontrou o arquivo pyproject.toml no <path> ou em seus parentes.

Para solucionar esse problema, adicionaremos um passo antes da execu\u00e7\u00e3o dos testes para copiar o c\u00f3digo do nosso reposit\u00f3rio para o ambiente do workflow. O GitHub Actions oferece uma a\u00e7\u00e3o espec\u00edfica para isso, chamada actions/checkout. Vamos inclu\u00ed-la como o primeiro passo:

Vers\u00e3o 3.11Vers\u00e3o 3.12 .github/workflows/pipeline.yaml
jobs:\n  test:\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: Copia os arquivos do reposit\u00f3rio\n        uses: actions/checkout@v3\n\n      - name: Instalar o python\n        uses: actions/setup-python@v5\n        with:\n          python-version: '3.11'\n\n      # continua com os passos anteriormente definidos\n
.github/workflows/pipeline.yaml
jobs:\n  test:\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: Copia os arquivos do reposit\u00f3rio\n        uses: actions/checkout@v3\n\n      - name: Instalar o python\n        uses: actions/setup-python@v5\n        with:\n          python-version: '3.12'\n\n      # continua com os passos anteriormente definidos\n

Para testar a execu\u00e7\u00e3o desse passo faremos um novo commit para triggar o Actions:

$ Execu\u00e7\u00e3o no terminal!
git add .\ngit commit -m \"Adicionando o checkout ao pipeline\"\ngit push\n

Com isso, o erro anterior deve ser resolvido e teremos os testes sendo executados no workflow:

Ap\u00f3s resolver este problema, nos deparamos com outro desafio. Evidenciado no bloco a seguir:

Erro do CI!
ImportError while loading conftest '/home/runner/work/<path>/tests/conftest.py'.\ntests/conftest.py:6: in <module>\n    from fast_zero.app import app\nfast_zero/app.py:3: in <module>\n    from fast_zero.routes import auth, todos, users\nfast_zero/routes/auth.py:8: in <module>\n    from fast_zero.database import get_session\nfast_zero/database.py:6: in <module>\n    engine = create_engine(Settings().DATABASE_URL)\n../../../.cache/pypoetry/virtualenvs/fast-zero-IubsqyUK-py3.11/lib/python3.11/site-packages/pydantic_settings/main.py:61: in __init__\n    super().__init__(\nE   pydantic_core._pydantic_core.ValidationError: 4 validation errors for Settings\nE   DATABASE_URL\nE     Field required [type=missing, input_value={}, input_type=dict]\nE       For further information visit https://errors.pydantic.dev/2.1.2/v/missing\n
Erro completo no CI

Ao iniciar a execu\u00e7\u00e3o dos testes, encontramos um erro relacionado \u00e0 nossa classe settings.Settings. Isso ocorreu porque as vari\u00e1veis de ambiente necess\u00e1rias, como DATABASE_URL, n\u00e3o estavam definidas no workflow do CI. Este problema \u00e9 comum quando as vari\u00e1veis do arquivo .env, que utilizamos localmente, n\u00e3o s\u00e3o transferidas para o ambiente de CI.

"},{"location":"11/#configuracao-de-variaveis-de-ambiente-no-actions","title":"Configura\u00e7\u00e3o de vari\u00e1veis de ambiente no Actions","text":"

Como vimos anteriormente, nossa configura\u00e7\u00e3o de CI encontrou um problema devido \u00e0 aus\u00eancia de vari\u00e1veis de ambiente. Para resolver isso, utilizaremos uma funcionalidade dos reposit\u00f3rios do GitHub chamada 'Secrets'. Os 'Secrets' s\u00e3o uma maneira segura de armazenar informa\u00e7\u00f5es confidenciais, como vari\u00e1veis de ambiente, de forma criptografada. Eles s\u00e3o acess\u00edveis dentro do nosso workflow, permitindo que o GitHub Actions utilize esses valores sem exp\u00f4-los publicamente.

"},{"location":"11/#definindo-secrets-no-repositorio","title":"Definindo Secrets no Reposit\u00f3rio","text":"

Para definirmos as vari\u00e1veis de ambiente como 'Secrets', temos duas alternativas. A primeira \u00e9 acessar a aba Settings -> Secrets and variables do nosso reposit\u00f3rio no GitHub. Neste local, podemos inserir manualmente cada 'Secret', como URLs de banco de dados e chaves secretas.

A segunda alternativa \u00e9 utilizar o CLI do GitHub (gh) para adicionar todas as vari\u00e1veis de ambiente que temos no nosso arquivo .env. Isso pode ser feito com o seguinte comando:

$ Execu\u00e7\u00e3o no terminal!
gh secret set -f .env\n

Este comando pega todas as vari\u00e1veis de ambiente do arquivo .env e as configura como 'Secrets' no seu reposit\u00f3rio GitHub.

Configurando Secrets pela Interface Web

Se preferir configurar 'Secrets' pela interface web do GitHub, siga estes passos:

1 - Acesse Settings no seu reposit\u00f3rio2 - Adicione um novo segredo3 - Visualiza\u00e7\u00e3o dos segredos

Acesse Settings no seu reposit\u00f3rio GitHub. Em seguida clique na guia \"Secrets and variables\". Ap\u00f3s isso clique em \"New Repository secret\":

Para adicionar um novo scregredo no campo Name colocamos o nome de um de nossas vari\u00e1veis de ambientes. No campo Secret adicione o valor de uma vari\u00e1vel. Como, por exemplo:

Em seguida clique em Add secret.

Ap\u00f3s adicionar todos os segredos, sua p\u00e1gina de segredos deve se parecer com isso:

"},{"location":"11/#implementacao-no-arquivo-yaml","title":"Implementa\u00e7\u00e3o no Arquivo YAML","text":"

Ap\u00f3s definir as 'Secrets', o pr\u00f3ximo passo \u00e9 integr\u00e1-las ao nosso arquivo de workflow (.github/workflows/pipeline.yaml). Aqui, utilizamos uma sintaxe especial para acessar os valores armazenados como 'Secrets'. Cada 'Secret' \u00e9 mapeado para uma vari\u00e1vel de ambiente no job do nosso workflow, tornando esses valores seguros e acess\u00edveis durante a execu\u00e7\u00e3o do workflow. Vejamos como isso \u00e9 feito:

.github/workflows/pipeline.yaml
jobs:\n  test:\n    runs-on: ubuntu-latest\n\n    env:\n      DATABASE_URL: ${{ secrets.DATABASE_URL }}\n      SECRET_KEY: ${{ secrets.SECRET_KEY }}\n      ALGORITHM: ${{ secrets.ALGORITHM }}\n      ACCESS_TOKEN_EXPIRE_MINUTES: ${{ secrets.ACCESS_TOKEN_EXPIRE_MINUTES }}\n

Neste trecho de c\u00f3digo, a sintaxe ${{ secrets.NOME_DA_VARIAVEL }} \u00e9 usada para referenciar os 'Secrets' que definimos no reposit\u00f3rio. Por exemplo, secrets.DATABASE_URL buscar\u00e1 o valor da 'Secret' chamada DATABASE_URL que definimos. Assim que o workflow \u00e9 acionado, esses valores s\u00e3o injetados no ambiente do job, permitindo que nosso c\u00f3digo os acesse como vari\u00e1veis de ambiente normais.

Essa abordagem n\u00e3o s\u00f3 mant\u00e9m nossos dados confidenciais seguros, mas tamb\u00e9m nos permite gerenciar configura\u00e7\u00f5es sens\u00edveis de forma centralizada, facilitando atualiza\u00e7\u00f5es e manuten\u00e7\u00e3o.

"},{"location":"11/#atualizando-o-workflow","title":"Atualizando o Workflow","text":"

Com as 'Secrets' agora configuradas, precisamos atualizar o nosso workflow para incorporar essas mudan\u00e7as. Isso \u00e9 feito por meio de um novo commit e push para o reposit\u00f3rio, que acionar\u00e1 o workflow com as novas configura\u00e7\u00f5es.

$ Execu\u00e7\u00e3o no terminal!
git add .\ngit commit -m \"Adicionando as vari\u00e1veis de ambiente para o CI\"\ngit push\n

A execu\u00e7\u00e3o do workflow com as novas 'Secrets' nos permitir\u00e1 verificar se os problemas anteriores foram resolvidos.

E SIM, tudo funcionou como esper\u00e1vamos \ud83c\udf89

Agora a cada novo commit ou PR em nossa aplica\u00e7\u00e3o, os testes ser\u00e3o executados para garantir que a integra\u00e7\u00e3o pode acontecer sem problemas.

"},{"location":"11/#conclusao","title":"Conclus\u00e3o","text":"

Atrav\u00e9s deste m\u00f3dulo sobre Integra\u00e7\u00e3o Cont\u00ednua com GitHub Actions, ganhamos uma compreens\u00e3o s\u00f3lida de como a CI \u00e9 vital no desenvolvimento moderno de software. Vimos como o GitHub Actions, uma ferramenta poderosa e vers\u00e1til, pode ser utilizada para automatizar nossos testes e garantir a qualidade e estabilidade do c\u00f3digo a cada commit. Esta pr\u00e1tica n\u00e3o apenas otimiza nosso fluxo de trabalho, mas tamb\u00e9m nos ajuda a identificar e resolver problemas precocemente.

No pr\u00f3ximo m\u00f3dulo, o foco ser\u00e1 na prepara\u00e7\u00e3o da nossa aplica\u00e7\u00e3o FastAPI para o deployment em produ\u00e7\u00e3o. Exploraremos as etapas necess\u00e1rias e as melhores pr\u00e1ticas para tornar nossa aplica\u00e7\u00e3o pronta para o uso no mundo real, abordando desde configura\u00e7\u00f5es at\u00e9 estrat\u00e9gias de deployment eficazes.

Agora que a aula acabou, \u00e9 um bom momento para voc\u00ea relembrar alguns conceitos e fixar melhor o conte\u00fado respondendo ao question\u00e1rio referente a ela.

Quiz

  1. H\u00e1 alternativas para testar o workflow de CI sem fazer um commit, como a ferramenta Act que simula a execu\u00e7\u00e3o do workflow localmente usando Docker.\u00a0\u21a9

"},{"location":"12/","title":"Fazendo deploy no Fly.io","text":""},{"location":"12/#fazendo-deploy-no-flyio","title":"Fazendo deploy no Fly.io","text":"

Objetivos da aula:

Caso prefira ver a aula em v\u00eddeo

Esse aula ainda n\u00e3o est\u00e1 dispon\u00edvel em formato de v\u00eddeo, somente em texto ou live!

Aula Slides C\u00f3digo Quiz

Agora que temos uma API criada com integra\u00e7\u00e3o ao banco de dados e testes sendo executados via integra\u00e7\u00e3o cont\u00ednua. Chegou a t\u00e3o esperada hora de colocar nossa aplica\u00e7\u00e3o em produ\u00e7\u00e3o para que todas as pessoas possam acess\u00e1-la. Colocaremos nossa aplica\u00e7\u00e3o em produ\u00e7\u00e3o usando um servi\u00e7o de PaaS, chamado Fly.io.

"},{"location":"12/#o-flyio","title":"O Fly.io","text":"

O Fly.io \u00e9 uma plataforma de deploy que nos permite lan\u00e7ar nossas aplica\u00e7\u00f5es na nuvem e que oferece servi\u00e7os para diversas linguagens de programa\u00e7\u00e3o e frameworks como Python e Django, PHP e Laravel, Ruby e Rails, Elixir e Phoenix, etc.

Ao mesmo tempo, em que permite que o deploy de aplica\u00e7\u00f5es em containers docker tamb\u00e9m possam ser utilizadas, como \u00e9 o nosso caso. Al\u00e9m disso, o Fly disponibiliza bancos de dados para serem usados em nossas aplica\u00e7\u00f5es, como PostgreSQL e Redis.

O motivo pela escolha do Fly \u00e9 que ele permite que fa\u00e7amos deploys de aplica\u00e7\u00f5es em desenvolvimento / provas de conceito de forma gratuita - o que usaremos para \"colocar nossa aplica\u00e7\u00e3o no mundo\".

Para fazer o uso do fly.io \u00e9 necess\u00e1rio que voc\u00ea crie uma conta no servi\u00e7o.

"},{"location":"12/#flyclt","title":"Flyclt","text":"

Uma das formas de interagir com a plataforma \u00e9 via uma aplica\u00e7\u00e3o de linha de comando disponibilizada pelo Fly, o flyctl.

O flyctl precisa ser instalado em seu computador. Em algumas distribui\u00e7\u00f5es linux o flyctl est\u00e1 dispon\u00edvel nos reposit\u00f3rios de aplica\u00e7\u00f5es. Para Mac/Windows ou distribui\u00e7\u00f5es linux que n\u00e3o contam com o pacote no reposit\u00f3rio, voc\u00ea pode seguir o guia de instala\u00e7\u00e3o oficial.

Ap\u00f3s a instala\u00e7\u00e3o, voc\u00ea pode verificar se o flyctl est\u00e1 instalado em seu sistema operacional digitando o seguinte comando no terminal:

$ Execu\u00e7\u00e3o no terminal!
flyctl version\n\nflyctl v0.1.134 linux/amd64 Commit: ... BuildDate: 2023-12-08T18:58:44Z\n

A vers\u00e3o instalada no meu sistema \u00e9 a 0.1.134. No momento da sua instala\u00e7\u00e3o, voc\u00ea pode se deparar com uma vers\u00e3o mais recente do que a minha no momento, mas os comandos devem funcionar da mesma forma em qualquer vers\u00e3o menor que 0.2.0.

"},{"location":"12/#fazendo-login-via-terminal","title":"Fazendo login via terminal","text":"

Ap\u00f3s a instala\u00e7\u00e3o do flyctl \u00e9 importante que voc\u00ea efetue o login usando suas credenciais, para que o flyctl consiga vincular suas credenciais com a linha de comando. Para isso podemos executar o seguinte comando:

$ Execu\u00e7\u00e3o no terminal!
flyctl auth login\nOpening https://fly.io/app/auth/cli/91283719231023123 ...\n\nWaiting for session...\n

Isso abrir\u00e1 uma janela em seu browser pedindo que voc\u00ea efetue o login:

Ap\u00f3s inserir suas credenciais, voc\u00ea pode fechar o browser e no shell a execu\u00e7\u00e3o do comando terminar\u00e1 mostrando a conta em que voc\u00ea est\u00e1 logado:

$ Continua\u00e7\u00e3o da resposta do terminal
Waiting for session... Done\nsuccessfully logged in as <seu-email@de-login.com>\n

Desta forma, toda a configura\u00e7\u00e3o necess\u00e1ria para o iniciar o deploy est\u00e1 pronta!

"},{"location":"12/#configuracoes-para-o-deploy","title":"Configura\u00e7\u00f5es para o deploy","text":"

Agora com o flyctl devidamente configurado. Podemos iniciar o processo de lan\u00e7amento da nossa aplica\u00e7\u00e3o. O flyctl tem um comando espec\u00edfico para lan\u00e7amento, o launch. Contudo, o comando launch \u00e9 bastante interativo e ao final dele, o deploy da aplica\u00e7\u00e3o \u00e9 executado. Para evitar o deploy no primeiro momento, pois ainda existem coisas para serem configuradas, vamos execut\u00e1-lo da seguinte forma:

$ Execu\u00e7\u00e3o no terminal!
flyctl launch --no-deploy\n

Como resultado desse comando, o flyctl iniciar\u00e1 o modo interativo e exibir\u00e1 uma resposta pr\u00f3xima a essa:

$ Resposta do comando `launch`
Detected a Dockerfile app\nCreating app in /home/dunossauro/ci-example-fastapi\nWe're about to launch your app on Fly.io. Here's what you're getting:\n\nOrganization: <Seu Nome>             (fly launch defaults to the personal org)\nName:         fast-zero              (derived from your directory name)\nRegion:       Sao Paulo, Brazil      (this is the fastest region for you)\nApp Machines: shared-cpu-1x, 1GB RAM (most apps need about 1GB of RAM)\nPostgres:     <none>                 (not requested)\nRedis:        <none>                 (not requested)\n\n? Do you want to tweak these settings before proceeding? (y/N) \n

Nesse texto est\u00e3o destacadas as configura\u00e7\u00f5es padr\u00f5es do Fly. Como a Regi\u00e3o onde seu deploy ser\u00e1 feito (Sao Paulo, Brazil, o mais pr\u00f3ximo a mim nesse momento), a configura\u00e7\u00e3o da m\u00e1quina do deploy App Machines: shared-cpu-1x, 1GB RAM e a op\u00e7\u00e3o padr\u00e3o do Postgres: Postgres: <none>.

Uma nota sobre o Postgres + Fly

Antes de avan\u00e7armos, \u00e9 importante mencionar uma especificidade do Fly.io: para criar uma inst\u00e2ncia do PostgreSQL, a plataforma requer que um cart\u00e3o de cr\u00e9dito seja fornecido. Esta \u00e9 uma medida de seguran\u00e7a adotada para evitar o uso indevido de seus servi\u00e7os, como a execu\u00e7\u00e3o de ferramentas de minera\u00e7\u00e3o. Apesar dessa exig\u00eancia, o servi\u00e7o de PostgreSQL \u00e9 oferecido de forma gratuita. Mais detalhes podem ser encontrados neste artigo.

Caso voc\u00ea n\u00e3o adicione um cart\u00e3o, o erro levatado pelo flyctl est\u00e1 descrito na issue #73

A pergunta feita ao final dessa se\u00e7\u00e3o Do you want to tweak these settings before proceeding? pode ser traduzida como: Voc\u00ea deseja ajustar essas configura\u00e7\u00e3o antes de prosseguir?. Diremos que sim, digitando Y e em seguida Enter.

Assim, a configura\u00e7\u00e3o do lan\u00e7amento deve avan\u00e7ar e travar novamente com um texto parecido com esse:

$ Continua\u00e7\u00e3o do comando `launch`
? Do you want to tweak these settings before proceeding? Yes\nOpening https://fly.io/cli/launch/59f08b31a5efd30bdf5536ac516de5ga ...\n\nWaiting for launch data...\u28fd\n

Nesse momento, ele abrir\u00e1 o browser novamente exibira uma tela de ajustes de configura\u00e7\u00f5es:

Nesse momento faremos alguns ajustes em nossa configura\u00e7\u00e3o:

  1. Se\u00e7\u00e3o Basics: adicionaremos o nome da nossa aplica\u00e7\u00e3o no Fly. (Usarei fastzeroapp)
  2. Se\u00e7\u00e3o Memory & CPU: alteraremos o campo VM Memory para 256MB
  3. Se\u00e7\u00e3o Database:
    • alteraremos o campo Postgres para Fly Postgres
    • criaremos um nome para o nosso servi\u00e7o de banco de dados. (Usarei fastzerodb)
    • no campo Configuration alteraremos para Development - Single node, 1x shared CPU, 256MB RAM, 1GB disk
  4. Por fim, clicamos em Confirm Settings!

Ap\u00f3s esse ajuste, voc\u00ea pode fechar a janela do browser e voltar ao terminal, pois a parte interativa do launch ainda estar\u00e1 em execu\u00e7\u00e3o. Como a resposta a seguir \u00e9 bastante grande, colocarei ... para pular algumas linhas que n\u00e3o nos interessam nesse momento:

$ Continua\u00e7\u00e3o do comando `launch`
Created app 'fastzeroapp' in organization 'personal'\nAdmin URL: https://fly.io/apps/fastzeroapp\nHostname: fastzeroapp.fly.dev\nCreating postgres cluster in organization personal\nCreating app...\n\n...\n\nPostgres cluster fastzerodb created\n  Username:    postgres\n  Password:    t0Vf35P21eDlIVS\n  Hostname:    fastzerodb.internal\n  Flycast:     fdaa:2:77b0:0:1::a\n  Proxy port:  5432\n  Postgres port:  5433\n  Connection string: postgres://postgres:t0Vf35P21eDlIVS@fastzerodb.flycast:5432\n\n...\n\nPostgres cluster fastzerodb is now attached to fastzeroapp\nThe following secret was added to fastzeroapp:\n  DATABASE_URL=postgres://fastzeroapp:zHgBlc6JNaslGtz@fastzerodb.flycast:5432/fastzeroapp?sslmode=disable\nPostgres cluster fastzerodb is now attached to fastzeroapp\n? Create .dockerignore from .gitignore files? (y/N)\n

Nas linhas em destaque, vemos que o Fly se encarregou de criar um dashboard para vermos o status atual da nossa aplica\u00e7\u00e3o (https://fly.io/apps/nome-do-seu-app), inicializou um banco de dados postgres para usarmos em conjunto com nossa aplica\u00e7\u00e3o e tamb\u00e9m adicionou a url do banco de dados a vari\u00e1vel de ambiente DATABASE_URL com a configura\u00e7\u00e3o do postgres referente a nossa aplica\u00e7\u00e3o.

A Connection string do banco de dados deve ser armazenada por voc\u00ea, essa informa\u00e7\u00e3o n\u00e3o ser\u00e1 disponibilizada novamente, nem mesmo na parte web do Fly. Por isso guarde-a com cuidado e n\u00e3o compartilhem de forma alguma.

Assim sendo, para prosseguir com o launch devemos responder a seguinte pergunta: Create .dockerignore from .gitignore files? (y/N), que pode ser traduzida como Crie um .dockerignore partindo do arquivo .gitignore?. Vamos novamente responder que sim. Digitando Y e em seguida Enter.

$ Continua\u00e7\u00e3o do comando `launch`
Created <seu-path>/.dockerignore from 6 .gitignore files.\nWrote config file fly.toml\nValidating <seu-path>/fly.toml\nPlatform: machines\n\u2713 Configuration is valid\nYour app is ready! Deploy with `flyctl deploy`\n

Agora o flyctl criou um arquivo .dockerignore que n\u00e3o copia os arquivos do .gitignore para dentro do container docker e tamb\u00e9m criou um arquivo de configura\u00e7\u00e3o do Fly, o arquivo fly.toml.

Na \u00faltima linha ele nos disse que nossa aplica\u00e7\u00e3o est\u00e1 pronta para o deploy. Mas ainda temos mais configura\u00e7\u00f5es a fazer!

"},{"location":"12/#configuracao-dos-segredos","title":"Configura\u00e7\u00e3o dos segredos","text":"

Para que nossa aplica\u00e7\u00e3o funcione de maneira adequada, todas as vari\u00e1veis de ambiente precisam estar configuradas no ambiente. O flyctl tem um comando para vermos as vari\u00e1veis que j\u00e1 foram definidas no ambiente e tamb\u00e9m para definir novas. O comando secrets.

Para vermos as vari\u00e1veis j\u00e1 configuradas no ambiente, podemos executar o seguinte comando:

$ Execu\u00e7\u00e3o no terminal!
flyctl secrets list\n\nNAME            DIGEST              CREATED AT\nDATABASE_URL    f803df294e7326fa    22m43s ago\n

Uma coisa que podemos notar na resposta do secrets \u00e9 que ele leu nosso arquivo .env e adicionou a vari\u00e1vel de ambiente DATABASE_URL com base no postgres que foi criado durante o comando launch. Um ponto de aten\u00e7\u00e3o que devemos tomar nesse momento, \u00e9 que a vari\u00e1vel criada \u00e9 iniciada com o prefixo postgres://. Para que o sqlalchemy reconhe\u00e7a esse endere\u00e7o como v\u00e1lido, o prefixo deve ser alterado para postgresql+psycopg://. Para isso, usaremos a url fornecida pelo comando launch e alterar o prefixo.

Desta forma, podemos registrar a vari\u00e1vel de ambiente DATABASE_URL novamente. Agora com o valor correto:

$ Execu\u00e7\u00e3o no terminal!
flyctl secrets set DATABASE_URL=postgresql+psycopg://postgres:t0Vf35P21eDlIVS@fastzerodb.flycast:5432\nSecrets are staged for the first deployment\n

Contudo, n\u00e3o \u00e9 somente a vari\u00e1vel de ambiente do postgres que \u00e9 importante para que nossa aplica\u00e7\u00e3o seja executada. Temos que adicionar as outras vari\u00e1veis contidas no nosso .env ao Fly.

Iniciaremos adicionando a vari\u00e1vel ALGORITHM:

$ Execu\u00e7\u00e3o no terminal!
flyctl secrets set ALGORITHM=\"HS256\"\nSecrets are staged for the first deployment\n

Seguida pela vari\u00e1vel SECRET_KEY:

$ Execu\u00e7\u00e3o no terminal!
flyctl secrets set SECRET_KEY=\"your-secret-key\"\nSecrets are staged for the first deployment\n

E por fim a vari\u00e1vel ACCESS_TOKEN_EXPIRE_MINUTES:

$ Execu\u00e7\u00e3o no terminal!
flyctl secrets set ACCESS_TOKEN_EXPIRE_MINUTES=30\nSecrets are staged for the first deployment\n

Com isso, todos os segredos da nossa aplica\u00e7\u00e3o j\u00e1 est\u00e3o configurados no nosso ambiente do Fly. Agora podemos partir para o nosso t\u00e3o aguardado deploy.

"},{"location":"12/#deploy-da-aplicacao","title":"Deploy da aplica\u00e7\u00e3o","text":"

Para efetuarmos o deploy da aplica\u00e7\u00e3o, podemos usar o comando deploy doflyctl. Uma coisa interessante nessa parte do processo \u00e9 que o Fly pode fazer o deploy de duas formas:

  1. Copiar seus arquivos e fazer o build do docker na nuvem;
  2. Voc\u00ea pode fazer o build localmente e subir apenas o container para um reposit\u00f3rio dispon\u00edvel no Fly.

Optaremos por fazer o build localmente para n\u00e3o serem alocadas duas m\u00e1quinas em nossa aplica\u00e7\u00e3o1. Para executar o build localmente usamos a flag --local-only.

O Fly sobre duas inst\u00e2ncias por padr\u00e3o da nossa aplica\u00e7\u00e3o para melhorar a disponibilidade do app. Como vamos nos basear no uso gratuito, para todos poderem executar o deploy, adicionaremos a flag --ha=false ao deploy. Para desativamos a alta escalabilidade:

$ Execu\u00e7\u00e3o no terminal!
fly deploy --local-only --ha=false\n

Como a resposta do comando deploy \u00e9 bastante grande, substituirei o texto por ... para pular algumas linhas que n\u00e3o nos interessam nesse momento:

$ Resultado do comando `deploy`
==> Verifying app config\nValidating /home/dunossauro/ci-example-fastapi/fly.toml\nPlatform: machines\n\u2713 Configuration is valid\n--> Verified app config\n==> Building image\n==> Building image with Docker\n...\n => exporting to image                                                  0.0s\n => => exporting layers                                                 0.0s\n => => writing image sha256:b95a9d9f8abcea085550449a720a0bb9176e195fe4  0.0s\n => => naming to registry.fly.io/fastzeroapp:deployment-01HHKKDMF87FN4  0.0s\n--> Building image done\n==> Pushing image to fly\nThe push refers to repository [registry.fly.io/fastzeroapp]\n...\ndeployment-01HHKKDMF87FN441VA6H0JR4BS: digest: sha256:153a13e2931f923ab60df7e9dd0f18e2cc89fff7833ac18443935c7d0763a329 size: 2419\n--> Pushing image done\nimage: registry.fly.io/fastzeroapp:deployment-01HHKKDMF87FN441VA6H0JR4BS\nimage size: 349 MB\n\nWatch your deployment at https://fly.io/apps/fastzeroapp/monitoring\n\n-------\nUpdating existing machines in 'fastzeroapp' with rolling strategy\n\n-------\n \u2714 Machine 1781551ad22489 [app] update succeeded\n-------\n\nVisit your newly deployed app at https://fastzeroapp.fly.dev/\n

As primeiras linhas da resposta est\u00e3o relacionadas ao build do docker e a publica\u00e7\u00e3o no reposit\u00f3rio de imagens docker do Fly.

Na sequ\u00eancia, temos algumas informa\u00e7\u00f5es importantes a respeito do deploy da nossa aplica\u00e7\u00e3o. Como a URL de monitoramento (https://fly.io/apps/<nome-do-app>/monitoring), o aviso de que o deploy foi efetuado com sucesso (Machine 1781551ad22489 [app] update succeeded) e por fim, a URL de acesso a nossa aplica\u00e7\u00e3o (https://<nome-do-app>.fly.dev/).

Dessa forma podemos acessar a nossa aplica\u00e7\u00e3o acessando a URL fornecida pela \u00faltima linha de resposta em nosso browser, como https://fastzeroapp.fly.dev/:

E pronto, nossa aplica\u00e7\u00e3o est\u00e1 dispon\u00edvel para acesso! Obtivemos o nosso \"Ol\u00e1 mundo\". \ud83d\ude80\ud83d\ude80\ud83d\ude80\ud83d\ude80\ud83d\ude80\ud83d\ude80

Por\u00e9m, contudo, entretanto, ainda existe um problema na nossa aplica\u00e7\u00e3o no ar. Para ficar evidente, tente acessar o swagger da sua aplica\u00e7\u00e3o no ar e registrar um usu\u00e1rio usando o endpoint /user com o m\u00e9todo POST:

Voc\u00ea receber\u00e1 uma mensagem de erro, um erro 500: Internal Server Error, por de n\u00e3o efetuarmos a migra\u00e7\u00e3o no banco de dados de produ\u00e7\u00e3o. Por\u00e9m, para ter certeza disso, podemos usar a URL de monitoramento do Fly para ter certeza do erro ocorrido. Acessando: https://fly.io/apps/<nome-do-app>/monitoring, podemos visualizar os erros exibidos no console da nossa aplica\u00e7\u00e3o:

Podemos ver no console a mensagem: Relation \"users\" does not exist. Que traduzida pode ser lido como A rela\u00e7\u00e3o \"users\" n\u00e3o existe. O significa que a tabela \"users\" n\u00e3o foi criada ou n\u00e3o existe no banco de dados.

Desta forma, para que nossa aplica\u00e7\u00e3o funcione corretamente precisamos executar as migra\u00e7\u00f5es.

"},{"location":"12/#migrations","title":"Migrations","text":"

Agora que nosso container j\u00e1 est\u00e1 em execu\u00e7\u00e3o no fly, podemos executar o comando de migra\u00e7\u00e3o dos dados, pois ele est\u00e1 na mesma rede do postgres configurado pelo Fly2. Essa conex\u00e3o \u00e9 feita via SSH e pode ser efetuada com o comando ssh do flyctl.

Podemos fazer isso de duas formas, acessando efetivamente o container remotamente ou enviando somente um comando para o Fly. Optarei pela segunda op\u00e7\u00e3o, pois ela n\u00e3o \u00e9 interativa e usar\u00e1 somente uma \u00fanica chamada do shell. Desta forma:

$ Execu\u00e7\u00e3o no terminal!
flyctl ssh console -a fastzeroapp -C \"poetry run alembic upgrade head\"\n\nConnecting to fdaa:2:77b0:a7b:1f60:3f74:a755:2... complete\nSkipping virtualenv creation, as specified in config file.\nINFO  [alembic.runtime.migration] Context impl PostgresqlImpl.\nINFO  [alembic.runtime.migration] Will assume transactional DDL.\nINFO  [alembic.runtime.migration] Running upgrade  -> e018397cecf4, create users table\nINFO  [alembic.runtime.migration] Running upgrade e018397cecf4 -> de865434f506, create todos table\n
Poss\u00edvel erro que pode ocorrer!

Uma das formas de funcionamento padr\u00e3o do Fly \u00e9 desativar a m\u00e1quina caso ningu\u00e9m esteja usando. A\u00ed quando uma requisi\u00e7\u00e3o for feita para aplica\u00e7\u00e3o, ele inicia a m\u00e1quina novamente.

Caso voc\u00ea tente fazer um ssh e a aplica\u00e7\u00e3o n\u00e3o estiver de p\u00e9 no momento, voc\u00ea vai receber um erro como esse:

flyctl ssh console -a fastzeroapp -C \"poetry run alembic upgrade head\"\n\nError: app fastzeroapp has no started VMs.\nIt may be unhealthy or not have been deployed yet.\nTry the following command to verify:\n\nfly status\n

Nesse caso, voc\u00ea pode tentar acessar sua aplica\u00e7\u00e3o pelo browser ou via terminal e ela iniciar\u00e1 novamente. Nesse momento, quando a m\u00e1quina estiver rodando, voc\u00ea pode rodar a migra\u00e7\u00e3o novamente.

O comando ssh do flyctl \u00e9 um grupo de subcomandos para executar opera\u00e7\u00f5es espec\u00edficas em um container. Podemos pedir os logs de certificado com ssh log, inserir ou recuperar arquivos via FTP com o ssh ftp.

O subcomando que utilizamos ssh console nos fornece acesso ao shell do container. Por isso tivemos que especificar com a flag -a o nome da nossa aplica\u00e7\u00e3o (poder\u00edamos acessar o console do banco de dados, tamb\u00e9m). E a flag -C \u00e9 o comando que queremos que seja executado no console do container. Nesse caso, o comando completo representa: \"Acesse o console do app fastzeroapp via SSH e execute o comando poetry run alembic upgrade head\".

Dessa forma temos a migra\u00e7\u00e3o executada com sucesso. Voc\u00ea pode usar o comando ssh console sem especificar o comando tamb\u00e9m, dessa forma ele far\u00e1 um login via ssh no container.

Com isso, podemos voltar ao swagger e tentar executar a opera\u00e7\u00e3o de cria\u00e7\u00e3o de um novo user com um POST no endpoit /users. Tudo deve ocorrer perfeitamente dessa vez:

Agora, SIM, nossa aplica\u00e7\u00e3o est\u00e1 em produ\u00e7\u00e3o para qualquer pessoa poder usar e aproveitar da sua aplica\u00e7\u00e3o. Mande o link para geral e veja o que as pessoas acham da sua mais nova aplica\u00e7\u00e3o. \ud83d\ude80

"},{"location":"12/#commit","title":"Commit","text":"

Agora que fizemos todas as altera\u00e7\u00f5es necess\u00e1rias, devemos adicionar ao nosso reposit\u00f3rio os arquivos criados pelo flyctl launch. Os arquivos .dockerignore e fly.toml:

$ Execu\u00e7\u00e3o no terminal!
git add .\ngit commit -m \"Adicionando arquivos gerados pelo Fly\"\ngit push\n

E pronto!

"},{"location":"12/#conclusao","title":"Conclus\u00e3o","text":"

Assim, como prometido, chegamos ao final da jornada! Temos uma aplica\u00e7\u00e3o pequena, mas funcional em produ\u00e7\u00e3o! \ud83d\ude80

Ao longo desta aula, percorremos uma jornada sobre como implantar uma aplica\u00e7\u00e3o FastAPI com Docker no Fly.io, uma plataforma que oferece uma maneira simples e acess\u00edvel de colocar suas aplica\u00e7\u00f5es na nuvem. Exploramos alguns comandos do flyctl e fomos desde a configura\u00e7\u00e3o inicial at\u00e9 o processo de deploy e resolu\u00e7\u00e3o de problemas. Agora, com nossa aplica\u00e7\u00e3o pronta para o mundo, voc\u00ea possui o conhecimento necess\u00e1rio para compartilhar suas cria\u00e7\u00f5es com outras pessoas e continuar sua jornada no desenvolvimento web.

Na pr\u00f3xima aula discutiremos um pouco sobre o que mais voc\u00ea pode aprender para continuar desenvolvendo seus conhecimentos em FastAPI e desenvolvimento web, al\u00e9m de claro, de algumas dicas de materiais. At\u00e9 l\u00e1!

Agora que a aula acabou, \u00e9 um bom momento para voc\u00ea relembrar alguns conceitos e fixar melhor o conte\u00fado respondendo ao question\u00e1rio referente a ela.

Quiz

  1. No plano gratuito existe uma limita\u00e7\u00e3o de m\u00e1quinas dispon\u00edveis por aplica\u00e7\u00e3o. Quando usamos mais de uma m\u00e1quina, temos que ter um plano pago, por esse motivo, faremos o build localmente.\u00a0\u21a9

  2. \u00c9 poss\u00edvel executar a migra\u00e7\u00e3o usando a sua m\u00e1quina como ponto de partida. Para isso \u00e9 necess\u00e1rio usar o proxy do Fly: fly proxy 5432 -a fastzerodb. Dessa forma, a porta 5432 \u00e9 disponibilizada localmente para executar o comando. Acredito, por\u00e9m, que a conex\u00e3o via ssh \u00e9 mais proveitosa, no momento em que podemos explorar mais uma forma de interagir com o Fly.\u00a0\u21a9

"},{"location":"13/","title":"Despedida e pr\u00f3ximos passos","text":""},{"location":"13/#despedida-e-proximos-passos","title":"Despedida e pr\u00f3ximos passos","text":"

Objetivos da aula:

Caso prefira ver a aula em v\u00eddeo

Esse aula ainda n\u00e3o est\u00e1 dispon\u00edvel em formato de v\u00eddeo, somente em texto ou live!

Aula Slides

Estamos chegando ao final de nossa jornada juntos neste curso. Durante esse tempo, tivemos a oportunidade de explorar uma s\u00e9rie de conceitos e tecnologias essenciais para o desenvolvimento de aplica\u00e7\u00f5es web modernas e escal\u00e1veis. \u00c9 importante lembrar que o que vimos aqui \u00e9 apenas a ponta do iceberg. Ainda h\u00e1 muitos aspectos e detalhes que n\u00e3o pudemos cobrir neste curso, como tratamento de logs, observabilidade, seguran\u00e7a avan\u00e7ada, otimiza\u00e7\u00f5es de desempenho, entre outros. Encorajo a todos que continuem explorando e aprendendo.

"},{"location":"13/#revisao","title":"Revis\u00e3o","text":"

Ao longo deste curso, cobrimos uma s\u00e9rie de t\u00f3picos essenciais para o desenvolvimento de aplica\u00e7\u00f5es web modernas e robustas:

"},{"location":"13/#outros-materiais-produzidos-por-mim-sobre-fastapi","title":"Outros materiais produzidos por mim sobre FastAPI","text":"

J\u00e1 cobrimos alguns temas n\u00e3o citados neste curso usando FastAPI em algumas Lives de Python. Voc\u00ea pode assistir para aprender mais tamb\u00e9m.

"},{"location":"13/#templates-e-websockets","title":"Templates e WebSockets","text":"

Na Live de Python #164 conversamos sobre websockets com Python e usamos FastAPI para exemplificar o comportamento. Durante essa live criamos uma aplica\u00e7\u00e3o de chat e usamos os templates com HTML e Jinja2 e Brython para a\u00e7\u00f5es din\u00e2micas como far\u00edamos com JavaScript.

"},{"location":"13/#graphql-strawberry","title":"GraphQL (Strawberry)","text":"

Na Live de Python #185 conversamos sobre GraphQL um padr\u00e3o alternativo a REST APIs. Todos os exemplos foram aplicados usando Strawberry e FastAPI

"},{"location":"13/#sqlmodel","title":"SQLModel","text":"

Na Live de Python #235 conversamos sobre SQLModel um ORM alternativo ao SQLAlchemy que se integra com o Pydantic. O SQLModel tamb\u00e9m foi desenvolvido pelo Sebastian (criador do FastAPI). Caminhando ao final dessa aula, podemos ver a implementa\u00e7\u00e3o do SQLModel em uma aplica\u00e7\u00e3o b\u00e1sica com FastAPI.

"},{"location":"13/#fastui","title":"FastUI","text":"

Na Live de Python #259 conversamos sobre FastUI. Uma forma de usar modelos do Pydantic para retornar componentes React e contruir o front-end da aplica\u00e7\u00e3o coordenado pelo back-end. Um esquema de intera\u00e7\u00e3o de Back/Front conhecido como SDUI (Server-Driver User Interface).

"},{"location":"13/#proximos-passos","title":"Pr\u00f3ximos passos","text":"

Parte importante do aprendizado vem de entender que o que vimos aqui \u00e9 o b\u00e1sico, o m\u00ednimo que devemos saber para conseguir fazer uma aplica\u00e7\u00e3o consistente usando FastAPI. Agora \u00e9 a hora de trilhar novos caminhos e conhecer mais as possibilidades. Tanto na constru\u00e7\u00e3o de APIs, quanto no aprofundamento de recursos do FastAPI.

"},{"location":"13/#observabilidade","title":"Observabilidade","text":"

Embora tenhamos conseguido colocar nossa aplica\u00e7\u00e3o no ar sem grandes problemas. Quando a aplica\u00e7\u00e3o passa da nossa m\u00e1quina, em nosso contexto, para ser utilizada em escala no deploy. Perdemos a visualiza\u00e7\u00e3o do que est\u00e1 acontecendo de fato com a aplica\u00e7\u00e3o. Os erros que est\u00e3o acontecendo, quais partes do sistema est\u00e3o sendo mais utilizadas, o tempo que nossa aplica\u00e7\u00e3o est\u00e1 levando para executar algumas tarefas, etc.

Temos diversas pr\u00e1ticas e ferramentas que nos ajudam a entender como a aplica\u00e7\u00e3o est\u00e1 rodando em produ\u00e7\u00e3o. Como:

Caso queria aprender mais sobre observabilidade com FastAPI

Fizemos uma s\u00e9rie sobre opentelemetry com os exemplos usando FastAPI e diversas integra\u00e7\u00f5es entre servi\u00e7os. Pode ser que voc\u00ea goste e aprenda mais sobre o framework.

Uma introdu\u00e7\u00e3o a observabilidade usando FastAPI:

M\u00e9tricas de observabilidade usando FastAPI como exemplo:

Traces distribu\u00eddos com exemplos com FastAPI:

Logs de observabilidade com exemplos com FastAPI:

Uma pratica geral sobre observabilidade com FastAPI:

Uma forma de unir todos os conceitos de observabilidade \u00e9 utilizando um APM ou construindo sua pr\u00f3pria \"central de observabilidade\" com ferramentas como o Opentelemetry. Ele permite que instalemos diversas formas de instrumenta\u00e7\u00e3o em nossa aplica\u00e7\u00e3o e distribui os dados gerados para diversos backends. Como o Jaeger e o Grafana Tempo para armazenar traces. O Prometheus para ser um backend de m\u00e9tricas. O Grafana Loki para o armazenamento de logs. E por fim, criar um dashboard juntando todas essas informa\u00e7\u00f5es para exibir a sa\u00fade tanto da aplica\u00e7\u00e3o quanto das regras estabelecidas pelo neg\u00f3cio com o Grafana.

"},{"location":"13/#assincronismo","title":"Assincronismo","text":"

Um exemplo b\u00e1sico

Um exemplo b\u00e1sico de uso de Asyncio + FastAPI pode ser encontrado no ap\u00eandice B.

Outro ponto importante, e talvez o carro chefe do FastAPI \u00e9 poder ser usado de forma concorrente. O que significa que ele pode fazer outro trabalho enquanto aguarda por chamadas de input/ouput. Por exemplo, enquanto esperamos o postgres responder, podemos outra requisi\u00e7\u00e3o. Nesse momento, enquanto essa requisi\u00e7\u00e3o faz outra chamada ao banco, podemos responder a que est\u00e1vamos aguardando a resposta no banco de dados. Isso faz com que o tempo da aplica\u00e7\u00e3o seja otimizado durante a execu\u00e7\u00e3o.

Chamadas ass\u00edncronas em python s\u00e3o caracterizadas pelo uso das corrotinas async def e as esperas com await. A pr\u00f3pria documenta\u00e7\u00e3o do fastAPI apresenta um tutorial sobre AsyncIO.

Conversamos sobre AsyncIO diversas vezes na Live de Python. Se pudesse destacar um material que gostei de ter feito sobre esse assunto, seria a live sobre requisi\u00e7\u00f5es ass\u00edncronas:

Se tiver curiosidade de ver um exemplo real de AsyncIO e FastAPI nos mesmos moldes que aprendemos durante esse curso.

Temos o projeto do chat que fica dispon\u00edvel durante as lives. Tanto na Twitch, quanto no YouTube. O livestream-chat.

Aqui temos v\u00e1rios conceitos aplicados ao projeto. Templates com HTML e Jinja, WebSockets com diferentes canais, requisi\u00e7\u00f5es em backgroud, uso de asyncio, eventos do FastAPI, logs com loguru, integra\u00e7\u00e3o com um APM (Sentry), testes ass\u00edncronos, etc.

"},{"location":"13/#anotacao-de-tipos","title":"Anota\u00e7\u00e3o de tipos","text":"

Um dos pontos principais do uso do Pydantic e do FastAPI, que n\u00e3o nos aprofundamos nesse material.

Durante esse material vimos tipos embutidos diferentes como typing.Annotated, tipos customizados pelo Pydantic como email: EmailStr ou at\u00e9 mesmo tipos criados pelo SQLAlchemy como: Mapped[str]. Entender como o sistema de tipos usa essas anota\u00e7\u00f5es em tempo de execu\u00e7\u00e3o pode ser bastante proveitoso para escrever um c\u00f3digo que ser\u00e1 mais seguro em suas rela\u00e7\u00f5es.

O sistema de tipos do python est\u00e1 descrito aqui. Voc\u00ea pode estudar mais por esse material.

Nota do @dunossauro

Meu pr\u00f3ximo material em texto ser\u00e1 um livro online e gratuito sobre tipagem gradual com python. Quando estiver dispon\u00edvel, eu atualizarei essa p\u00e1gina com o link!

"},{"location":"13/#tarefas-em-background","title":"Tarefas em background","text":"

Um exemplo b\u00e1sico

Um exemplo b\u00e1sico de uso de tarefas em segundo plano pode ser encontrado no ap\u00eandice B.

Uma das coisas legais de poder usar AsyncIO \u00e9 poder realizar tarefas em segundo plano. Isso pode ser uma confirma\u00e7\u00e3o de cria\u00e7\u00e3o de conta, como um e-mail. Ou at\u00e9 mesmo a gera\u00e7\u00e3o de um relat\u00f3rio semanal.

Existem v\u00e1rias formas incr\u00edveis de uso, n\u00e3o irei me estender muito nesse t\u00f3pico, pois a documenta\u00e7\u00e3o do fastAPI tem uma \u00f3tima p\u00e1gina em portugu\u00eas sobre Tarefas em segundo plano. Acredito que valha a pena a leitura!

"},{"location":"13/#conclusao","title":"Conclus\u00e3o","text":"

Todos esses conceitos e pr\u00e1ticas s\u00e3o componentes fundamentais no desenvolvimento de aplica\u00e7\u00f5es web modernas e escal\u00e1veis. Eles nos permitem criar aplica\u00e7\u00f5es robustas, confi\u00e1veis e eficientes, que podem ser facilmente mantidas e escaladas.

Gostaria de agradecer a todos que acompanharam essa s\u00e9rie de aulas. Espero que tenham encontrado valor nas informa\u00e7\u00f5es e pr\u00e1ticas que compartilhamos aqui. Lembre-se, a jornada do aprendizado \u00e9 cont\u00ednua e cada passo conta. Continue explorando, aprendendo e crescendo.

At\u00e9 mais!

"},{"location":"14/","title":"Projeto final","text":""},{"location":"14/#projeto-final","title":"Projeto final","text":"

Voc\u00ea chegou ao final, PARABAINS \ud83c\udf89

No aprendizado, nada melhor que praticar! Para isso, vamos fazer nosso \"TCC\" ou como gostam de chamar no mundo \"interprize bizines\": um teste t\u00e9cnico.

A ideia deste projeto final \u00e9 simplesmente extrair tudo que aprendemos no curso para um grande exerc\u00edcio de fixa\u00e7\u00e3o em formato de projeto.

"},{"location":"14/#o-projeto","title":"O projeto","text":"

Neste projeto vamos construir uma API que segue os mesmos moldes da que desenvolvemos durante o curso, por\u00e9m, com outra proposta. Iremos fazer uma vers\u00e3o simplificado de um acervo digital de livros. Chamaremos de MADR (Mader), uma sigla para \"Meu Acervo Digital de Romances\".

O objetivo do projeto \u00e9 criarmos um gerenciador de livros e relacionar com seus autores. Tudo isso em um contexto bastante simplificado. Usando somente as funcionalidades que aprendemos no curso.

A implementa\u00e7\u00e3o ser\u00e1 baseada em 3 pilares:

graph\n    MADR --> A[\"Controle de acesso / Gerenciamento de contas\"]\n    MADR --> B[\"Gerenciamento de Livros\"]\n    MADR --> C[\"Gerenciamento de Romancistas\"]\n    A --> D[\"Gerenciamento de contas\"]\n    D --> Cria\u00e7\u00e3o\n    D --> Atualiza\u00e7\u00e3o\n    A --> G[\"Acesso via JWT\"]\n    D --> Dele\u00e7\u00e3o\n    B --> E[\"CRUD\"]\n    C --> F[\"CRUD\"]
"},{"location":"14/#a-api","title":"A API","text":"

Dividiremos os endpoints em tr\u00eas routers:

  1. contas: Gerenciamento de contas e de acesso \u00e0 API
  2. livros: Gerenciamento de livros
  3. romancistas: Gerenciamento de romancistas
"},{"location":"14/#contas","title":"Contas","text":"

O router de conta deve ser respons\u00e1vel pelas opera\u00e7\u00f5es referentes a cria\u00e7\u00e3o, altera\u00e7\u00e3o e dele\u00e7\u00e3o de contas. Os endpoints:

"},{"location":"14/#as-condicoes-do-token-jwt","title":"As condi\u00e7\u00f5es do token JWT","text":"

O tempo de expira\u00e7\u00e3o do token deve ser de 60 minutos, o algor\u00edtimo usado deve ser HS256 e o subject deve ser o email.

"},{"location":"14/#livros","title":"Livros","text":""},{"location":"14/#romancistas","title":"Romancistas","text":""},{"location":"14/#sanitizacao-de-dados","title":"Sanitiza\u00e7\u00e3o de dados","text":"

Antes de inserir no banco, os nomes de romancistas ou livros devem ser sanitizados.

Exemplos para os nomes:

Entrada Sanitizado \"Machado de Assis\" machado de assis \"Manuel\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Bandeira\" manuel bandeira \"Edgar Alan Poe\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\" edgar alan poe \"Androides Sonham Com Ovelhas El\u00e9tricas?\" androides sonham com ovelhas el\u00e9tricas \"\u00a0\u00a0breve \u00a0hist\u00f3ria \u00a0do tempo\u00a0\" breve hist\u00f3ria do tempo \"O mundo assombrado pelos dem\u00f4nios\" o mundo assombrado pelos dem\u00f4nios"},{"location":"14/#erros","title":"Erros","text":""},{"location":"14/#erros-de-autenticacao","title":"Erros de autentica\u00e7\u00e3o","text":"

Todos os erros relativos \u00e0 autentica\u00e7\u00e3o devem retornar o status code 400 BAD REQUEST com o seguinte schema:

{\n    \"message\": \"Email ou senha incorretos\"\n}\n
"},{"location":"14/#erros-de-permissao","title":"Erros de permiss\u00e3o","text":"

Caso uma pessoa tente fazer uma opera\u00e7\u00e3o sem a permiss\u00e3o necess\u00e1ria, o status code401 Unauthorized dever\u00e1 ser retornado com o json:

{\n    \"message\": \"N\u00e3o autorizado\"\n}\n
"},{"location":"14/#erro-nao-encontrado","title":"Erro n\u00e3o encontrado","text":"

Caso o id n\u00e3o exista no MADR, um erro 404 NOT FOUND deve ser retornado com o json:

{\n    \"message\": \"Romancista n\u00e3o consta no MADR\"\n}\n

ou ent\u00e3o

{\n    \"message\": \"Livro n\u00e3o consta no MADR\"\n}\n
"},{"location":"14/#erro-de-conflito","title":"Erro de conflito","text":"

Caso o recurso j\u00e1 exista, devemos retornar 409 CONFLICT com o json:

{\n    \"message\": \"{recurso} j\u00e1 consta no MADR\"\n}\n

Onde a vari\u00e1vel recurso \u00e9 relativa ao recurso que est\u00e1 duplicado. Exemplos para:

"},{"location":"14/#o-banco-de-dados-orm","title":"O banco de dados / ORM","text":"

A modelagem do banco deve contar com tr\u00eas tabelas: User, Livro e Romancista. Onde Livro e Romancista se relacionam da forma que romancistas podem estar relacionado a diversos livros e diversos livros devem ser associados a uma \u00fanica romancista. Como sugere o DER:

erDiagram\n  Romancista |o -- |{ Livro : livros\n  User {\n      int id PK\n      string email UK\n      string username UK\n      string senha\n  }\n  Livro {\n      int id  PK\n      string ano\n      string titulo UK\n      string id_romancista FK\n  }\n  Romancista {\n      int id PK\n      string nome UK\n      string livros\n  }
"},{"location":"14/#relacionamentos-no-orm","title":"Relacionamentos no ORM","text":"

Alguns problemas podem ser encontrados durante a cria\u00e7\u00e3o dos relacionamentos com SQLAlchemy, ent\u00e3o segue uma cola simples caso sinta que travou.

Em caso de emerg\u00eancia quebre o vidro
class Livro:\n    ...\n\n    autoria: Mapped[Romancista] = relationship(\n        init=False, back_populates='livros'\n    )\n\nclass Romancista:\n    ...\n\n    livros: Mapped[list['Livro']] = relationship(\n        init=False, back_populates='romancista', cascade='all, delete-orphan'\n    )\n
"},{"location":"14/#cenarios-de-teste","title":"Cen\u00e1rios de teste","text":"

O ideal \u00e9 que esse projeto tenha uma cobertura de testes de 100%. Afinal, foi dessa forma que passamos nosso tempo no curso, testando absolutamente tudo e garantindo que o c\u00f3digo funcione da maneira como deveria.

Nesse t\u00f3pico separei alguns cen\u00e1rios de testes usando a linguagem gherkin para te ajudar a pensar em como as requisi\u00e7\u00f5es ser\u00e3o recebidas e devem ser respondidas pela aplica\u00e7\u00e3o.

Esses cen\u00e1rios podem te guiar tanto para escrever a aplica\u00e7\u00e3o, quanto os testes.

"},{"location":"14/#gerenciamento-de-contas","title":"Gerenciamento de contas","text":"Cria\u00e7\u00e3o de contasCasos de erroAutentica\u00e7\u00e3o e autoriza\u00e7\u00e3o
Funcionalidade: Gerenciamento de conta\n\nCen\u00e1rio: Cria\u00e7\u00e3o de conta\n    Quando enviar um \"POST\" em \"/user\"\n    \"\"\"\n    {\n        \"username\": \"dunossauro\",\n        \"email\": \"dudu@dudu.com\",\n        \"password\": \"123456\"\n    }\n    \"\"\"\n    Ent\u00e3o devo receber o status \"201\"\n    E o json contendo\n    \"\"\"\n    {\n        \"email\": \"dudu@dudu.com\",\n        \"username\": \"dunossauro\"\n    }\n    \"\"\"\n\nCen\u00e1rio: Altera\u00e7\u00e3o de conta\n    Quando enviar um \"POST\" em \"/user\"\n    \"\"\"\n    {\n        \"username\": \"dunossauro\",\n        \"email\": \"dudu@dudu.com\",\n        \"password\": \"123456\"\n    }\n    \"\"\"\n    Quando enviar um \"PUT\" em \"/user/1\"\n    \"\"\"\n    {\n        \"username\": \"dunossauro\",\n        \"email\": \"dudu@dudu.com\",\n        \"password\": \"654321\"\n    }\n    \"\"\"\n    Ent\u00e3o devo receber o status \"200\"\n    E o json contendo\n    \"\"\"\n    {\n        \"username\": \"dunossauro\",\n        \"email\": \"dudu@dudu.com\"\n    }\n    \"\"\"\n\nCen\u00e1rio: Dele\u00e7\u00e3o da conta\n    Quando enviar um \"POST\" em \"/user\"\n    \"\"\"\n    {\n        \"username\": \"dunossauro\",\n        \"email\": \"dudu@dudu.com\",\n        \"password\": \"123456\"\n    }\n    \"\"\"\n    Quando enviar um \"DELETE\" em \"/user/1\"\n    Ent\u00e3o devo receber o status \"200\"\n    E o json contendo\n    \"\"\"\n    {\n        \"message\": \"Conta deletada com sucesso\"\n    }\n    \"\"\"\n
Cen\u00e1rio: Cria\u00e7\u00e3o de conta j\u00e1 existente\n    Quando enviar um \"POST\" em \"/user\"\n    \"\"\"\n    {\n        \"username\": \"dunossauro\",\n        \"email\": \"dudu@dudu.com\",\n        \"password\": \"123456\"\n    }\n    \"\"\"\n    Quando enviar um \"POST\" em \"/user\"\n    \"\"\"\n    {\n        \"username\": \"dunossauro\",\n        \"email\": \"dudu@dudu.com\",\n        \"password\": \"123456\"\n    }\n    \"\"\"\n    Ent\u00e3o devo receber o status \"400\"\n    E o json contendo\n    \"\"\"\n    {\n        \"message\": \"Conta j\u00e1 cadastrada\"\n    }\n    \"\"\"\n
TODO\n
"},{"location":"14/#gerenciamento-de-livros","title":"Gerenciamento de livros","text":"
Funcionalidade: Livro\n\nCen\u00e1rio: Registro de livro\n    Quando enviar um \"POST\" em \"/livro/\"\n    \"\"\"\n    {\n        \"ano\": 1973,\n        \"titulo\": \"Caf\u00e9 Da Manh\u00e3 Dos Campe\u00f5es\",\n        \"romancista_id\": 1\n    }\n    \"\"\"\n\n    Ent\u00e3o devo receber o status \"201\"\n    E o json contendo\n    \"\"\"\n    {\n        \"ano\": 1973,\n        \"titulo\": \"caf\u00e9 da manh\u00e3 dos campe\u00f5es\",\n        \"romancista_id\": 1\n    }\n    \"\"\"\n\n\nCen\u00e1rio: Altera\u00e7\u00e3o de livro\n    Quando enviar um \"PATCH\" em \"/livro/1\"\n    \"\"\"\n    {\n        \"ano\": 1974\n    }\n    \"\"\"\n\n    Ent\u00e3o devo receber o status \"200\"\n    E o json contendo\n    \"\"\"\n    {\n        \"ano\": 1974,\n        \"titulo\": \"caf\u00e9 da manh\u00e3 dos campe\u00f5es\",\n        \"romancista_id\": 1\n    }\n    \"\"\"\n\nCen\u00e1rio: Buscar livro por ID\n    Quando enviar um \"GET\" em \"/livro/1\"\n    Ent\u00e3o devo receber o status \"200\"\n    E o json contendo\n    \"\"\"\n    {\n        \"ano\": 1974,\n        \"titulo\": \"caf\u00e9 da manh\u00e3 dos campe\u00f5es\",\n        \"romancista_id\": 1\n    }\n    \"\"\"\n\nCen\u00e1rio: Dele\u00e7\u00e3o de livro\n    Quando enviar um \"DELETE\" em \"/livro/1\"\n\n    Ent\u00e3o devo receber o status \"200\"\n    E o json contendo\n    \"\"\"\n    {\n        \"message\": \"Livro deletado no MADR\"\n    }\n    \"\"\"\n\nCen\u00e1rio: Filtro de livros\n    Quando enviar um \"POST\" em \"/livro/\"\n    \"\"\"\n    {\n        \"ano\": 1900,\n        \"titulo\": \"Caf\u00e9 Da Manh\u00e3 Dos Campe\u00f5es\",\n        \"romancista_id\": 1\n    }\n    \"\"\"\n    E enviar um \"POST\" em \"/livro/\"\n    \"\"\"\n    {\n        \"ano\": 1900,\n        \"titulo\": \"Mem\u00f3rias P\u00f3stumas de Br\u00e1s Cubas\",\n        \"romancista_id\": 2\n    }\n    \"\"\"\n    E enviar um \"POST\" em \"/livro/\"\n    \"\"\"\n    {\n        \"ano\": 1865,\n        \"titulo\": \"Iracema\",\n        \"romancista_id\": 3\n    }\n    \"\"\"\n    E enviar um \"GET\" em \"/livro/?titulo=a&ano=1900\"\n\n    Ent\u00e3o devo receber o status \"200\"\n    E o json contendo\n    \"\"\"\n    {\n        \"livros\": [\n            {\"ano\": 1900, \"titulo\": \"caf\u00e9 da manh\u00e3 dos campe\u00f5es\", \"romancista_id\": 1, \"id\": 1},\n            {\"ano\": 1900, \"titulo\": \"mem\u00f3rias p\u00f3stumas de br\u00e1s cubas\", \"romancista_id\": 2, \"id\": 2}\n        ]\n    }\n    \"\"\"\n
"},{"location":"14/#gerenciamento-de-romancistas","title":"Gerenciamento de romancistas","text":"
Funcionalidade: Romancistas\n\n\nCen\u00e1rio: Cria\u00e7\u00e3o de Romancista\n    Quando enviar um \"POST\" em \"/romancista\"\n    \"\"\"\n    {\n        \"nome\": \"Clarice Lispector\"\n    }\n    \"\"\"\n\n    Ent\u00e3o devo receber o status \"201\"\n    E o json contendo\n    \"\"\"\n    {\n        \"nome\": \"clarice lispector\"\n    }\n    \"\"\"\n\nCen\u00e1rio: Buscar romancista por ID\n    Quando enviar um \"GET\" em \"/romancista/1\"\n    Ent\u00e3o devo receber o status \"200\"\n    E o json contendo\n    \"\"\"\n    {\n        \"nome\": \"clarice lispector\"\n    }\n    \"\"\"\n\nCen\u00e1rio: Altera\u00e7\u00e3o de Romancista\n    Quando enviar um \"PUT\" em \"/romancista/1\"\n    \"\"\"\n    {\n        \"nome\": \"manuel bandeira\"\n    }\n    \"\"\"\n    Ent\u00e3o devo receber o status \"200\"\n    E o json contendo\n    \"\"\"\n    {\n        \"nome\": \"manuel bandeira\"\n    }\n    \"\"\"\n\nCen\u00e1rio: Dele\u00e7\u00e3o de Romancista\n    Quando enviar um \"DELETE\" em \"/romancista/1\"\n    Ent\u00e3o devo receber o status \"200\"\n    E o json contendo\n    \"\"\"\n    {\n        \"message\": \"Romancista deletada no MADR\"\n    }\n    \"\"\"\n\nCen\u00e1rio: Busca de romancistas por filtro\n    Quando enviar um \"POST\" em \"/romancista\"\n    \"\"\"\n    {\n        \"nome\": \"Clarice Lispector\"\n    }\n    \"\"\"\n\n    E enviar um \"POST\" em \"/romancista\"\n    \"\"\"\n    {\n        \"nome\": \"Manuel Bandeira\"\n    }\n    \"\"\"\n\n    E enviar um \"POST\" em \"/romancista\"\n    \"\"\"\n    {\n        \"nome\": \"Paulo Leminski\"\n    }\n    \"\"\"\n\n    Quando enviar um \"GET\" em \"/romancista?nome=a\"\n    Ent\u00e3o devo receber o status \"200\"\n    E o json contendo\n    \"\"\"\n    {\n        \"romancistas\": [\n            {\"nome\": \"clarice lispector\", \"id\": 1},\n            {\"nome\": \"manuel bandeira\", \"id\": 2},\n            {\"nome\": \"paulo leminski\", \"id\": 3}\n        ]\n    }\n    \"\"\"\n
"},{"location":"14/#ferramentas","title":"Ferramentas","text":"

Gostaria que voc\u00ea se sentissem livres para escolher o conjunto de ferramentas que mais gostarem para fazer esse projeto. O formatador preferido, o servidor de aplica\u00e7\u00e3o preferido, projeto de vari\u00e1veis de ambiente preferido, etc.

As \u00fanicas coisas exigidas para a cria\u00e7\u00e3o desse projeto s\u00e3o:

  1. Python 3.11+
  2. FastAPI
  3. SQLAlchemy
  4. Alguma ferramenta para gerenciamento de projeto que suporte pyproject.toml
  5. PostgreSQL
  6. Containers (a ferramenta que preferir. Podman/docker/k8s/...)
  7. Pytest
"},{"location":"14/#entrega-do-projeto-final","title":"Entrega do projeto final","text":"

Criar um projeto utilizando git e hospedado em alguma plataforma (github/gitlab/codeberg/...) e postar nessa issue. Ao final, juntarei todos os projetos finais em uma tabela nesse site para que as pessoas possam aprender com as diferen\u00e7as entre os projetos.

\u00c9 imprescind\u00edvel que seu projeto tenha um README.md explicando quais foram as suas escolhas e como executar o seu projeto. Para podermos rodar e aprender com ele.

"},{"location":"apendices/a_instalacoes/","title":"A - Instalando as ferramentas externas","text":""},{"location":"apendices/a_instalacoes/#a-instalando-as-ferramentas-externas","title":"A - Instalando as ferramentas externas","text":"

Durante as aulas s\u00edncronas, diversas d\u00favidas sobre a configura\u00e7\u00e3o e instala\u00e7\u00e3o das ferramentas fora do python foram levantadas. A ideia dessa p\u00e1gina \u00e9 te auxiliar nas instala\u00e7\u00f5es.

S\u00e3o comandos r\u00e1pidos e simples, n\u00e3o tenho a intens\u00e3o de explicar o que essas ferramentas fazem exatamente, muitas explica\u00e7\u00f5es j\u00e1 foram escritas sobre elas na p\u00e1gina de configura\u00e7\u00e3o do projeto. A ideia \u00e9 agrupar todas as instala\u00e7\u00f5es um \u00fanico lugar.

"},{"location":"apendices/a_instalacoes/#pyenv-no-windows","title":"Pyenv no Windows","text":"

Para instalar o pyenv voc\u00ea precisa abrir seu terminal como administrado e executar o comando:

Invoke-WebRequest -UseBasicParsing -Uri \"https://raw.githubusercontent.com/pyenv-win/pyenv-win/master/pyenv-win/install-pyenv-win.ps1\" -OutFile \"./install-pyenv-win.ps1\"; &\"./install-pyenv-win.ps1\"\n

A mensagem pyenv-win is successfully installed. You may need to close and reopen your terminal before using it. aparecer\u00e1 na tela. Dizendo que precisamos reinicar o shell.

S\u00f3 precisamos fech\u00e1-lo e abrir de novo.

"},{"location":"apendices/a_instalacoes/#pyenv-no-linuxmacos","title":"Pyenv no Linux/MacOS","text":"

Como n\u00e3o tenho como cobrir a instala\u00e7\u00e3o em todos as distros, vou usar uma ferramenta chamada pyenv-installer. \u00c9 bastante simples, somente executar o comando:

$ Execu\u00e7\u00e3o no terminal!
curl https://pyenv.run | bash\n

Ap\u00f3s isso \u00e9 importante que voc\u00ea siga a instru\u00e7\u00e3o de adicionar a configura\u00e7\u00e3o no seu .bashrc:

~/.bashrc
export PATH=\"$HOME/.pyenv/bin:$PATH\"\neval \"$(pyenv init --path)\"\neval \"$(pyenv virtualenv-init -)\"\n

Caso use zsh, xonsh, .... bom... Voc\u00ea deve saber o que est\u00e1 fazendo :)

Ap\u00f3s isso reinicie o shell para que a vari\u00e1vel de ambiente seja carregada.

Caso esteja no ubuntu

\u00c9 importante que voc\u00ea instale o curl e o git antes:

$ Execu\u00e7\u00e3o no terminal!
sudo apt update\nsudo apt install curl git\n
"},{"location":"apendices/a_instalacoes/#instalacao-do-python-via-pyenv","title":"Instala\u00e7\u00e3o do Python via pyenv","text":"

Agora, com o pyenv instalado, voc\u00ea pode instalar a vers\u00e3o do python que usaremos no curso. Como descrito na p\u00e1gina de configura\u00e7\u00e3o do projeto:

$ Execu\u00e7\u00e3o no terminal!
pyenv install 3.12.3\n

A seguinte mensagem deve aparecer na tela:

:: [Info] ::  Mirror: https://www.python.org/ftp/python\n:: [Downloading] ::  3.12.3 ...\n:: [Downloading] ::  From https://www.python.org/ftp/python/3.12.3/python-3.12.3-amd64.exe\n:: [Downloading] ::  To   C:\\Users\\vagrant\\.pyenv\\pyenv-win\\install_cache\\python-3.12.3-amd64.exe\n:: [Installing] ::  3.12.3 ...\n:: [Info] :: completed! 3.12.3\n
"},{"location":"apendices/a_instalacoes/#configurando-a-versao-no-pyenv","title":"Configurando a vers\u00e3o no pyenv","text":"

Agora com vers\u00e3o instalada, devemos dizer ao shim, qual vers\u00e3o ser\u00e1 usada globalmente. Podemos executar esse comando:

$ Execu\u00e7\u00e3o no terminal!
pyenv global 3.12.3\n

Esse comando n\u00e3o costuma exibir nenhuma mensagem em caso de sucesso, se nada foi retornado, significa que tudo ocorreu como esperado.

Para testar se a vers\u00e3o foi definida, podemos chamar o python no terminal:

$ Execu\u00e7\u00e3o no terminal!
python --version\nPython 3.12.3 #(1)!\n
  1. Responde que a vers\u00e3o correta foi setada!
"},{"location":"apendices/a_instalacoes/#pipx","title":"pipx","text":"

O pipx \u00e9 uma ferramenta opcional na configura\u00e7\u00e3o do ambiente, mas \u00e9 extremamente recomendado que voc\u00ea a instale para simplificar a instala\u00e7\u00e3o de pacotes globais.

Para isso, voc\u00ea pode executar:

$ Execu\u00e7\u00e3o no terminal!
pip install pipx\n

A resposta do comando dever\u00e1 ser parecida com essa:

Collecting pipx\n  Downloading pipx-1.6.0-py3-none-any.whl.metadata (18 kB)\nCollecting argcomplete>=1.9.4 (from pipx)\n  Downloading argcomplete-3.3.0-py3-none-any.whl.metadata (16 kB)\nCollecting colorama>=0.4.4 (from pipx)\n  Downloading colorama-0.4.6-py2.py3-none-any.whl.metadata (17 kB)\nCollecting packaging>=20 (from pipx)\n  Downloading packaging-24.1-py3-none-any.whl.metadata (3.2 kB)\nCollecting platformdirs>=2.1 (from pipx)\n  Downloading platformdirs-4.2.2-py3-none-any.whl.metadata (11 kB)\nCollecting userpath!=1.9,>=1.6 (from pipx)\n  Downloading userpath-1.9.2-py3-none-any.whl.metadata (3.0 kB)\nCollecting click (from userpath!=1.9,>=1.6->pipx)\n  Downloading click-8.1.7-py3-none-any.whl.metadata (3.0 kB)\nDownloading pipx-1.6.0-py3-none-any.whl (77 kB)\n   \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501 77.8/77.8 kB 2.2 MB/s eta 0:00:00\nDownloading argcomplete-3.3.0-py3-none-any.whl (42 kB)\n   \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501 42.6/42.6 kB ? eta 0:00:00\nDownloading colorama-0.4.6-py2.py3-none-any.whl (25 kB)\nDownloading packaging-24.1-py3-none-any.whl (53 kB)\n   \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501 54.0/54.0 kB ? eta 0:00:00\nDownloading platformdirs-4.2.2-py3-none-any.whl (18 kB)\nDownloading userpath-1.9.2-py3-none-any.whl (9.1 kB)\nDownloading click-8.1.7-py3-none-any.whl (97 kB)\n   \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501 97.9/97.9 kB 295.3 kB/s eta 0:00:00\nInstalling collected packages: platformdirs, packaging, colorama, argcomplete, click, userpath, pipx\nSuccessfully installed argcomplete-3.3.0 click-8.1.7 colorama-0.4.6 packaging-24.1 pipx-1.6.0 platformdirs-4.2.2 userpath-1.9.2\n

Para testar se o pipx foi instalado com sucesso, podemos executar:

$ Execu\u00e7\u00e3o no terminal!
pipx --version\n

Se a vers\u00e3o for respondida, tudo est\u00e1 certo :)

Uma coisa recomendada de fazer, \u00e9 adicionar os paths do pipx nas vari\u00e1veis de ambiente, para isso podemos executar:

$ Execu\u00e7\u00e3o no terminal!
pipx ensurepath\n

Dessa forma, os pacotes estar\u00e3o no path. Podendo ser chamados pelo terminal sem problemas. A \u00faltima coisa que precisa ser feita \u00e9 abrir o terminal novamente, para que as novas vari\u00e1veis de ambiente sejam lidas.

"},{"location":"apendices/a_instalacoes/#ignr","title":"ignr","text":"

Com o pipx voc\u00ea pode executar:

$ Execu\u00e7\u00e3o no terminal!
pipx install ignr\n
"},{"location":"apendices/a_instalacoes/#poetry","title":"poetry","text":"

Com o pipx voc\u00ea pode executar:

$ Execu\u00e7\u00e3o no terminal!
pipx install poetry\n
"},{"location":"apendices/a_instalacoes/#gh","title":"GH","text":"

Gh \u00e9 um CLI para o github. Facilita em diversos momentos.

A instala\u00e7\u00e3o para diversos sistemas e variantes pode ser encontrada aqui.

"},{"location":"apendices/a_instalacoes/#docker","title":"Docker","text":"

A instala\u00e7\u00e3o do docker \u00e9 bastante diferente para sistemas operacionais diferentes e at\u00e9 mesmo em arquiteturas de processadores diferentes. Por exemplo, MacOS com intel ou arm, ou windows com WSL, ou hyper-V.

Por esse motivo, acredito que seja interessante voc\u00ea seguir os tutoriais oficiais:

"},{"location":"apendices/a_instalacoes/#docker-compose","title":"Docker Compose","text":"

A instala\u00e7\u00e3o varia bastante de sistema para sistema, mas voc\u00ea pode olhar o guia de instala\u00e7\u00e3o oficial.

"},{"location":"apendices/a_instalacoes/#git","title":"Git","text":"

O git pode ser baixado no site oficial para windows e mac. No Linux acredito que todas as distribui\u00e7\u00f5es t\u00eam o git como um pacote dispon\u00edvel para instala\u00e7\u00e3o.

"},{"location":"apendices/b_proximos_passos/","title":"B - Pr\u00f3ximos passos","text":""},{"location":"apendices/b_proximos_passos/#b-proximos-passos","title":"B - Pr\u00f3ximos passos","text":"

Esse ap\u00eandice se destina a mostrar alguns exemplos de c\u00f3digo da p\u00e1gina de despedida/pr\u00f3ximos passos. Alguns exemplos simples de como fazer algumas tarefas que n\u00e3o trabalhamos durante o curso.

"},{"location":"apendices/b_proximos_passos/#templates","title":"Templates","text":"

O FastAPI conta com um recurso de carregamento de arquivos est\u00e1ticos, como CSS e JS. E tamb\u00e9m permite a renderiza\u00e7\u00e3o de templates com jinja.

Os templates s\u00e3o formas de passar informa\u00e7\u00f5es para o HTML diretamente dos endpoints. Mas, comecemos pela estrutura. Criaremos dois diret\u00f3rios. Um para os templates e um para os arquivos est\u00e1ticos:Os templates s\u00e3o formas de passar informa\u00e7\u00f5es para o HTML diretamente dos endpoints. Mas, comecemos pela estrutura. Criaremos dois diret\u00f3rios. Um para os templates e um para os arquivos est\u00e1ticos:

Estrutura dos arquivos
.\n\u251c\u2500\u2500 app.py\n\u251c\u2500\u2500 static #(1)!\n\u2502  \u2514\u2500\u2500 style.css\n\u2514\u2500\u2500 templates #(2)!\n   \u2514\u2500\u2500 index.html\n
  1. Diret\u00f3rio para arquivos est\u00e1ticos.
  2. Diret\u00f3rio para os templates do jinja

Vamos adicionar um arquivo de estilo bastante simples, somente para ver o efeito da configura\u00e7\u00e3o:

static/style.css
h1 {\n    text-align: center;\n}\n

E um arquivo html usando a tag dos templates:

templates/index.html
<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\"/>\n    <title>index.html</title>\n    <link href=\"static/style.css\" rel=\"stylesheet\"/>\n  </head>\n  <body>\n    <h1>Ol\u00e1 {{ nome }}</h1> <!-- (1)! -->\n  </body>\n</html>\n
  1. Nomes dentro de {{ }} s\u00e3o vari\u00e1veis que ser\u00e3o preenchidas pelo contexto

Todas as vari\u00e1veis inclu\u00eddas em {{ vari\u00e1vel }} s\u00e3o passadas pelo endpoint no momento de retornar o template jinja. Com isso, podemos incluir valores da aplica\u00e7\u00e3o no HTML.

Para unir os arquivos est\u00e1ticos e os templates na aplica\u00e7\u00e3o podemos aplicar o seguinte bloco de c\u00f3digo:

app.py
from fastapi import FastAPI, Request\nfrom fastapi.responses import HTMLResponse\nfrom fastapi.staticfiles import StaticFiles\nfrom fastapi.templating import Jinja2Templates\n\napp = FastAPI()\n\n# Diret\u00f3rio contendo arquivos est\u00e1ticos\napp.mount('/static', StaticFiles(directory='static'), name='static')#(1)!\n\n# Diret\u00f3rio contendo os templates Jinja\ntemplates = Jinja2Templates(directory='templates')#(2)!\n\n\n@app.get('/{nome}', response_class=HTMLResponse)\ndef home(request: Request, nome: str):#(3)!\n    return templates.TemplateResponse(#(4)!\n        request=request, name='index.html', context={'nome': nome}\n    )\n
  1. O m\u00e9todo .mount cria um endpoint /static para retornar os arquivos no diret\u00f3rio static.
  2. Jinja2Templates mapeia um diret\u00f3rio em nossa aplica\u00e7\u00e3o onde armazenamos templates jinja para serem lidos pela aplica\u00e7\u00e3o.
  3. O objeto Request do FastAPI \u00e9 o objeto que representa corpo da requisi\u00e7\u00e3o e seu escopo.
  4. O m\u00e9todo TemplateResponse se encarrega de dizer qual o nome (name) do template que ser\u00e1 renderizado no html e context \u00e9 um dicion\u00e1rio que passa as vari\u00e1veis do endpoint para o arquivo html.

Para que os templates sejam renderizados pelo FastAPI precisamos instalar o jinja:

$ Execu\u00e7\u00e3o no terminal!
poetry add jinja2\n

E executar nosso projeto com:

$ Execu\u00e7\u00e3o no terminal!
task run\n

Desta forma, ao acessar o endpoint pela API, temos a jun\u00e7\u00e3o de templates e est\u00e1ticos acontecendo:

"},{"location":"apendices/b_proximos_passos/#asyncio","title":"Asyncio","text":"

O FastAPI tem suporte nativo para programa\u00e7\u00e3o ass\u00edncrona. A \u00fanica coisa que precisa ser feita para isso, a n\u00edvel do framework (n\u00e3o incluindo as depend\u00eancias) \u00e9 adicionar a palavra reservada async no in\u00edcio dos endpoints.

Da seguinte forma:

app.py
from asyncio import sleep\n\nfrom fastapi import FastAPI\n\napp = FastAPI()\n\n\n@app.get('/')\nasync def home():#(1)!\n    await sleep(1)#(2)!\n    return {'message': 'Ol\u00e1 mundo!'}\n
  1. Cria\u00e7\u00e3o de uma fun\u00e7\u00e3o ass\u00edncrona
  2. Contexto de escalonamento usando await

O escalonamento do loop durante as chamadas nos endpoints pode ser feito por meio da palavra reservada await.

Para que os testes sejam executados de forma ass\u00edncrona, precisamos instalar uma extens\u00e3o do pytest:

$ Execu\u00e7\u00e3o no terminal!
poetry add pytest-asyncio\n

Dessa forma podemos executar fun\u00e7\u00f5es de teste que tamb\u00e9m s\u00e3o ass\u00edncronas usando um marcador do pytest:

test_app.py
from fastapi.testclient import TestClient\n\nfrom app import app\n\nimport pytest\n\n\n@pytest.mark.asyncio #(1)!\nasync def test_async():\n    client = TestClient(app)\n    response = client.get('/')\n\n    assert response.json() == {'message': 'Ol\u00e1 mundo!'}\n
  1. Marcador para executar a fun\u00e7\u00e3o de teste dentro de um loop de eventos
"},{"location":"apendices/b_proximos_passos/#tarefas-em-segundo-plano-background","title":"Tarefas em segundo plano (Background)","text":"

TODO: adicionar explica\u00e7\u00e3o a esse t\u00f3pico

app.py
from time import sleep\n\nfrom fastapi import BackgroundTasks, FastAPI\n\n\napp = FastAPI()\n\n\ndef tarefa_em_segundo_plano(tempo=0):#(1)!\n    sleep(tempo)\n\n\n@app.get('/segundo-plano/{tempo}')\ndef segundo_plano(tempo: int, task: BackgroundTasks):#(2)!\n    task.add_task(tarefa_em_segundo_plano, tempo)#(3)!\n    return {'message': 'Sua requisi\u00e7\u00e3o est\u00e1 sendo processada!'}\n
  1. Uma fun\u00e7\u00e3o qualquer em python que ser\u00e1 executada a qualquer momento
  2. O tipo BackgroundTasks deve ser passado ao endpoint para que ele tenha a possibilidade de adicionar uma tarefa ao loop de eventos
  3. O m\u00e9todo .add_task adiciona a tarefa (fun\u00e7\u00e3o) ao loop de eventos.
"},{"location":"apendices/b_proximos_passos/#eventos-de-ciclo-de-vida","title":"Eventos de ciclo de vida","text":"

Os eventos de ciclo de vida s\u00e3o formas de iniciar ou testar alguma condi\u00e7\u00e3o antes da aplica\u00e7\u00e3o ser de fato inicializada. Voc\u00ea pode criar valida\u00e7\u00f5es, como saber se outra aplica\u00e7\u00e3o est\u00e1 de p\u00e9, configurar coisas antes da aplica\u00e7\u00e3o ser iniciada, como iniciar o banco de dados, etc.

Da mesma forma alguns casos para antes da aplica\u00e7\u00e3o ser finalizada tamb\u00e9m podem ser criadas. Como garantir que todas as tarefas em segundo plano estejam de fato finalizadas antes da aplica\u00e7\u00e3o parar de rodar.

app.py
from logging import getLogger\nfrom time import sleep\n\nfrom fastapi import FastAPI\n\n\nlogger = getLogger('uvicorn')\n\n@asynccontextmanager\nasync def lifespan(app: FastAPI):\n    logger.info('Iniciando a aplica\u00e7\u00e3o')#(1)!\n    yield  # Executa a aplica\u00e7\u00e3o\n    logger.info('Finalizando a aplica\u00e7\u00e3o')#(2)!\n\n\napp = FastAPI(lifespan=lifespan)#(3)!\n
  1. Log que ser\u00e1 emitido antes da aplica\u00e7\u00e3o ser iniciada
  2. Log que ser\u00e1 emitido antes da aplica\u00e7\u00e3o ser finalizada
  3. O par\u00e2metro lifespan recebe uma fun\u00e7\u00e3o ass\u00edncrona com yield para uma condi\u00e7\u00e3o de parada. Assim como uma fixture do pytest.

Podemos observar que os logs foram adicionados ao uvicorn antes e depois da execu\u00e7\u00e3o da aplica\u00e7\u00e3o:

$ Execu\u00e7\u00e3o no terminal!
uvicorn app:app\nINFO:     Started server process [254037]\nINFO:     Waiting for application startup.\nINFO:     Iniciando a aplica\u00e7\u00e3o\nINFO:     Application startup complete.\nINFO:     Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)\n# Apertando Ctrl + C\n^C\nINFO:     Shutting down\nINFO:     Waiting for application shutdown.\nINFO:     Finalizando a aplica\u00e7\u00e3o\nINFO:     Application shutdown complete.\nINFO:     Finished server process [254037]\n
"},{"location":"aulas/sincronas/","title":"Aulas s\u00edncronas","text":""},{"location":"aulas/sincronas/#aulas-sincronas","title":"Aulas s\u00edncronas","text":"

Faremos 14 encontros para as aulas s\u00edncronas em formato de live no meu canal do YouTube entre as datas de 11/06 e 25/07.

"},{"location":"aulas/sincronas/#como-vai-funcionar","title":"Como vai funcionar?","text":"

Nossos encontros acontecer\u00e3o as ter\u00e7as e quintas com dura\u00e7\u00e3o de 1h30m. Entre \u00e0s 20:00h e 21:30. Com chat aberto para tirar d\u00favidas enquanto a aula acontece. O material base \u00e9 o que est\u00e1 disposto neste site.

"},{"location":"aulas/sincronas/#agenda","title":"Agenda","text":"N Aula Data Link 00 Abertura e apresenta\u00e7\u00e3o do curso 11/06 Aula 00 01 Configurando o Ambiente de Desenvolvimento 13/06 Aula 01 02 Introdu\u00e7\u00e3o ao desenvolvimento WEB 18/06 Aula 02 03 Estruturando o Projeto e Criando Rotas CRUD 20/06 Aula 03 04 Configurando o Banco de Dados e Gerenciando Migra\u00e7\u00f5es com Alembic 25/06 Aula 04 05 Integrando Banco de Dados a API 27/06 Aula 05 06 Autentica\u00e7\u00e3o e Autoriza\u00e7\u00e3o com JWT 02/07 Aula 06 07 Refatorando a Estrutura do Projeto 04/07 Aula 07 S Aula reservada para tirar d\u00favidas 09/07 Aula d\u00favidas 08 Tornando o sistema de autentica\u00e7\u00e3o robusto 11/07 Aula 08 09 Criando Rotas CRUD para Gerenciamento de Tarefas 16/07 Aula 09 10 Dockerizando a nossa aplica\u00e7\u00e3o e introduzindo o PostgreSQL 18/07 Aula 10 11 Automatizando os testes com Integra\u00e7\u00e3o Cont\u00ednua (CI) 23/07 Aula 11 12 Fazendo deploy no Fly.io 25/07 Aula 12 13 Despedida e pr\u00f3ximos passos 30/07 Aula 13

Agenda com as datas:

Caso queira conversar e tirar d\u00favidas com as pessoas que tamb\u00e9m est\u00e3o/ir\u00e3o fazer o curso, temos um grupo no Telegram.

"},{"location":"aulas/sincronas/#o-que-sera-necessario-para-acompanhar","title":"O que ser\u00e1 necess\u00e1rio para acompanhar?","text":"
  1. Um link com internet para participar das lives
  2. Um computador para acompanhar as aulas
  3. Um sistema operacional que seja capaz de rodar o Docker (algumas vers\u00f5es do Windows tem problemas com isso)
  4. Uma conta no GitHub para hospedar os c\u00f3digos e exerc\u00edcios
  5. Uma conta no Fly.io para fazer o deploy de sua aplica\u00e7\u00e3o
"},{"location":"exercicios_resolvidos/aula_01/","title":"Exerc\u00edcios da aula 01","text":""},{"location":"exercicios_resolvidos/aula_01/#exercicios-da-aula-01","title":"Exerc\u00edcios da aula 01","text":""},{"location":"exercicios_resolvidos/aula_01/#exercicio-01","title":"Exerc\u00edcio 01","text":"

Crie um reposit\u00f3rio para acompanhar o curso e suba em alguma plataforma, como Github, gitlab, codeberg, etc. E compartilhe o link no reposit\u00f3rio do curso para podermos aprender juntos.

"},{"location":"exercicios_resolvidos/aula_02/","title":"Exerc\u00edcios da aula 02","text":""},{"location":"exercicios_resolvidos/aula_02/#exercicios-da-aula-02","title":"Exerc\u00edcios da aula 02","text":""},{"location":"exercicios_resolvidos/aula_02/#exercicio-01","title":"Exerc\u00edcio 01","text":"

Crie um endpoint que retorne \"ol\u00e1 mundo\" usando HTML e escreva seu teste.

Dica: para capturar a resposta do HTML do cliente de testes, voc\u00ea pode usar response.text

"},{"location":"exercicios_resolvidos/aula_02/#solucao","title":"Solu\u00e7\u00e3o","text":"

Para cria\u00e7\u00e3o do endpoint retornando HTML devemos alterar a classe de resposta padr\u00e3o do FastAPI para HTMLResponse:

Implementa\u00e7\u00e3o do endpoint
from fastapi import FastAPI\nfrom fastapi.responses import HTMLResponse\n\napp = FastAPI()\n\n\n@app.get('/', response_class=HTMLResponse)\ndef read_root():\n    return \"\"\"\n    <html>\n      <head>\n        <title> Nosso ol\u00e1 mundo!</title>\n      </head>\n      <body>\n        <h1> Ol\u00e1 Mundo </h1>\n      </body>\n    </html>\"\"\"\n

O teste que faz a valida\u00e7\u00e3o do valor retornado pelo endpoint n\u00e3o precisa ser muito robusto. A ideia principal do exerc\u00edcio \u00e9 somente validar se estamos retornando o \"Ol\u00e1 Mundo\" em formato de HTML:

Implementa\u00e7\u00e3o do teste
from http import HTTPStatus\n\nfrom fastapi.testclient import TestClient\n\nfrom fast_zero.app import app\n\n\ndef test_root_deve_retornar_ola_mundo_em_html():\n    client = TestClient(app)\n\n    response = client.get('/')\n\n    assert response.status_code == HTTPStatus.OK\n    assert '<h1> Ol\u00e1 Mundo </h1>' in response.text\n

O response.text \u00e9 um m\u00e9todo do cliente de testes do FastAPI que converte os bytes de resposta em string.

"},{"location":"exercicios_resolvidos/aula_03/","title":"Exerc\u00edcios da aula 03","text":""},{"location":"exercicios_resolvidos/aula_03/#exercicios-da-aula-03","title":"Exerc\u00edcios da aula 03","text":""},{"location":"exercicios_resolvidos/aula_03/#exercicio-01","title":"Exerc\u00edcio 01","text":"

Escreva um teste para o erro de 404 (NOT FOUND) para o endpoint de PUT.

"},{"location":"exercicios_resolvidos/aula_03/#solucao","title":"Solu\u00e7\u00e3o","text":"

A ideia de um teste de 404 para o PUT \u00e9 tentar fazer a altera\u00e7\u00e3o de um usu\u00e1rio que n\u00e3o existe no banco de dados.

Teste de 404
def test_update_user_should_return_not_found__exercicio(client):\n    response = client.put(\n        '/users/666', #(1)!\n        json={\n            'username': 'bob',\n            'email': 'bob@example.com',\n            'password': 'mynewpassword',\n        },\n    )\n    assert response.status_code == HTTPStatus.NOT_FOUND #(2)!\n    assert response.json() == {'detail': 'User not found'} #(3)!\n
  1. O user com id 666 n\u00e3o existe no nosso sistema.
  2. Como o user n\u00e3o existe, o status code retornado pela fun\u00e7\u00e3o ser\u00e1 404
  3. Por entrar no bloco de valida\u00e7\u00e3o do if o HTTPException foi preenchido com detail='User not found'
"},{"location":"exercicios_resolvidos/aula_03/#exercicio-02","title":"Exerc\u00edcio 02","text":"

Escreva um teste para o erro de 404 (NOT FOUND) para o endpoint de DELETE

"},{"location":"exercicios_resolvidos/aula_03/#solucao_1","title":"Solu\u00e7\u00e3o","text":"

A ideia de um teste de 404 para o DELETE \u00e9 tentar fazer a altera\u00e7\u00e3o de um usu\u00e1rio que n\u00e3o existe no banco de dados.

Teste de 404
def test_delete_user_should_return_not_found__exercicio(client):\n    response = client.delete('/users/666') #(1)!\n\n    assert response.status_code == HTTPStatus.NOT_FOUND #(2)!\n    assert response.json() == {'detail': 'User not found'} #(3)!\n
  1. O user com id 666 n\u00e3o existe no nosso sistema.
  2. Como o user n\u00e3o existe, o status code retornado pela fun\u00e7\u00e3o ser\u00e1 404
  3. Por entrar no bloco de valida\u00e7\u00e3o do if o HTTPException foi preenchido com detail='User not found'
"},{"location":"exercicios_resolvidos/aula_03/#exercicio-03","title":"Exerc\u00edcio 03","text":"

Crie um endpoint de GET para pegar um \u00fanico recurso como users/{id} e fazer seus testes para 200 e 404.

"},{"location":"exercicios_resolvidos/aula_03/#solucao_2","title":"Solu\u00e7\u00e3o","text":"

A implementa\u00e7\u00e3o do endpoint \u00e9 bastante parecida com as que fizemos at\u00e9 agora. Precisamos validar se existe um id compar\u00edvel no nosso banco de dados falso. Nos baseando pela posi\u00e7\u00e3o do elento na lista.

@app.get('/users/{user_id}', response_model=UserPublic)\ndef read_user__exercicio(user_id: int):\n    if user_id > len(database) or user_id < 1:\n        raise HTTPException(\n            status_code=HTTPStatus.NOT_FOUND, detail='User not found'\n        )\n\n    return database[user_id - 1]\n

Um dos testes \u00e9 sobre o retorno 404, que \u00e9 retornado um user que n\u00e3o existe na base de dados e outro \u00e9 o comportamento padr\u00e3o para quando o user \u00e9 retornado com sucesso:

def test_get_user_should_return_not_found__exercicio(client):\n    response = client.get('/users/666')\n\n    assert response.status_code == HTTPStatus.NOT_FOUND\n    assert response.json() == {'detail': 'User not found'}\n\n\ndef test_get_user___exercicio(client):\n    response = client.get('/users/1')\n\n    assert response.status_code == HTTPStatus.OK\n    assert response.json() == {\n        'username': 'bob',\n        'email': 'bob@example.com',\n        'id': 1,\n    }\n
"},{"location":"exercicios_resolvidos/aula_04/","title":"Exerc\u00edcios da aula 04","text":""},{"location":"exercicios_resolvidos/aula_04/#exercicios-da-aula-04","title":"Exerc\u00edcios da aula 04","text":""},{"location":"exercicios_resolvidos/aula_04/#exercicio-01","title":"Exerc\u00edcio 01","text":"

Fazer uma altera\u00e7\u00e3o no modelo (tabela User) e adicionar um campo chamado updated_at:

"},{"location":"exercicios_resolvidos/aula_04/#solucao","title":"Solu\u00e7\u00e3o","text":"
@table_registry.mapped_as_dataclass\nclass User:\n    __tablename__ = 'users'\n\n    id: Mapped[int] = mapped_column(init=False, primary_key=True)\n    username: Mapped[str] = mapped_column(unique=True)\n    password: Mapped[str]\n    email: Mapped[str] = mapped_column(unique=True)\n    created_at: Mapped[datetime] = mapped_column(\n        init=False, server_default=func.now()\n    )\n    updated_at: Mapped[datetime] = mapped_column(  # Exerc\u00edcio\n        init=False, server_default=func.now(), onupdate=func.now()\n    )\n
"},{"location":"exercicios_resolvidos/aula_04/#exercicio-02","title":"Exerc\u00edcio 02","text":"

Criar uma nova migra\u00e7\u00e3o autogerada com alembic.

"},{"location":"exercicios_resolvidos/aula_04/#solucao_1","title":"Solu\u00e7\u00e3o","text":"

Comando explicado na aula para gerar uma migra\u00e7\u00e3o autom\u00e1tica:

$ Execu\u00e7\u00e3o no terminal!
alembic revision --autogenerate -m \"exercicio 02 aula 04\"\n

O Comando deve retornar algo parecido com isso:

Resultado do comando
INFO  [alembic.runtime.migration] Context impl SQLiteImpl.\nINFO  [alembic.runtime.migration] Will assume non-transactional DDL.\nINFO  [alembic.autogenerate.compare] Detected added column 'users.updated_at'\n  Generating /home/dunossauro/git/fastapi-do-\n  zero/codigo_das_aulas/04/migrations/versions/bb77f9679811_exercicio_02_aula_04.py ...  done\n

O arquivo de migra\u00e7\u00f5es deve se parecer com esse:

/migrations/versions/bb77f9679811_exercicio_02_aula_04.py
\"\"\"exercicio 02 aula 04\n\nRevision ID: bb77f9679811\nRevises: 74f39286e2f6\nCreate Date: ...\n\n\"\"\"\nfrom typing import Sequence, Union\n\nfrom alembic import op\nimport sqlalchemy as sa\n\n\n# revision identifiers, used by Alembic.\nrevision: str = 'bb77f9679811'\ndown_revision: Union[str, None] = '74f39286e2f6'\nbranch_labels: Union[str, Sequence[str], None] = None\ndepends_on: Union[str, Sequence[str], None] = None\n\n\ndef upgrade() -> None:\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.add_column('users', sa.Column('updated_at', sa.DateTime(), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False)) #(1)!\n    # ### end Alembic commands ###\n\n\ndef downgrade() -> None:\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.drop_column('users', 'updated_at') #(2)!\n    # ### end Alembic commands ###\n
  1. Adiciona a coluna updated_at na tabela users
  2. Remove a coluna updated_at na tabela users
"},{"location":"exercicios_resolvidos/aula_04/#exercicio-03","title":"Exerc\u00edcio 03","text":"

Aplicar essa migra\u00e7\u00e3o ao banco de dados

"},{"location":"exercicios_resolvidos/aula_04/#solucao_2","title":"Solu\u00e7\u00e3o","text":"

Para aplicar a ultima migra\u00e7\u00e3o devemos nos mover at\u00e9 a head:

$ Execu\u00e7\u00e3o no terminal!
alembic upgrade head\nINFO  [alembic.runtime.migration] Context impl SQLiteImpl.\nINFO  [alembic.runtime.migration] Will assume non-transactional DDL.\nINFO  [alembic.runtime.migration] Running upgrade 74f39286e2f6 -> bb77f9679811, exercicio 02 aula 04\n

Checando o resultado no schema do banco de dados:

$ Execu\u00e7\u00e3o no terminal!
sqlite3 database.db \nSQLite version 3.46.1 2024-08-13 09:16:08\nEnter \".help\" for usage hints.\nsqlite> .schema\nCREATE TABLE alembic_version (\n        version_num VARCHAR(32) NOT NULL, \n        CONSTRAINT alembic_version_pkc PRIMARY KEY (version_num)\n);\nCREATE TABLE users (\n        id INTEGER NOT NULL, \n        username VARCHAR NOT NULL, \n        password VARCHAR NOT NULL, \n        email VARCHAR NOT NULL, \n        created_at DATETIME DEFAULT (CURRENT_TIMESTAMP) NOT NULL,\n        updated_at DATETIME DEFAULT (CURRENT_TIMESTAMP) NOT NULL, \n        PRIMARY KEY (id), \n        UNIQUE (email), \n        UNIQUE (username)\n);\n

Podemos ver que o campo updated_at foi criado com o tipo DATETIME e com o valor padr\u00e3o para CURRENT_TIMESTAMP, assim como no created_at.

"},{"location":"exercicios_resolvidos/aula_05/","title":"Exerc\u00edcios da aula 05","text":""},{"location":"exercicios_resolvidos/aula_05/#exercicios-da-aula-05","title":"Exerc\u00edcios da aula 05","text":""},{"location":"exercicios_resolvidos/aula_05/#exercicio-01","title":"Exerc\u00edcio 01","text":"

Escrever um teste para o endpoint de POST (create_user) que contemple o cen\u00e1rio onde o username j\u00e1 foi registrado. Validando o erro 400.

"},{"location":"exercicios_resolvidos/aula_05/#solucao","title":"Solu\u00e7\u00e3o","text":"

Para testar esse cen\u00e1rio, precisamos de um username que j\u00e1 esteja registrado na base de dados. Para isso, podemos usar a fixture de user que criamos. Ela \u00e9 uma garantia que o valor j\u00e1 est\u00e1 inserido no banco de dados:

/tests/test_app.py
def test_create_user_should_return_400_username_exists__exercicio(client, user):\n    response = client.post(\n        '/users/',\n        json={\n            'username': user.username,\n            'email': 'alice@example.com',\n            'password': 'secret',\n        },\n    )\n    assert response.status_code == HTTPStatus.BAD_REQUEST\n    assert response.json() == {'detail': 'Username already exists'}\n
"},{"location":"exercicios_resolvidos/aula_05/#exercicio-02","title":"Exerc\u00edcio 02","text":"

Escrever um teste para o endpoint de POST (create_user) que contemple o cen\u00e1rio onde o e-mail j\u00e1 foi registrado. Validando o erro 400.

"},{"location":"exercicios_resolvidos/aula_05/#solucao_1","title":"Solu\u00e7\u00e3o","text":"

Para testar esse cen\u00e1rio, precisamos de um e-mail que j\u00e1 esteja registrado na base de dados. Para isso, podemos usar a fixture de user que criamos. Ela \u00e9 uma garantia que o valor j\u00e1 est\u00e1 inserido no banco de dados:

/tests/test_app.py
def test_create_user_should_return_400_email_exists__exercicio(client, user):\n    response = client.post(\n        '/users/',\n        json={\n            'username': 'alice',\n            'email': user.email,\n            'password': 'secret',\n        },\n    )\n    assert response.status_code == HTTPStatus.BAD_REQUEST\n    assert response.json() == {'detail': 'Email already exists'}\n
"},{"location":"exercicios_resolvidos/aula_05/#exercicio-03","title":"Exerc\u00edcio 03","text":"

Atualizar os testes criados nos exerc\u00edcios 1 e 2 da aula 03 para suportarem o banco de dados.

"},{"location":"exercicios_resolvidos/aula_05/#solucao_2","title":"Solu\u00e7\u00e3o","text":"

O objetivo desse exerc\u00edcio n\u00e3o necessariamente uma atualiza\u00e7\u00e3o dos testes, mas o caso de uma execu\u00e7\u00e3o para validar se os testes, como foram feitos ainda funcionariam nessa nova estrutura.

Os meus testes da aula 03:

def test_delete_user_should_return_not_found__exercicio(client):\n    response = client.delete('/users/666')\n\n    assert response.status_code == HTTPStatus.NOT_FOUND\n    assert response.json() == {'detail': 'User not found'}\n\n\ndef test_update_user_should_return_not_found__exercicio(client):\n    response = client.put(\n        '/users/666',\n        json={\n            'username': 'bob',\n            'email': 'bob@example.com',\n            'password': 'mynewpassword',\n        },\n    )\n    assert response.status_code == HTTPStatus.NOT_FOUND\n    assert response.json() == {'detail': 'User not found'}\n

Ao executar eles continuam passando.

"},{"location":"exercicios_resolvidos/aula_05/#exercicio-04","title":"Exerc\u00edcio 04","text":"

Implementar o banco de dados para o endpoint de listagem por id, criado no exerc\u00edcio 3 da aula 03.

"},{"location":"exercicios_resolvidos/aula_05/#solucao_3","title":"Solu\u00e7\u00e3o","text":"

Esse exerc\u00edcio basicamente consiste em duas partes. A primeira \u00e9 alterar o endpoint para usar o banco de dados. Isso pode ser feito de maneira simples injetando a depend\u00eancia da session:

@app.get('/users/{user_id}', response_model=UserPublic)\ndef read_user__exercicio(\n    user_id: int, session: Session = Depends(get_session)\n):\n    db_user = session.scalar(select(User).where(User.id == user_id))\n\n    if not db_user:\n        raise HTTPException(\n            status_code=HTTPStatus.NOT_FOUND, detail='User not found'\n        )\n\n    return db_user\n

A segunda parte \u00e9 entender o que precisa ser feito nos testes para que eles consigam cobrir os dois casos previstos. O de sucesso e o de falha.

O teste de sucesso continua passando, pois ele de fato n\u00e3o depende de nenhuma intera\u00e7\u00e3o com o banco de dados:

def test_get_user_should_return_not_found__exercicio(client):\n    response = client.get('/users/666')\n\n    assert response.status_code == HTTPStatus.NOT_FOUND\n    assert response.json() == {'detail': 'User not found'}\n

J\u00e1 o teste de sucesso, depende que exista um usu\u00e1rio na base dados. Com isso podemos usar a fixture de user tanto na chamada, quanto na valida\u00e7\u00e3o dos dados:

def test_get_user___exercicio(client, user):\n    response = client.get(f'/users/{user.id}')\n\n    assert response.status_code == HTTPStatus.OK\n    assert response.json() == {\n        'username': user.username,\n        'email': user.email,\n        'id': user.id,\n    }\n
"},{"location":"exercicios_resolvidos/aula_06/","title":"Exerc\u00edcios da aula 06","text":""},{"location":"exercicios_resolvidos/aula_06/#exercicios-da-aula-06","title":"Exerc\u00edcios da aula 06","text":""},{"location":"exercicios_resolvidos/aula_06/#exercicio-01","title":"Exerc\u00edcio 01","text":"

Fa\u00e7a um teste para cobrir o cen\u00e1rio que levanta exception credentials_exception na autentica\u00e7\u00e3o caso o User n\u00e3o seja encontrado. Ao olhar a cobertura de security.py voc\u00ea vai notar que esse contexto n\u00e3o est\u00e1 coberto.

"},{"location":"exercicios_resolvidos/aula_06/#solucao","title":"Solu\u00e7\u00e3o","text":"

...

"},{"location":"exercicios_resolvidos/aula_06/#exercicio-02","title":"Exerc\u00edcio 02","text":"

Reveja os testes criados at\u00e9 a aula 5 e veja se eles ainda fazem sentido (testes envolvendo 400)

"},{"location":"exercicios_resolvidos/aula_06/#solucao_1","title":"Solu\u00e7\u00e3o","text":"

...

"},{"location":"projetos/projetos_finais/","title":"Projetos finais","text":""},{"location":"projetos/projetos_finais/#projetos-finais","title":"Projetos finais","text":"

O objetivo dessa p\u00e1gina \u00e9 unir todos os projetos finais de pessoas que fizeram o curso em uma tabela para que voc\u00ea possa consultar, estudar, aprender com as pessoas que tamb\u00e9m fizeram o curso e ver como elas criaram o projeto final.

Caso seu projeto final n\u00e3o esteja aqui, o poste nessa issue

Link do projeto Seu @ no git Coment\u00e1rio (opcional) projeto @dunossauro Ainda n\u00e3o fiz FastZero-MADR @clcosta - madr @alfmorais - bookshelf @Tomas-Tamantini - fast_zero_projeto_final vitorTheDev \u00d3timo curso! madr elyssonmr Sensacional. Consegui fazer tudo async :) madr arturpeixoto Muitos aprendizados! madr_fast @itsGab Projeto baseado no curso, com fastapi_pagination. Obrigado pelo \u00f3timo curso! app_library @thigoap Docker est\u00e1 em uma branch separada (docker). madr @henriquesebastiao 201 CREATED \u2705 fastapi-madr @michelebswm Sensacional madr @taconi API async, com templates usando Jinja2 e httpx e 99.9% em ~portugu\u00eas~ brasileiro. madr @MuriloRohor MADR @gabitrombetta madr @danielbrito91 Baita curso \u2764\ufe0f madr-fastapi @lealre tcc_madr @guilopes15 madr @eduardoklosowski Projeto utilizando Dev Containers integrado ao Visual Studio Code, deploy no Kubernetes via Helm usando um cluster local gerenciado pelo minikube. Mais detalhes confira o hist\u00f3rico do projeto. madr @Romariolima1998 mada_sync LuizPerciliano Projeto fluindo de vento em popa, muito obrigado Edu por t\u00e3o grande aprendizado! \u2764\ufe0f fastapi-acervo-digital @heltonteixeira92 Conte\u00fado supimpa \ud83d\ude80 madr @duca_meneses Excelente curso MADR @hugocs1 Muito foda, obrigado! fastapi_zero_madr_projeto_final @devfabiopedro \ud83d\udcbb Feito! \u270c\ufe0f\ud83d\ude01"},{"location":"projetos/repositorios/","title":"Reposit\u00f3rios do curso","text":""},{"location":"projetos/repositorios/#repositorios-do-curso","title":"Reposit\u00f3rios do curso","text":"

O objetivo dessa p\u00e1gina \u00e9 unir todos os reposit\u00f3rios de pessoas que fizeram o curso em uma tabela para que voc\u00ea possa consultar, estudar, aprender com as pessoas que tamb\u00e9m fizeram o curso e ver como elas resolveram os exerc\u00edcios do curso.

Caso o seu reposit\u00f3rio n\u00e3o esteja aqui, \u00e9 por que voc\u00ea n\u00e3o resolveu os exerc\u00edcios da Primeira aula

Link do projeto Seu @ no git Coment\u00e1rio (opcional) fast_zero @dunossauro Implementa\u00e7\u00e3o do material do curso sem altera\u00e7\u00f5es e sem exerc\u00edcios fast_zero @morgannadev Implementa\u00e7\u00e3o do material do curso sem altera\u00e7\u00f5es fast_zero @rodrigosbarretos Foi bacana enfrentar os problemas instalando as coisas no Ubuntu no WLS fast_zero @azmovi Que projeto bacana dudu, muito obrigado fastapi-do-zero @aguynaldo Estudo a partir do curso de FastAPI do Dunossauro. fastapi-do-zero @gercinei Minha primeira experi\u00eancia com um framework fast_zero @ju14x Implementa\u00e7\u00e3o do material do curso sem altera\u00e7\u00f5es Fast_zero @IsisG13 Estudando com o curso de FastAPI economio @marcos-ag-nolasco Criando um app fullstack, cujo backend vai ser baseado no fast_zero fastapi-do-zero @RWallan Tentando implementar o curso com Async fastapi-do-zero @gylmonteiro Estudos inicias com fastapi crono_task_backend @mau-me App para gerenciamento de tasks, com o backend baseado no fast_zero fast_zero @navegantes Mais uma ferramenta de paito pra caixinha fast_zero @willrockoliv Projeto incr\u00edvel @dunossauro! Muito obrigado!! fastapi-training @Brunoliy Implementa\u00e7\u00e3o do material do curso sem altera\u00e7\u00f5es backend-portfolio @stherzada Implementa\u00e7\u00e3o do curso e aprimorando aprendizado no backend \u2728 fast_zero @lbmendes Usando a VM gratis da OCI para fazer o Curso fast_zero @vilmarspies Implementa\u00e7\u00e3o do material do curso sem altera\u00e7\u00f5es fast_zero @RogerPatriota Implementa\u00e7\u00e3o do material do curso sem altera\u00e7\u00f5es fast-zero @machadoah Aprendendo FastAPI \ud83d\udc0d \u2728 fast_zero @FabricioPython Curtindo FastAPI fast_api @juniohorniche Massa demais esse conte\u00fado \ud83d\ude0d fast_zero @taconi Com hatch, async, podman, Woodpecker CI e hospedado no Codeberg fastapi-do-zero @joceliovieira Repo com material pessoal (notas, c\u00f3digos, etc), sem clone do repo oficial curso-fastapi-webdev @joaobugelli Parab\u00e9ns pelo conte\u00fado e material excelentes! Voc\u00ea \u00e9 demais Duno! notas-musicais-api @rochamatcomp API para o Notas musicais fast_cometa @mpdiasrosa Estudando FastAPI do zero \ud83d\udc0d fastapi_do_zero @arturfarias Projeto de estudos com poetry e fastAPI fastapi-do-zero-dunossauro @leopoldocouto Material de estudo do Curso de FastAPI do Zero do @dunossauro fast_zero @psifabiohenrique Implementa\u00e7\u00e3o do material do curso sem altera\u00e7\u00f5es fast_zero 2005869 Implementa\u00e7\u00e3o do material sem altera\u00e7\u00f5es fast_zero @miguelferreiraZ Estudo a partir do curso: Fast-API do Zero fast_zero @kassoliver Aprendendo um pouco mais de FastAPI com o Dunossauro \ud83d\udc0d fast_zero @jhonatacaiob Aprendendo um pouco de FastAPI com o Dunossauro \ud83d\udc0d fast_zero @arnaldovitor Material de estudo do curso \"FastAPI do Zero\" do @dunossauro fast_zero @vcwild Acompanhando o conte\u00fado do curso s\u00edncrono fast_api @viniciusaito Curtindo as aulas do curso de fast api fast_zero @andreztz Aprendendo FastAPI com @dunossauro \ud83d\udc0d fast_zero @SouzaPatrick Aprendendo um pouco de FastAPI com o Dunossauro \ud83d\udc0d fastzero @AndreGM Aprendendo FastAPI com @dunossauro fastapidozero-dunossaudo @francadev Aprendendo FastAPI com @dunossauro course_fast_api_zero @vmfrois Aprendendo FastAPI com @dunossauro fastapi-do-zero @Everton92 Aprendendo FastAPI com o mais brabo @dunossauro fastapi_zero_duno @guiribeirodev Desenvolvimento Web e FastAPI com o @dunossauro fast_zero @andrefelipemsc Implementa\u00e7\u00e3o do material do curso sem altera\u00e7\u00f5es. Porque este \u00e9 o melhor e mais completo curso da internet. fast_zero @jlplautz Projeto baseado no curso FastAPI com o mestre Dunossauro. fasst_zero @prpires66 Implementa\u00e7\u00e3o do material do curso sem altera\u00e7\u00f5es fastAPI_do_zero @BrunoPinheirofe Primeiros passos com FastAPI fastapidozero @lucaspaimrj21 Configurando o ambiente de desenvolvimento e primeiro commit fast_api_todo @joiltonrsilva Desenvolvendo aplica\u00e7\u00e3o TODO com FastAPI nas aulas do prof. @dunossauro fast_zero @duca-meneses Aprendendo mais sobre fastAPI com o @dunossauro fast_split @thigoap FastAPI com o nome do futuro projeto. fast_sync @edisonmsj First project using fast api FastAPI_do_ZERO @GedeilsonLopes Curso foda demais\u00a0@dunossauro! fast_zero_sync @animarubr Implementa\u00e7\u00e3o do material do curso na plataforma windows dunossauro_fast_api @danielbrito91 Implementa\u00e7\u00e3o do curso fast_zero_sync @marcossa Projeto produzido durante a aula. Aprendendo Python hands-on! fast_zero @FilipeNeiva Muito bom o curso fast_zero @elyssonmr Muito bom aprender ao vivo fast_zero_sync @WilliamCutrim Muito bom fastapi-do-zero @paulinhomacedo Obrigado Edu por sua disposi\u00e7\u00e3o de ensiar. fast_zero_api @peixoto-pc Implementa\u00e7\u00e3o do material do curso sem altera\u00e7\u00f5es fast_do_dunossauro @hebertn88 Projeto desenvolvido durante Curso FastAPI do Zero Toad_list @victorvhs Implementa\u00e7\u00e3o do material do curso sem altera\u00e7\u00f5es fast_zero @josedembo Implementa\u00e7\u00e3o do material do curso sem altera\u00e7\u00f5es curso-fastapi-dunossauro @sigaocaue Implementa\u00e7\u00e3o do material do curso de FastAPI do Dunossauro fast_zero_sync @RRFFTT Meu primeiro projeto, construindo uma API fast_zero @Alan-Gomes1 Implementa\u00e7\u00e3o do material do curso sem altera\u00e7\u00f5es fast_api @PedroP7l Implementa\u00e7\u00e3o do material do curso sem altera\u00e7\u00f5es estudos-fastapi @vizagre Upload da primeira tarefa de aula fastapi-do-zero @gleissonribeiro Projeto desenvolvido durante o curso de fastapi do @dunossauro (Eduardo Mendes) em 2024. fast_zero @cesargodoi muito obrigado pelo empenho e pelo conte\u00fado fastapi_do_zero_dunossauro @CleberNandi fast_zero @alvie40 Excelente did\u00e1tica e estrutura do curso. Obrigado fast_zero @LisandroGuerra Obrigado por este curso excelente! Apredendo tamb\u00e9m a usar o Poetry. fastapi_todo @dubirajara Acompanhando o curso de FastApi do Zero fast_api_zero @IgorStrauss Excelente metodologia, e conceitos muito importantes para o dia a dia na carreira de Dev. fast_zero @divirjo fast_zero @sandrocarval - fast_zero @migueluff - fast_zero @gustaaragao Bem divertido ;) fast_zero @DanielDlc Muito bom, conte\u00fado feito com carinho e intelig\u00eancia. FastAPI_dunossauro @Fernanda-Prado Conte\u00fado excelente, desconhecia o taskipy e quero colocar ele em todo projeto meu to_do_list FrAnKlInSousa - fast_zero @itsGab - Fastapi @kleytonls Muito Obrigado pela dedica\u00e7\u00e3o em fazer um conte\u00fado de t\u00e3o boa qualidade dunossauro FastAPIZero @Leandro-VS Conteudo incr\u00edvel desse curso fast_zero @gabriel19913 Estava a tempos na expectativa por esse curso! T\u00f4 muito ansioso pra aprender coisas novas! fast_zero @marlonato Curso excelente, adorei ver a ideia do ruff e pytest fast_zero @joncasdam Que saudade de lidar com python fast_zero_sync @gabriellcristiann Did\u00e1tica incrivel cara Parab\u00e9ns fast_zero @GuilhermeAndre1 Baita aula boa! full_fast_api @Oseiasdfarias Bom demaizi fast_zero @rbsantosbr Projeto sensacional, aprendizado muito al\u00e9m das tecnologias! fast_zero @CarlosPetrikov Reposit\u00f3rio referente ao curso de FastAPI do Eduardo Mendes fast_zero @Samaelpicoli Aprendendo FastAPI, conte\u00fado sensacional! fast_zero @WesleyPacca Come\u00e7ando FastAPI fastapi_zero @emanoelmendes2 Aprendendo FastAPI fastapi-zero @joaobrc Reposit\u00f3rio do curso de FastAPI fast_api_curso matheuspdf Excelente curso fast_zero @Gui-mp8 Melhor Curso! fast_zero @HulitosCode Fazendo o curso de Fastapi do Zero fast_zero_sync @renatobarramansa Projeto utilizando fastApi fast_zero @renatonaper fast_zero @lidymonteiro Reposit\u00f3rio do curso de FastAPI fast_zero @dgeison Estou utilizando o Windows Subsystem for Linux (WSL) para desenvolver em FastAPI. Valeu pela explica\u00e7\u00e3o, did\u00e1tica, conte\u00fado e material. FastAPI_Lab @tallesemmanuel Por mais que saiba algo, vi que n\u00e3o sei de nada FAST-API Francisco-Libanio Iniciei o projeto estou usando pycharm fast_zero @KrisEgidio Aprendendo FastAPI seguindo o curso FastAPI do Zero! Fast_api_sync @JoaoGBC Aprendendo FastAPI seguindo o curso FastAPI do Zero! fastapi @PedroP7l Implementa\u00e7\u00e3o do material do curso sem altera\u00e7\u00f5es fast_zero_sync @diogogonnelli Implementa\u00e7\u00e3o do material do curso fast_zero_sync @davidrangelrj Implementa\u00e7\u00e3o do material do curso fastapi_project @alsombra Aprendendo fastapi e webdev com a lenda Dunossauro fast_zero @alyssondaniel Implementa\u00e7\u00e3o do material do curso\u00a0COM\u00a0altera\u00e7\u00f5es fast_zero @eduardoalsilva api_master @matheusfly Implementa\u00e7\u00e3o do material do curso sem altera\u00e7\u00f5es fast_zero @MatheusLPolidoro Implementa\u00e7\u00e3o do material do curso sem altera\u00e7\u00f5es running_fast_api @santana98 Repo acompanhando as aulas do curso fastapi-do-zero @thiagosouzalink Excelente curso, parab\u00e9ns! fast_zero_sync @caio-io Minha primeira API fast_zero_sync @giovanezanardo0 Curso top demais fast_zero_sync @Matheus-Novoa Primeiro projeto web com python fast_api_tutorial @Tomas_Tamantini Aprendendo Fast API fast_zero @FariasMi Aprendendo Fast API (dunossauro sou sua f\u00e3) FastOpenDBBrasil @NercinoN21 Uma API Python com FastAPI para descoberta de bases de dados p\u00fablicas do Brasil por tema, ideal para pesquisa e estudos. fast_zero @MuriloRoho fast_zero @flaviacastro - fast_zero @vizagre Tive problemas com o WSL e recriei o projeto do zero no windows. Esse \u00e9 o novo repositorio fast_zero @w1zard Implementa\u00e7\u00e3o do material do curso sem altera\u00e7\u00f5es APIcultura @rmndvngrpslhr Fazendo o curso sozinho foi quando montei uma APIs pela primeira, agora t\u00e1 sendo divertido refazer tudo revendo o conte\u00fado no modo s\u00edncrono o Edu fast_zero_sync @andrescalassara fast_zero Victor Berselli Valeu Eduardo! \ud83e\udd96 festapi Santos Duuu obrigada \ud83e\udd96! my-fastapi @slottwo Implementa\u00e7\u00e3o do material do curso com pequenas altera\u00e7\u00f5es fast_zero @AmeriCodes To perdidinho kk fast_zero @NataGago \u00e9 a tropa do Dunossauro! \ud83e\udd96 fast_zero @Dxm42 Muito obrigado por criar este curso! Estou aprendendo muito. fast_zero_sync @FelipeSantiagoMenezes Estou gostando muito do curso! learning-fastapi @fernandoortolan Implementa\u00e7\u00e3o do material do curso. fast_zero @rafaael1 Aprendendo Fast API, Valeu Eduardo! \ud83e\udd96 fast_zero @felipeCaetano Fazendo o Curso de FastAPI fast_zero @thiagosp Vamos pra cima!!! fast_zero @ssantos89 Aprendendo FastAPI com Dunossauro - First Commit fast_zero_classes @oTerra Projeto criado com base no curso FastAPI do zero do Eduardo Mendes fast_zero @Cmte-Kirk Aspas duplas \u00e9 pra quem tem as duas m\u00e3os! Gostei dessa frase! fast_zero @eduardobrennand Muito bom! \ud83d\ude80\ud83d\ude80\ud83d\ude80 fastapi-dunossauro @gillianoliveira Conte\u00fado nota 100! fast_zero @danweb80 Acompanhamento do Curso FasAPI do Dunossauro fast_api_zero @anselmotaccola fast_zero @epfolletto Curso de FastAPI - Live de Python - Eduardo Mendes fastapi-do-zero-exercicios rg3915 Implementa\u00e7\u00e3o do material do curso sem altera\u00e7\u00f5es. fastapi_zero @devfabiopedro Curso de FastAPI do Dunossauro fastapi_zero @baronetogio Curso de FastAPI fast_zero @thalissonvs Implementa\u00e7\u00e3o do material do curso sem altera\u00e7\u00f5es fast_api_project @guilherme.canfield Obrigado mestre Dunossauro! fastapi_zero @edipolferreira O curso est\u00e1 sensacional! fast_api_zero @brunopmendes Curso t\u00e1 demais (amei a integra\u00e7\u00e3o nativa com o swagger) fast_zero fabiomattes2016 Ta lindo esse curso, continue assim :) fast_zero @tuxanator fastapi, seu lindo. fast_zero @thamibetin Aprendendo mais de Python \ud83d\udc0d com o melhor! \ud83d\udcab fast_zero @washingtonnuness Parab\u00e9ns pelo conte\u00fado e material excelentes! Voc\u00ea \u00e9 demais Duno! FastAPI_Du_Zero @rodten23 Implementa\u00e7\u00e3o do material do curso sem altera\u00e7\u00f5es. Muito Obrigado, Dunossauro! fast_zero @me15degrees Tardei mas n\u00e3o falhei fast_zero @wilsonritt fast_zero @pedronora Curso sensacional fast_zero @Andersonmathema Projeto maravilhoso, espero melhorar muito com esse aprendizado e compartilhar o pouco que sei com a galera fast_zero @BrunoRimbanoJunior Muito Aprendizado, s\u00f3 tenho a agradecer ao professor Eduardo. curso-fastapi-do-zero @mferreiracosta Implementa\u00e7\u00e3o do material do curso sem altera\u00e7\u00f5es fast_zero @ViniNardelli Come\u00e7ando um pouco atrasado, mas aprendendo bastante fastapi_curso @juacy fast_zero @paullosergio Implementa\u00e7\u00e3o do material do curso sem altera\u00e7\u00f5es fast_zero @JordyAraujo Implementa\u00e7\u00e3o do material do curso sem altera\u00e7\u00f5es fast_zero @marfebr implementa\u00e7\u00e3o do material do curso sem altera\u00e7\u00f5es fast_zero @guilopes15 Implementa\u00e7\u00e3o do material do curso estudo_kaoz allandarus Estudos com Fast Api fast_zero @arturpeixoto Expandindo os conhecimentos de back-end com o FastAPI fast_zero @CarlosHenriquePSouza fastapi_duno_curso @LeandroDeJesus-S antes tarde do que mais tarde fastapi_zero_sync @Rafael-Inacio fast_zero @ArthurTZ Inicio do projeto fast_zero_sync @LuizPerciliano Come\u00e7ar \u00e9 importante, terminar \u00e9 melhor ainda! fastapi_learn @viniciusmilk Conhecimento sempre \u00e9 bem-vindo fastapi_curso @alves05 Curso excelente de FastAPI, o melhor que ta tendo! Vlw Eduardo! Fast_Zero_Hero @Lyarkh API desenvolvida com base no Curso de FastAPI trilha_fastAPI @vitoriarntrindade Obrigada por ser t\u00e3o bom pra comunidade Python! fast_zero @Tiago-Verde Obrigado Dunossauro fastapi_do_zero SantosTavares Este reposit\u00f3rio ser\u00e1 utilizado como base para novos projetos fast_zero_july_sync @marceloc4rdoso Esse reposit\u00f3rio \u00e9 destinado a estudos de FastAPI com @dunossauro Fast Notebook Matheus Um ambiente para anota\u00e7\u00f5es do curso. Fast_Api_Sync Braian N Ribeiro Fui descobri o curso quase no final das aulas online mas ainda vai da pra participar de umas 2 aulas valeu Duduzito Curso_FastApi regianemr Aprendendo a usar o Fast Api fast_zer0 xjhfsz Aulas s\u00edncronas fastapi_zero @sandenbergmelo Aprendendo como construir APIs em python com o FastAPI FastAPI @frbelotto Coment\u00e1rio fast_zero @Viniscorza Implementa\u00e7\u00e3o do material do curso - FastApi - Dunossauro FastAPI do Zero Hiroowtf Iniciando o Curso fast_zero @Pedro-hen Aprendendo FastAPI fast_api_zero andre-alves77 Obrigado Eduu fast_zero @williamslews Aulas sincronas fast_zero vitorTheDev Obrigado duno! curso-fastapi Tchez Parab\u00e9ns pelo curso! project_fastapi amandapolari Construindo API em Python utilizando FastAPI fast_zero vgrcontreras Implementa\u00e7\u00e3o do curso FastAPI do Zero! fast_zero @HigorTadeu Reposit\u00f3rio utilizado para os c\u00f3digos em Python do curso com FastAPI fastapi-do-zero @rodfersou Usando Nix no lugar de Pyenv; Scripts to rule then all no lugar do taskipy fast_zero @alexrodriguesdasilva Implementa\u00e7\u00e3o do material do curso sem altera\u00e7\u00f5es fastapi_ai_project @lesampaio Implementando o material do curso + aplica\u00e7\u00e3o de intelig\u00eancia artificial :shipit: fast_zero @rdgr1 Aprendendo FastAPI para Fins Educacionais :) fast_zero hsdanield Acompanhamento do curso fastapi-do-zero fast_zero_sync @amanmdest Bel\u00edssimo curso de FastAPI, me divertindo e aprendendo bastante curso_fast_zero @QuintelaCarvalho Aprendendo Sempre Mais com voc\u00ea Eduardo, Obrigado! fast_zero_sync seu @ Coment\u00e1rio fast_zero @huhero Colombiano \ud83c\udde8\ud83c\uddf4 aprendiendo FastApi fast_zero_classes @luismineo Setup inicial da aula de fastAPI fast_zero @Brugarassis :D fast_api_task @daniloheraclio \ud83c\udf89 fast_zero @DevSchoof Iniciando o curso fastapi-do-zero @heltonteixeira92 To infinity and beyond fast_zero seu @barscka Aprendo python com o melhor fastAPI_do_zero @viniciusCalcantara UaU! fast_zero @balaios Implementa\u00e7\u00e3o do material do curso sem altera\u00e7\u00f5es fastapizero @levyvix Aprendendo FastAPI do Zero python-curso-fastapi @fabiocasadossites Aprendendo FastAPI do Zero fast_zero @henriquesebastiao Muito grato por todo o curso e pela dedica\u00e7\u00e3o Edu. Sensacional! FastAPI_Zero JuniorD-Isael Primeira aula e eu j\u00e1 aprendi um zilh\u00e3o de coisas novas fast_zero_sync_v2 LuizPerciliano Opi! Refazendo aulas em novo projeto, vamos que vamos de muito aprendizado! do_know_fastapi @LucasDatilioCarderelli Correndo para assistir as aulas e fazer os desafios fast_zero @perylemke Aprendendo FastAPI fast_zero_sync vallejoguilherme fast_zero otonielnn Explorando FastApi fast-zero icaroHenrique Aprendendo FastAPI"},{"location":"quizes/aula_01/","title":"01 - Configurando o Ambiente de Desenvolvimento","text":"01 - Configurando o Ambiente de Desenvolvimento 01 - Qual a fun\u00e7\u00e3o do pyenv?Criar e gerenciar ambientes virtuais para bibliotecasInstalar vers\u00f5es diferentes do python no seu ambienteUma alternativa ao pipUma alternativa ao venvEnviar 02 - Qual a fun\u00e7\u00e3o do Poetry?Uma alternativa ao pipGerenciar um projeto pythonTem o mesmo prop\u00f3sito do pyenvEnviar 03 - O que faz o comando \"fastapi dev\"?Cria um ambiente de desenvolvimento para o FastAPIInicia o servidor de produ\u00e7\u00e3o do FastAPIInicia o Uvicorn em modo de desenvolvimentoInstala o FastAPIEnviar 04 - Ao que se refere o endere\u00e7o \"127.0.0.1\"?Ao endere\u00e7o de rede localCaminho de loopbackEndere\u00e7o do FastAPIEnviar 05 - A flag do poetry \"--group dev\" instala os pacotesde produ\u00e7\u00e3ode desenvolvimentode testesEnviar 06 - Qual a fun\u00e7\u00e3o do taskipy?Criar \"atalhos\" para comandos mais simplesFacilitar o manuseio das opera\u00e7\u00f5es de terminalInstalar ferramentas de desenvolvimentoGerenciar o ambiente virtualEnviar 07 - O pytest \u00e9:um linterum formatador de c\u00f3digoframework de testesEnviar 08 - Qual a ordem esperada de execu\u00e7\u00e3o de um teste?arrange, act, assertact, assert, arrangearrange, assert, actEnviar 09 - Dentro do nosso teste, qual a fun\u00e7\u00e3o da chamada na linha em destaque?
def test_root_deve_retornar_ok_e_ola_mundo():\n    client = TestClient(app)\n\n    response = client.get('/')\n\n    assert response.status_code == HTTPStatus.OK\n    assert response.json() == {'message': 'Ol\u00e1 Mundo!'}\n
assert arrange act Enviar 10 - Na cobertura de testes, o que quer dizer \"Stmts\"?Linha cobertas pelo testeLinhas n\u00e3o cobertas pelo testeLinhas de c\u00f3digoEnviar"},{"location":"quizes/aula_02/","title":"02 - Introdu\u00e7\u00e3o ao desenvolvimento WEB","text":""},{"location":"quizes/aula_02/#02-introducao-ao-desenvolvimento-web","title":"02 - Introdu\u00e7\u00e3o ao desenvolvimento WEB","text":"01 - Sobre o modelo cliente servidor, podemos afirmar que:O servidor \u00e9 respons\u00e1vel por servir dados aos clientesO cliente \u00e9 quem consome recursos do servidorA comunica\u00e7\u00e3o \u00e9 iniciada pelo servidorAs respostas s\u00e3o originadas pelo clienteEnviar 02 - Um servidor de aplica\u00e7\u00e3o como uvicorn:pode servir a aplica\u00e7\u00e3o em loopbackpode servir a aplica\u00e7\u00e3o em rede localdeve servir a aplica\u00e7\u00e3o somente em produ\u00e7\u00e3oEnviar 03 - O que significa URL?Uma Rota LocalRotas Locais UnidasLocalizador de Recursos LocaisLocalizador Uniforme de RecursosEnviar 04 - Qual dessas op\u00e7\u00f5es faz parte da URL:ProtocoloEndere\u00e7oHTMLCaminhoPortaVerboEnviar 05 - Qual desses campos n\u00e3o faz parte do cabe\u00e7alho HTTP?ServerContent-TypeCorpoAcceptEnviar 06 - O verbo PUT tem a fun\u00e7\u00e3o de:Solicitar um recursoDeletar um recursoAtualizar um recurso existenteTodas as anterioresEnviar 07 - Respostas com c\u00f3digos da classe 500, significamSucessoErro no servidorInformacionaisErro no clienteEnviar 08 - O c\u00f3digo 200 de resposta significa:FoundAcceptedCreatedOKEnviar 09 - O c\u00f3digo 422 de resposta significa:Not FoundOKUnprocessable EntityBad RequestForbiddenEnviar 10 - Qual a fun\u00e7\u00e3o do pydantic?Validar os dados que saem da APIValidar os dados que entram na APIDocumenta\u00e7\u00e3o autom\u00e1ticaEnviar"},{"location":"quizes/aula_03/","title":"03 - Estruturando o Projeto e Criando Rotas CRUD","text":"03 - Estruturando o Projeto e Criando Rotas CRUD 01 - O m\u00e9todo POST pode ser associado a qual letra da sigla CRUD?UDCREnviar 02 - Quando um recurso \u00e9 criado via POST, qual o Status deve ser retornado para sucesso?200201202301Enviar 03 - Quando um schema n\u00e3o \u00e9 respeitado pelo cliente, qual o status retornado?500404401422Enviar 04 - O FastAPI retorna qual status para quando o servidor n\u00e3o respeita o contrato? UNPROCESSABLE ENTITYI'M A TEAPOTINTERNAL SERVER ERRORNOT IMPLEMENTEDEnviar 05 - O que faz a seguinte fixture
@pytest.fixture\ndef client():\n    return TestClient(app)\n
Faz uma requisi\u00e7\u00e3o a aplica\u00e7\u00e3o Cria um cliente de teste reutiliz\u00e1vel Faz o teste automaticamente Enviar 06 - Qual c\u00f3digo de resposta deve ser enviado quando o recurso requerido n\u00e3o for encontrado?201404401500Enviar 07 - Sobre o relacionamento dos schemas, qual seria a resposta esperada pelo cliente em UserList?
class UserPublic(BaseModel):\n    username: str\n    email: str\n\n\nclass UserList(BaseModel):\n    users: list[UserPublic]\n
{\"users\": {\"username\": \"string\", \"email\": \"e@mail.com\"}} {\"users\": [{\"username\": \"string\", \"email\": \"e@mail.com\"}]} As duas est\u00e3o corretas Enviar 08 - HTTPException tem a fun\u00e7\u00e3o de:Criar um erro de servidorRetornar um erro ao clienteFazer uma valida\u00e7\u00e3o HTTPEnviar 09 - 'users/{user_id}' permite:Parametrizar a URLPedir por um recurso com id espec\u00edficoAumentar a flexibilidade dos endpointsEnviar 10 - Qual a fun\u00e7\u00e3o desse bloco de c\u00f3digo nos endpoints de PUT E DELETE?
if user_id > len(database) or user_id < 1:\n    raise HTTPException(\n        status_code=HTTPStatus.NOT_FOUND, detail='User not found'\n    )\n
Garantir que s\u00f3 sejam chamados id v\u00e1lidos Montar um novo schema do pydantic Criar um erro de servidor Enviar"},{"location":"quizes/aula_04/","title":"04 - Configurando o Banco de Dados e Gerenciando Migra\u00e7\u00f5es com Alembic","text":"04 - Configurando o Banco de Dados e Gerenciando Migra\u00e7\u00f5es com Alembic 01 - Qual a fun\u00e7\u00e3o do sqlalchemy em nosso projeto?Gerenciar a conex\u00e3o com o banco de dadosRepresentar o modelo dos dados como objetosFazer busca de dados no bancoTodas as alternativasEnviar 02 - O Registry do sqlalchemy tem a fun\u00e7\u00e3o de:Criar um schema de valida\u00e7\u00e3o da APICriar um objeto que representa a tabela no banco de dadosCriar um registro no banco de dadosEnviar 03 - Qual a fun\u00e7\u00e3o do objeto Mapperexecutar a fun\u00e7\u00e3o map do python no banco de dadosCriar uma rela\u00e7\u00e3o entre o tipo de dados do python e o da tabela do bancoDizer qual o tipo de dado que ter\u00e1 no banco de dadosFazer uma convers\u00e3o para tipos do pythonEnviar 04 - O que faz o a fun\u00e7\u00e3o mapped_column?Indicar valores padr\u00f5es para as colunasCriar indicadores de SQL no objetoAdiciona restri\u00e7\u00f5es referentes a coluna no banco de dadosTodas as anterioresEnviar 05 - Qual a fun\u00e7\u00e3o do mapped_column no seguinte c\u00f3digo:
quiz: Mapped[str] = mapped_column(unique=True)\n
O valor de quiz deve ser \u00fanico na coluna Este campo \u00e9 o \u00fanico da tabela A tabela s\u00f3 tem um campo S\u00f3 \u00e9 poss\u00edvel inserir um valor \u00fanico nesse campo Enviar 06 - O que significa init=False no mapeamento?Diz que a coluna n\u00e3o deve ser iniciada no bancoToma a responsabilidade do preenchimento do campo para o SQLAlchemyDiz que existe um valor padr\u00e3o na colunaEnviar 07 - O m\u00e9todo \"scalar\" da session tem o objetivo de:
session.scalar(select(User).where(User.username == 'Quiz'))\n
Executar uma query no banco de dados Retornar somente um resultado do banco Converter o resultado da query em um objeto do modelo Todas as alternativas est\u00e3o corretas Enviar 08 - A fun\u00e7\u00e3o \"select\" tem a objetivo de:
session.scalar(select(User).where(User.username == 'Quiz'))\n
Executar uma busca no banco de dados Selecionar objetos 'User' no projeto Montar uma query de SQL Criar um filtro de busca Enviar 09 - Qual o objetivo do arquivo .env?Isolar vari\u00e1veis do ambiente do c\u00f3digo fonteCriar vari\u00e1veis no ambiente virtualCriar vari\u00e1veis globais no projetoEnviar 10 - As migra\u00e7\u00f5es t\u00eam a fun\u00e7\u00e3o de:Refletir as tabelas do banco de dados no ORMCriar tabelas no banco de dadosRefletir as classes do ORM no banco de dadosCriar um banco de dadosEnviar"},{"location":"quizes/aula_05/","title":"05 - Integrando Banco de Dados a API","text":"05 - Integrando Banco de Dados a API 01 - Qual a fun\u00e7\u00e3o de adicionarmos a fun\u00e7\u00e3o \"Depends\" no seguinte c\u00f3digo:
@app.post('/users/', status_code=HTTPStatus.CREATED, response_model=UserPublic)\ndef create_user(\n    user: UserSchema,\n    session: Session = Depends(get_session)\n):\n
Indicar que a fun\u00e7\u00e3o depende de algo Documentar no schema os dados que s\u00e3o requeridos para chamar o endpoint Executar a fun\u00e7\u00e3o 'get_session' e passar seu resultado para fun\u00e7\u00e3o Indicar que a fun\u00e7\u00e3o 'get_session' tem que ser executada antes da 'create_user' Enviar 02 - Sobre a inje\u00e7\u00e3o na fixture podemos afirmar que:
with TestClient(app) as client:\n    app.dependency_overrides[get_session] = get_session_override\n    yield client\n\napp.dependency_overrides.clear()\n
Ela remover\u00e1 depend\u00eancia do c\u00f3digo durante a execu\u00e7\u00e3o do teste Ser\u00e1 feita a sobreescrita de uma dependencia por outra durante o teste A depend\u00eancia 'get_session' ser\u00e1 for\u00e7ada durante o teste Enviar 03 - Essa fixture no banco de dados garante que:
@pytest.fixture\ndef session():\n    engine = create_engine(\n        'sqlite:///:memory:',\n        connect_args={'check_same_thread': False},\n        poolclass=StaticPool,\n    )\n
O banco de dados estar\u00e1 em mem\u00f3ria N\u00e3o ser\u00e1 executada a verifica\u00e7\u00e3o entre a thread do banco e do teste Ser\u00e1 usado um pool de tamanho fixo Criar\u00e1 uma conex\u00e3o com o banco de dados para usar nos testes todas as alternativas anteriores Enviar 04 - Para que o cliente requisite o campo \"limit\" ele deve usar a url:
@app.get('/users/', response_model=UserList)\ndef read_users(\n    skip: int = 0, limit: int = 100, session: Session = Depends(get_session)\n):\n
/users/?limit=10 /users/limit/10 /users/limit=10? /users/&limit=10 Enviar 05 - Quais os padr\u00f5es de projeto implementados pela Session?Reposit\u00f3rioUnidade de trabalhoCompositeProxyEnviar 06 - O que faz o m\u00e9todo session.commit()?Faz um commit no gitPersiste os dados no banco de dadosExecuta as transa\u00e7\u00f5es na sess\u00e3oAbre uma conex\u00e3o com o banco de dadosEnviar 07 - O que faz o m\u00e9todo session.refresh(obj)?Atualiza a conex\u00e3o com o banco de dadosAtualiza dos dados da sess\u00e3oSincroniza o objeto do ORM com o banco de dadosSincroniza a sess\u00e3o com o banco de dadosEnviar 08 - O que o \"|\" siginifica na query?
session.scalar(\n    select(User).where(\n        (User.username == user.username) | (User.email == user.email)\n    )\n)\n
user.username 'E' user.email user.username 'SEM' user.email user.username 'OU' user.email user.username 'COM' user.email Enviar 09 - Quando usamos o m\u00e9todo 'model_validate' de um schema do Pydantic estamos:Validando um JSON com o modeloValidando um request com o modeloConverte um objeto em um schemaCoverte um objeto em JSONEnviar 10 - Quando usamos 'model_config' em um schema do Pydantic estamos:Alterando o comportamento de 'model_validate'Adicionando mais um campo de valida\u00e7\u00e3oAlterando a estrutura do modeloEnviar"},{"location":"quizes/aula_06/","title":"06 - Autentica\u00e7\u00e3o e Autoriza\u00e7\u00e3o com JWT","text":"06 - Autentica\u00e7\u00e3o e Autoriza\u00e7\u00e3o com JWT 01 - Qual a fun\u00e7\u00e3o da 'pwdlib' em nosso projeto?Criar um hash da senhaVerificar se o texto limpo bate com o texto sujoSalvar as senhas em texto limpoFazer valida\u00e7\u00e3o das senhasEnviar 02 - Qual a necessidade de adicionar a linha em destaque no endpoint de PUT?
@app.put('/users/{user_id}', response_model=UserPublic)\n# ...\n    db_user.username = user.username\n    db_user.password = get_password_hash(user.password)\n    db_user.email = user.email\n
Validar a senha no momento do update Criar o hash da senha durante a atualiza\u00e7\u00e3o Pegar a senha antiga durante o update Salvar a senha de forma limpa no banco de dados Enviar 03 - Qual o prop\u00f3sito da autentica\u00e7\u00e3o?Fornecer um mecanismo de tokensValidar que o cliente \u00e9 quem diz serGarantir que s\u00f3 pessoas autorizadas possam executar a\u00e7\u00f5esEnviar 04 - Qual a fun\u00e7\u00e3o do endpoint '/token'?Gerenciar a autoriza\u00e7\u00e3o do clienteFazer a autentica\u00e7\u00e3o do clienteRenovar o token JWTTodas as respostas est\u00e3o corretasEnviar 05 - O 'OAuth2PasswordRequestForm' fornece:Um formul\u00e1rio de cadastroUm formul\u00e1rio de autentica\u00e7\u00e3oUm formul\u00e1rio de autoriza\u00e7\u00e3oUm formul\u00e1rio de altera\u00e7\u00e3o de registroEnviar 06 - Qual a fun\u00e7\u00e3o do token JWT?Fornecer informa\u00e7\u00f5es sobre o cliente para o servidorGerenciar o tempo de validade do tokenCarregar dados sobre autoriza\u00e7\u00e3oTodas as respostas est\u00e3o corretasEnviar 07 - Qual o objetivo da claim 'sub'?Guardar o tempo de validade do tokenIdentificar o servidor que gerou o tokenIdentificar qual cliente gerou o tokenIdentificar o email do clienteEnviar 08 - Qual a fun\u00e7\u00e3o da 'secret_key'?Usar como base para criptografar a senha do cliente com Argon2Usar como base para gera\u00e7\u00e3o do HTTPSUsar como base para assinar o Token com HS256Enviar 09 - Qual o objetivo da fun\u00e7\u00e3o 'current_user'?Gerenciar a autentica\u00e7\u00e3o dos clientesValidar o token JWTGerenciar a autoriza\u00e7\u00e3o dos endpointsSaber que \u00e9 o usu\u00e1rio logadoEnviar"},{"location":"quizes/aula_07/","title":"07 - Refatorando a Estrutura do Projeto","text":"07 - Refatorando a Estrutura do Projeto 01 - Quais s\u00e3o as fun\u00e7\u00f5es do \"Router\" do FastAPICriar uma \"sub-aplica\u00e7\u00e3o\"Isolar endpoints por dom\u00ednioFacilitar a manuten\u00e7\u00e3o do c\u00f3digoMelhorando o desempenho da aplica\u00e7\u00e3oEnviar 02 - Sobre o par\u00e2metro \"prefix\" do router, podemos afirmar que:Adicionar os endpoints no roteadorFazer as chamadas unificadas do enpoint Padronizar um prefixo para N endpointsEnviar 03 - Qual a fun\u00e7\u00e3o do par\u00e2metro 'tag' nos routers?Dizer quais par\u00e2metros devem ser passados ao endpointsColocar um prefixo nos endpointsAgrupar os endpoins do mesmo dom\u00ednio na documenta\u00e7\u00e3oAdicionar cores diferentes no swaggerEnviar 04 - Qual a fun\u00e7\u00e3o do tipo \"Annotated\" no FastAPI!Reduzir o tamanho das fun\u00e7\u00f5esReutilizar anota\u00e7\u00f5es em N endpointsAtribuir metadados aos tiposTodas as alternativasEnviar 05 - O que o \"Annotated\" faz nesse c\u00f3digo?
@app.put('/users/{user_id}', response_model=UserPublic)\ndef endpoint(session: Annotated[Session, Depends(get_session)])\n
Diz que o par\u00e2metro 'session' \u00e9 do tipo 'Session' e depende de 'get_session' Diz que o par\u00e2metro 'session' \u00e9 do tipo 'Annotated' Faz a troca de 'session' por 'Session' Enviar"},{"location":"quizes/aula_08/","title":"08 - Tornando o sistema de autentica\u00e7\u00e3o robusto","text":"08 - Tornando o sistema de autentica\u00e7\u00e3o robusto 01 - Sobre o Factory-boy. O que siginifica a classe Meta?
class UserFactory(factory.Factory):\n    class Meta:\n        model = User\n
Diz que ser\u00e1 usada uma metaclasse Explica ao Factory qual objeto ele deve se basear Extente a classe Factory com os par\u00e2metros de Meta Enviar 02 - Ainda sobre o Factory-boy. O que siginifica \"factory.Sequence\"?Criar\u00e1 uma sequ\u00eancia de MetasAdicionar\u00e1 +1 em cada objeto criadoMonta uma sequ\u00eancia de objetosCria um novo objeto do factoryEnviar 03 - Ainda sobre o Factory-boy. O que siginifica \"factory.LazyAttribute\"?Diz que o atributo ser\u00e1 criado em tempo de execu\u00e7\u00e3oDiz que o atributo usar\u00e1 outros atributos para ser inicializadoUsa outros campos para ser compostoCria um atributo independ\u00eanteEnviar 04 - O que faz o gerenciador de contexto \"freeze_time\"?
with freeze_time('2023-07-14 12:00:00'):\n    response = client.post(\n        '/auth/token',\n        data={'username': user.email, 'password': user.clean_password},\n    )\n
Pausa o tempo nas instru\u00e7\u00f5es dentro do 'with' Congela o tempo da fun\u00e7\u00e3o toda Muda a hora do computador para um bloco Enviar 05 - O que levanta o erro \"ExpiredSignatureError\"?Quando deu erro no valor de 'exp'Quando n\u00e3o deu certo avaliar a claim de 'exp'Quando a claim de 'exp' tem um tempo expiradoEnviar"},{"location":"quizes/aula_09/","title":"09 - Criando Rotas CRUD para Gerenciamento de Tarefas em FastAPI","text":"09 - Criando Rotas CRUD para Gerenciamento de Tarefas em FastAPI 01 - Qual o papel da classe 'TodoState' em nosso c\u00f3digo?Fornece m\u00e9todos para gerar IDs sequenciais para tarefas.Armazena o hist\u00f3rico de altera\u00e7\u00f5es das tarefas ao longo do tempo.Permite a cria\u00e7\u00e3o de tarefas com diferentes n\u00edveis de prioridade.Tipo com valores nomeados e constantes para representar estados de tarefas.Enviar A classe `TodoState` define estados de tarefas (rascunho, pendente, etc.) com nomes claros, facilitando o c\u00f3digo e garantindo seguran\u00e7a e agilidade na manuten\u00e7\u00e3o. 02 - Qual o significado da rela\u00e7\u00e3o `user: Mapped[User] = relationship(...)` em nosso modelo?Define conex\u00e3o entre usu\u00e1rios e tarefas (N:N) com lista inicializada e acesso bidirecional.Estabelece rela\u00e7\u00e3o entre usu\u00e1rios e tarefas (1:N) com lista n\u00e3o inicializada e acesso bidirecional.Cria v\u00ednculo entre usu\u00e1rios e tarefas (1:1) com lista n\u00e3o inicializada e acesso unidirecional.Estabelece rela\u00e7\u00e3o entre usu\u00e1rios e tarefas (1:N) com lista inicializada e acesso unidirecional.Enviar A rela\u00e7\u00e3o `user: Mapped[User] = relationship(...)` define uma conex\u00e3o 1:N entre usu\u00e1rios e tarefas, permitindo que um usu\u00e1rio tenha v\u00e1rias tarefas e cada tarefa esteja ligada a um \u00fanico usu\u00e1rio. A lista de tarefas n\u00e3o \u00e9 inicializada automaticamente e as entidades podem se acessar mutuamente. 03 - Qual o significado do par\u00e2metro de consulta `state: str | None = None` no endpoint de busca?Permite filtrar resultados por valor de string obrigat\u00f3rio ('state'), n\u00e3o aceitando None.Filtra resultados por estado ('state'), com valor padr\u00e3o 'pendente' se n\u00e3o especificado.Habilita filtro por 'state' (string ou None), com valor padr\u00e3o None se n\u00e3o especificado.Cria um par\u00e2metro opcional 'state' que recebe floats para filtrar resultados.Enviar O par\u00e2metro `state: str | None = None` no FastAPI permite filtrar resultados por um valor de string opcional ('state'), que pode ser None por padr\u00e3o. 04 - Qual a fun\u00e7\u00e3o do `FuzzyChoice` no Factory Boy?Gera dados de teste aleat\u00f3rios e realistas, facilitando a cria\u00e7\u00e3o de testes de unidade robustos.Gera valores aleat\u00f3rios para cada atributo de um objeto de teste, facilitando a cria\u00e7\u00e3o de testes.Cria objetos de teste com valores predefinidos a partir de um conjunto de op\u00e7\u00f5es.Permite a gera\u00e7\u00e3o de dados de teste aleat\u00f3rios e realistas para diferentes tipos de dados.Enviar O `FuzzyChoice` do Factory Boy gera valores aleat\u00f3rios a partir de um conjunto predefinido, criando objetos de teste com dados realistas e facilitando testes de unidade robustos. 05 - Por qual raz\u00e3o usamos `# noqa` no endpoint `list_todos`:Para dizer aos QAs que esse c\u00f3digo n\u00e3o \u00e9 pra eles.Para dizer que esse c\u00f3digo n\u00e3o ser\u00e1 coberto por testes.Para remover a checagem no linter na express\u00e3o.Enviar 06 - Qual a fun\u00e7\u00e3o do `session.bulk_save_objects` nos testes de todo?Inserir uma lista de objetos na sessionSalvar diversos objetos de uma vez no banco de dadosEnviar 07 - Qual a fun\u00e7\u00e3o do `exclude_unset=True` no c\u00f3digo abaixo?
@router.patch('/{todo_id}', response_model=TodoPublic)\ndef patch_todo(\n    todo_id: int, session: Session, user: CurrentUser, todo: TodoUpdate\n):\n    # ...\n    for key, value in todo.model_dump(exclude_unset=True).items():\n        setattr(db_todo, key, value)\n
Exclui os valores que n\u00e3o fazem parte do schema Exclui os valores que n\u00e3o foram passados para o schema Exclui os valores que s\u00e3o None no schema Enviar"},{"location":"quizes/aula_10/","title":"10 - Dockerizando a nossa aplica\u00e7\u00e3o e introduzindo o PostgreSQL","text":""},{"location":"quizes/aula_10/#10-dockerizando-a-nossa-aplicacao-e-introduzindo-o-postgresql","title":"10 - Dockerizando a nossa aplica\u00e7\u00e3o e introduzindo o PostgreSQL","text":"01 - Qual a fun\u00e7\u00e3o do arquivo `compose.yaml`?Subir a aplica\u00e7\u00e3o de forma simplesEspecificar os servi\u00e7os e como eles se relacionamSubstituir o DockerfileCriar uma container dockerEnviar 02 - Qual instru\u00e7\u00e3o do Dockerfile o `entrypoint` substitui?O comando de execu\u00e7\u00e3o (CMD)A defini\u00e7\u00e3o da imagem base (FROM)A exposi\u00e7\u00e3o das portas (EXPOSE)Enviar 03 - O que quer dizer escopo nas fixtures?Em quais testes elas v\u00e3o atuarSe um m\u00f3dulo pode usar aquela fixtureQual a dura\u00e7\u00e3o da fixtureCapturar as vari\u00e1veis de ambienteEnviar 04 - Por que usamos o escopo de \"session\" na fixture?Pra dizer que ela vai substituir a fixture de sessionCriar uma sess\u00e3o do cliente com o banco de dadosDizer que a fixture tem a dura\u00e7\u00e3o de um testeDizer que a fixture ser\u00e1 executada uma \u00fanica vez durante os testesEnviar 05 - Para que serve o volume no docker?Para armazenar as imagens geradasPara adicionar um banco de dadosPara armazenar o cache do dockerPara persistir arquivos na m\u00e1quina hostEnviar 06 - O que faz a flag `-it` no CLI do docker?Conecta o container na internetRoda o container no modo interativoConfigura a rede do dockerPassa as vari\u00e1veis de ambienteEnviar 07 - Por que precisamos usar o TestContainers no projeto?Para executar os testes dentro de containersPara testar os containers da aplica\u00e7\u00e3oPara criar imagens durante o testePara iniciar containers durante o testeEnviar"},{"location":"quizes/aula_11/","title":"11 - Automatizando os testes com Integra\u00e7\u00e3o Cont\u00ednua (CI)","text":""},{"location":"quizes/aula_11/#11-automatizando-os-testes-com-integracao-continua-ci","title":"11 - Automatizando os testes com Integra\u00e7\u00e3o Cont\u00ednua (CI)","text":"01 - Qual a fun\u00e7\u00e3o da integra\u00e7\u00e3o cont\u00ednua?Proibir que c\u00f3digo que n\u00e3o funciona seja commitadoVerificar se a integra\u00e7\u00e3o das altera\u00e7\u00f5es foi bem sucedidaImpedir que pessoas de fora integrem c\u00f3digo em nosso reposit\u00f3rioIntegrar novos commits ao reposit\u00f3rioEnviar 02 - O que \u00e9 o Github Actions?Uma aplica\u00e7\u00e3o que executa os testes localmenteUm test runner como o pytestForma de integrar o github com outras aplica\u00e7\u00f5esUm servi\u00e7o do github para CIEnviar 03 - O que \u00e9 um workflow de CI?Uma lista de passos que o CI deve executarUma automa\u00e7\u00e3o executada sempre c\u00f3digo \u00e9 adicionado ao resposit\u00f3rioUma forma de versionar software como o gitPassos que ser\u00e3o executados antes do commitEnviar 04 - Quando o nosso trigger de CI \u00e9 ativado?Sempre que fazemos um pushSempre que criamos um pull requestSempre que um commit \u00e9 feitoSempre que uma issue \u00e9 abertaEnviar 05 - Nos steps, o que quer dizer \"uses\"?Diz que vamos usar uma action prontaDiz que vamos executar uma instru\u00e7\u00e3o de shellQue vamos fazer a instala\u00e7\u00e3o de um componente no workflowFazer checkout do c\u00f3digo do reposit\u00f3rioEnviar 06 - Nos steps, o que quer dizer \"run\"?Que vamos usar uma action pronta do githubServe para dizer que vamos usar um passoDefinir uma vari\u00e1vel de ambienteDiz que vamos executar uma instru\u00e7\u00e3o de shellEnviar 07 - Qual a fun\u00e7\u00e3o das \"secrets\" no arquivo yaml?Criar vari\u00e1veis de ambienteN\u00e3o expor dados sens\u00edveis no arquivo de ciSubstituir vari\u00e1veis \u200b\u200bcom valores din\u00e2micosOrganizar o c\u00f3digo YAMLEnviar"},{"location":"quizes/aula_12/","title":"12 - Fazendo deploy no Fly.io","text":""},{"location":"quizes/aula_12/#12-fazendo-deploy-no-flyio","title":"12 - Fazendo deploy no Fly.io","text":"01 - O que \u00e9 fazer \"deploy\"?Colocar a aplica\u00e7\u00e3o em produ\u00e7\u00e3oExecutar os testes da aplica\u00e7\u00e3oExecutar a aplica\u00e7\u00e3o localmenteFazer o processo de integra\u00e7\u00e3o cont\u00ednuaEnviar 02 - O quer dizer \"PaaS\"?Software como servi\u00e7oUm local para subir a aplica\u00e7\u00e3oSoftwares como o githubPlataforma como servi\u00e7oEnviar 03 - O que \u00e9 o Fly.io?Uma plataforma de c\u00f3digoUma plataforma de versionamentoUma plataforma de CloudUma plataforma de integra\u00e7\u00e3o cont\u00ednuaEnviar 04 - Para que usamos o \"flyctl\"?Para fazer o login no flyPara nos comunicarmos com o fly via terminalPara fazer deploy da aplica\u00e7\u00e3oPara fazer o build do containerEnviar"}]} \ No newline at end of file diff --git a/sitemap.xml b/sitemap.xml index a619ce88..3e61c056 100644 --- a/sitemap.xml +++ b/sitemap.xml @@ -2,154 +2,154 @@ https://fastapidozero.dunossauro.com/ - 2024-09-07 + 2024-09-19 https://fastapidozero.dunossauro.com/01/ - 2024-09-07 + 2024-09-19 https://fastapidozero.dunossauro.com/02/ - 2024-09-07 + 2024-09-19 https://fastapidozero.dunossauro.com/03/ - 2024-09-07 + 2024-09-19 https://fastapidozero.dunossauro.com/04/ - 2024-09-07 + 2024-09-19 https://fastapidozero.dunossauro.com/05/ - 2024-09-07 + 2024-09-19 https://fastapidozero.dunossauro.com/06/ - 2024-09-07 + 2024-09-19 https://fastapidozero.dunossauro.com/07/ - 2024-09-07 + 2024-09-19 https://fastapidozero.dunossauro.com/08/ - 2024-09-07 + 2024-09-19 https://fastapidozero.dunossauro.com/09/ - 2024-09-07 + 2024-09-19 https://fastapidozero.dunossauro.com/10/ - 2024-09-07 + 2024-09-19 https://fastapidozero.dunossauro.com/11/ - 2024-09-07 + 2024-09-19 https://fastapidozero.dunossauro.com/12/ - 2024-09-07 + 2024-09-19 https://fastapidozero.dunossauro.com/13/ - 2024-09-07 + 2024-09-19 https://fastapidozero.dunossauro.com/14/ - 2024-09-07 + 2024-09-19 https://fastapidozero.dunossauro.com/apendices/a_instalacoes/ - 2024-09-07 + 2024-09-19 https://fastapidozero.dunossauro.com/apendices/b_proximos_passos/ - 2024-09-07 + 2024-09-19 https://fastapidozero.dunossauro.com/aulas/sincronas/ - 2024-09-07 + 2024-09-19 https://fastapidozero.dunossauro.com/exercicios_resolvidos/aula_01/ - 2024-09-07 + 2024-09-19 https://fastapidozero.dunossauro.com/exercicios_resolvidos/aula_02/ - 2024-09-07 + 2024-09-19 https://fastapidozero.dunossauro.com/exercicios_resolvidos/aula_03/ - 2024-09-07 + 2024-09-19 https://fastapidozero.dunossauro.com/exercicios_resolvidos/aula_04/ - 2024-09-07 + 2024-09-19 https://fastapidozero.dunossauro.com/exercicios_resolvidos/aula_05/ - 2024-09-07 + 2024-09-19 https://fastapidozero.dunossauro.com/exercicios_resolvidos/aula_06/ - 2024-09-07 + 2024-09-19 https://fastapidozero.dunossauro.com/projetos/projetos_finais/ - 2024-09-07 + 2024-09-19 https://fastapidozero.dunossauro.com/projetos/repositorios/ - 2024-09-07 + 2024-09-19 https://fastapidozero.dunossauro.com/quizes/aula_01/ - 2024-09-07 + 2024-09-19 https://fastapidozero.dunossauro.com/quizes/aula_02/ - 2024-09-07 + 2024-09-19 https://fastapidozero.dunossauro.com/quizes/aula_03/ - 2024-09-07 + 2024-09-19 https://fastapidozero.dunossauro.com/quizes/aula_04/ - 2024-09-07 + 2024-09-19 https://fastapidozero.dunossauro.com/quizes/aula_05/ - 2024-09-07 + 2024-09-19 https://fastapidozero.dunossauro.com/quizes/aula_06/ - 2024-09-07 + 2024-09-19 https://fastapidozero.dunossauro.com/quizes/aula_07/ - 2024-09-07 + 2024-09-19 https://fastapidozero.dunossauro.com/quizes/aula_08/ - 2024-09-07 + 2024-09-19 https://fastapidozero.dunossauro.com/quizes/aula_09/ - 2024-09-07 + 2024-09-19 https://fastapidozero.dunossauro.com/quizes/aula_10/ - 2024-09-07 + 2024-09-19 https://fastapidozero.dunossauro.com/quizes/aula_11/ - 2024-09-07 + 2024-09-19 https://fastapidozero.dunossauro.com/quizes/aula_12/ - 2024-09-07 + 2024-09-19 \ No newline at end of file diff --git a/sitemap.xml.gz b/sitemap.xml.gz index 605aceea8f2775f9efa38129ce1cf9d3456fb789..eaeab40e9d76281bca066f44054c05359cad68dc 100644 GIT binary patch delta 316 zcmV-C0mJ@|1C9eEABzYGfNASy0{?SqbY*Q}a4vXlYyi!dzixvt5Qq0X1!4zdkt8q! z^E8W*S22GqOUt;PMSffx+-B3LI*|3X3;H%YfrGHRg^O~Kwxa<=G5V>5o>lBx`twg; z-9uaFSzaVrneft=-1lxM_Rab;HRDv^oC-Xr0vA-^1r>Nn1zu5s*Hqw=2+T7oFjaDK zs^sES$;GLXOIT%EEfq4+XyXezA;GVxZQdH|;H-apz{UzhATt`f=@Me310$#`)LA*H zrS_-c6he3ObvU^-WY>$|obmvj<^eht0Xi)LbfUWD_zZeigV*?)G&^mCMrs|WEck~J zI87FeAEUk~>I{02$U~5rhagdeAh8HRVi|(ODg=pj2ohxg5@gS*2Z<08^FPn5>4R#h OKLH3L60vah6951xbd3)H delta 316 zcmV-C0mJ@|1C9eEABzYGfS2250{?SqbY*Q}a4vXlYyi!dzixvt5Qq0X1!4yqkt8q! zXDN@7S22H#r)6BvB0sJTZnJ4r9mx9H1$~>Hz(H8u!bLeq+tC1`82wa2&nk8;{rRV_ z?xC%7o)-x(6Tb2#_q`j6eY5^du_>w$gUT^IpqO5%>#5Q0(4pg=tOnP@fq~42CwloX?EHOjnq0$S?~`d zaGER_KSq5~)EV?3k%u5L4?&^`L1Gbt#4-elRR|L65G2X~B*>mq4-z3H=6{}9(+Aa1 Oe*y?V_6M}~6953~