diff --git a/01/index.html b/01/index.html index 1b7f75ce..da238811 100644 --- a/01/index.html +++ b/01/index.html @@ -446,6 +446,40 @@ + + +
  • + + Estrutura de um teste + + + +
  • @@ -776,6 +810,40 @@ +
  • + +
  • + + Estrutura de um teste + + + +
  • @@ -1235,14 +1303,14 @@

    Escrevendo o teste

    10
    from fastapi.testclient import TestClient
     from fast_zero.app import app
     
    -client = TestClient(app)
    -
    +def test_root_deve_retornar_200_e_ola_mundo():
    +    client = TestClient(app)
     
    -def test_root_deve_retornar_200_e_ola_mundo():
    -    response = client.get('/')
    -    assert response.status_code == 200
    -    assert response.json() == {'message': 'Olá Mundo!'}
    -
    + response = client.get('/') + + assert response.status_code == 200 + assert response.json() == {'message': 'Olá Mundo!'} +

    Esse teste faz uma requisição GET no endpoint / e verifica se o código de status da resposta é 200 e se o conteúdo da resposta é {'message': 'Olá Mundo!'}.

    $ Execução no terminal!
    task test
     # parte da mensagem foi omitida
    @@ -1264,14 +1332,52 @@ 

    Escrevendo o teste

    Dessa forma, temos um teste que coletou 1 item (1 teste). Esse teste foi aprovado e a cobertura não deixou de abranger nenhuma linha de código.

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

    descrição

    +

    Estrutura de um teste

    +

    Agora que escrevemos nosso teste de forma intuitiva, podemos entender o que cada passo do teste faz. Essa compreensão é vital, pois pode nos ajudar a escrever testes no futuro com mais confiança e eficácia. Para desvendar o método por trás da nossa abordagem, vamos explorar a estratégia conhecida como AAA, que divide o teste em três fases distintas: Arrange, Act, Assert.

    +
    +Caso fazer testes ainda seja complicado para você +

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

    +

    Link direto

    +
    +

    Vamos pegar esse teste que fizemos e entender os passos que fizemos para conseguir testar esse endpoint:

    +
    tests/test_app.py
     1
    + 2
    + 3
    + 4
    + 5
    + 6
    + 7
    + 8
    + 9
    +10
    from fastapi.testclient import TestClient
    +from fast_zero.app import app
    +
    +def test_root_deve_retornar_200_e_ola_mundo():
    +    client = TestClient(app)  # Arrange
    +
    +    response = client.get('/')  # Act
    +
    +    assert response.status_code == 200  # Assert
    +    assert response.json() == {'message': 'Olá Mundo!'}  # Assert
    +
    +

    Com base nesse código, podemos observar as três fases:

    +

    Fase 1 - Organizar (Arrange)

    +

    Nesta primeira etapa, estamos preparando o ambiente para o teste. No exemplo, a linha com o comentário Arrange não é 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ção ao app.

    +

    Fase 2 - Agir (Act)

    +

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

    +

    Fase 3 - Afirmar (Assert)

    +

    Esta é a etapa de verificar se tudo correu como esperado. É fácil notar onde estamos fazendo a verificação, pois essa linha sempre tem a palavra reservada assert. A verificação é booleana, ou está correta ou não está. Por isso, um teste deve sempre incluir um assert para verificar se o comportamento esperado está correto.

    +
    +

    Agora que compreendemos o que cada linha de teste faz em específico, podemos nos orientar de forma clara nos testes que escreveremos no futuro. Cada uma das linhas usadas tem uma razão de estar no teste, e conhecer essa estrutura não só nos dá uma compreensão mais profunda do que estamos fazendo, mas também nos dá confiança para explorar e escrever testes mais complexos.

    Criando nosso repositório no git

    Antes de concluirmos a aula, precisamos criar nosso repositório no git e criar nosso arquivo .gitignore:

    -
    $ Execução no terminal!
    ignr -p python > .gitignore
    -git init .
    -gh repo create
    -git add .
    -git commit -m "Configuração inicial do projeto"
    -git push
    +
    $ Execução no terminal!
    ignr -p python > .gitignore
    +git init .
    +gh repo create
    +git add .
    +git commit -m "Configuração inicial do projeto"
    +git push
     

    Conclusão

    Pronto! Agora temos um ambiente de desenvolvimento totalmente configurado para começar a trabalhar com FastAPI e já fizemos nossa primeira imersão no Desenvolvimento Orientado por Testes. Na próxima aula, vamos aprofundar na estruturação da nossa aplicação FastAPI. Até lá!

    @@ -1281,7 +1387,7 @@

    Conclusão

    Última atualização: - August 1, 2023 + August 3, 2023 diff --git a/02/index.html b/02/index.html index fe93db73..5ffac0db 100644 --- a/02/index.html +++ b/02/index.html @@ -114,7 +114,7 @@
    - + Pular para conteúdo @@ -927,7 +927,11 @@ -

    Estruturando o Projeto e Criando Rotas CRUD

    +

    [REV] Estruturando o Projeto e Criando Rotas CRUD

    +

    Revisão:

    +
      +
    • Introduzir a fixture para o client nessa aula (no segundo teste (o do get)
    • +

    Objetivos dessa aula:

      @@ -1363,7 +1367,7 @@

      Conclusão

      - Authors: dunossauro, Julio Formiga, Ricardo Emanuel, Bruna Ferreira + Authors: dunossauro, Julio Formiga, Ricardo Emanuel, Not Committed Yet, Bruna Ferreira
      diff --git a/search/search_index.json b/search/search_index.json index 42b4d2c0..9fffa784 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":"#wip-fastapi-do-zero","title":"[WIP] FastAPI do ZERO","text":"

      Esse material ainda est\u00e1 em fase de desenvolvimento. Caso encontre algum erro, ficarei extremamente feliz que voc\u00ea me notifique ou envie um Pull Request!

      Construindo um Projeto com Bancos de Dados, Testes e Deploy

      Boas vindas \u00e0 jornada de aprendizado mais empolgante que voc\u00ea vai embarcar: \"FastAPI do Zero: Criando um Projeto com Bancos de Dados, Testes e Deploy\". Este curso tem como objetivo oferecer um entendimento pr\u00e1tico do framework FastAPI, proporcionando as habilidades essenciais para o desenvolvimento eficiente de APIs. Como integra\u00e7\u00e3o com banco de dados e como fazer testes!

      "},{"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 alto desempenho com anota\u00e7\u00f5es de tipo Python facilita o desenvolvimento de APIs RESTful.

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

      Este curso visa oferecer uma experi\u00eancia pr\u00e1tica completa, desde a configura\u00e7\u00e3o do ambiente de desenvolvimento at\u00e9 o deploy de uma aplica\u00e7\u00e3o FastAPI completa. O objetivo \u00e9 que, ao final do curso, voc\u00ea tenha uma compreens\u00e3o profunda do uso do FastAPI e das melhores pr\u00e1ticas associadas.

      Para isso, construiremos um projeto pr\u00e1tico de gerenciamento de tarefas (uma lista de tarefas) do zero, incluindo a autentica\u00e7\u00e3o do usu\u00e1rio e opera\u00e7\u00f5es CRUD completas. Nesse projeto, faremos uso das ferramentas mais recentes dispon\u00edveis em 2023, incluindo a vers\u00e3o 0.100 do FastAPI, a vers\u00e3o 2.0 do Pydantic e a vers\u00e3o 2.0 do SQLAlchemy ORM. Tamb\u00e9m usaremos o Python 3.11 e o Alembic para gerenciamento de migra\u00e7\u00f5es.

      Este curso tamb\u00e9m d\u00e1 \u00eanfase \u00e0 pr\u00e1tica de testes, utilizando o pytest para garantir que as APIs que constru\u00edmos sejam 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 vamos abordar neste curso:

      1. Configurando um ambiente de desenvolvimento para FastAPI: Vamos come\u00e7ar do absoluto zero, criando e configurando nosso ambiente de desenvolvimento.

      2. Primeiros Passos com FastAPI e TDD: Depois de 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 um outro n\u00edvel.

      4. Autentica\u00e7\u00e3o e Autoriza\u00e7\u00e3o em FastAPI: Vamos construir 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.

      6. Dockerizando e Fazendo Deploy de sua Aplica\u00e7\u00e3o FastAPI: Por fim, vamos aprender 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 est\u00e1 em fase de desenvolvimento e todas as aulas estar\u00e3o dispon\u00edveis no meu canal do YouTube. Voc\u00ea pode conferir outros materiais dispon\u00edveis por l\u00e1 enquanto os v\u00eddeos n\u00e3o saem, ou se inscrever para ser notificado quando os v\u00eddeos sa\u00edrem!

      http://youtube.com/@dunossauro

      Aqui estar\u00e1 listada a playlist quando dispon\u00edvel!

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

      Para aproveitar ao m\u00e1ximo este curso, \u00e9 recomendado que voc\u00ea tenha algum conhecimento pr\u00e9vio de Python. Al\u00e9m disso, algum entendimento b\u00e1sico de desenvolvimento web e APIs RESTful ser\u00e1 \u00fatil, mas n\u00e3o essencial, pois {dizer que a ideia \u00e9 fazer um projeto com foco pr\u00e1tico, ent\u00e3o voc\u00ea pode entender como as coisas s\u00e3o constru\u00eddas at\u00e9 o fim do processo!}

      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. Estruturando seu Projeto e Criando Rotas CRUD
      3. Configurando Banco de Dados e Gerenciando Migra\u00e7\u00f5es com Alembic
      4. Integrando Banco de Dados a API
      5. Autentica\u00e7\u00e3o e Autoriza\u00e7\u00e3o
      6. Refatorando a Estrutura do Projeto
      7. Tornando o sistema de autentica\u00e7\u00e3o robusto
      8. Criando Rotas CRUD para Tarefas
      9. Dockerizando a aplica\u00e7\u00e3o
      10. Automatizando os testes com integra\u00e7\u00e3o cont\u00ednua
      11. Fazendo o deploy no fly.io
      12. Despedida
      "},{"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.

      Eu sou um programador Python muito empolgado e curioso. Toco um projeto pessoal chamado Live de Python a pouco mais de 6 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":"#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 quer dizer que:

      • Voc\u00ea pode copiar e reproduzir esse material em qualquer meio e em qualquer formato;
      • Voc\u00ea pode adaptar esse material e construir outros materiais usando esse material.

      Pontos de aten\u00e7\u00e3o:

      • Voc\u00ea precisa dar os devidos cr\u00e9ditos a esse material onde for usar ou adaptar;
      • Voc\u00ea n\u00e3o pode usar para fins comerciais. Como vender ou usar para obter vantagens comerciais;
      • Todo o material derivado desse material deve ser redistribu\u00eddo com a licen\u00e7a CC BY-NC-SA.
      "},{"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:

      • MkDocs: Para gera\u00e7\u00e3o das p\u00e1ginas est\u00e1ticas usando Markdown
      • Mkdocs-material: Tema para o MKDocs
      • pymdown-extensions: Extens\u00f5es para MKDocs, como emojis, diagramas e blocos estilizados de c\u00f3digo
      • Python-Markdown: Extens\u00e3o do Python para Markdown
      • Mkdocs-video: Extens\u00e3o para o MKDocs exibir os v\u00eddeos na p\u00e1gina
      • Mermaid.js: Constru\u00e7\u00e3o dos diagramas
      • Glaxnimate: Pra criar a anima\u00e7\u00e3o no topo dessa p\u00e1gina
      • Lottie-Player: Pra exibir a anima\u00e7\u00e3o do glaxnimate

      Para os slides:

      • marp: Onde preparei os slides
      • Rose-Pine: Tema que usei no marp
      "},{"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":"

      Estou animado para embarcar nesta jornada de aprendizado com voc\u00ea. Se voc\u00ea est\u00e1 pronto para aprender a construir APIs web eficientes e test\u00e1veis com FastAPI, este \u00e9 definitivamente o curso para voc\u00ea. Estou ansioso para come\u00e7ar a primeira aula, onde vamos configurar nosso ambiente de desenvolvimento e dar nossos primeiros passos com FastAPI e TDD!

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

      Perguntas frequentes que me fizeram ap\u00f3s os v\u00eddeos

      • Que papel de parede \u00e9 esse? \u00c9 uma foto do Liam Wong
      • Qual o tema no shell? Todo o meu tema do Gnome foi customizado com Gradience o tema \u00e9 Pretty In Purple
      • Qual o tema do seu editor? Rebecca
      "},{"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:

      • Introdu\u00e7\u00e3o ao ambiente de desenvolvimento (terminal, ferramentas, etc.)
      • Instala\u00e7\u00e3o do FastAPI e suas depend\u00eancias
      • Configura\u00e7\u00e3o das ferramentas de desenvolvimento
      • Execu\u00e7\u00e3o do primeiro \"Hello, World!\" com FastAPI com testes!
      Caso prefira ver a aula em v\u00eddeo

      Aula Slides C\u00f3digo

      Nesta aula pr\u00e1tica, vamos come\u00e7ar nossa jornada na constru\u00e7\u00e3o de uma API com FastAPI. Esse \u00e9 um moderno e r\u00e1pido (altamente perform\u00e1tico) framework web para constru\u00e7\u00e3o de APIs com Python 3.7+ baseado em Python type hints.

      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, Blue, Isort, pytest e Taskipy.

      Depois de 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 essa aula voc\u00ea vai precisar de algumas ferramentas.

      1. Um editor de texto a sua escolha (Eu vou usar o GNU/Emacs)
      2. Um terminal a sua escolha (Usarei o Terminator)
      3. A 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
      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
      6. Docker: Para criar um container da nossa aplica\u00e7\u00e3o (caso n\u00e3o tenha nenhum experi\u00eancia com docker a Linuxtips tem uma playlist completa e gr\u00e1tis sobre docker no canal deles no Youtube)
      7. OPCIONAL: O pipx pode te ajudar bastante nesses momentos de instala\u00e7\u00f5es
      8. OPCIONAL: O ignr para criar nosso gitignore
      9. OPCIONAL: O gh para criar o reposit\u00f3rio e fazer altera\u00e7\u00f5es sem precisar acessar a p\u00e1gina do Github
      "},{"location":"01/#instalacao-do-python-311","title":"Instala\u00e7\u00e3o do Python 3.11","text":"

      Se voc\u00ea precisar reconstruir o ambiente usado nesse curso, \u00e9 recomendado que voc\u00ea use o pyenv.

      Caso tenha problemas durante a instala\u00e7\u00e3o. O pyenv conta com dois assistentes simplificados para sua configura\u00e7\u00e3o. Para windows, use o pyenv-windows. Para GNU/Linux e MacOS, use o pyenv-installer.

      Navegue at\u00e9 o diret\u00f3rio onde far\u00e1 os exerc\u00edcios e executar\u00e1 os c\u00f3digos de exemplo no seu terminal e digite os seguintes comandos:

      $ Execu\u00e7\u00e3o no terminal!
      pyenv update\npyenv install 3.11:latest\n

      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\n  3.11.4\n  3.12.0b1\n

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

      "},{"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
      "},{"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.

      Vamos inicialmente criar um novo diret\u00f3rio para nosso projeto e navegar para ele:

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

      Ele criar\u00e1 uma estrutura 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

      Para que a vers\u00e3o que instalamos com 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.4  # Essa era a maior vers\u00e3o do 3.11 quando escrevi\n

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

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

      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 um novo projeto Python com Poetry e instalaremos as depend\u00eancias necess\u00e1rias - FastAPI e Uvicorn:

      $ Execu\u00e7\u00e3o no terminal!
      poetry install\npoetry add fastapi uvicorn\n
      "},{"location":"01/#primeira-execucao-de-um-hello-world","title":"Primeira Execu\u00e7\u00e3o de um \"Hello, World!\"","text":"

      Para garantir que tudo est\u00e1 configurado corretamente, vamos criar um pequeno programa \"Hello, World!\" com FastAPI. Em um novo arquivo chamado app.py no diret\u00f3rio fast_zero adicione o seguinte c\u00f3digo:

      fast_zero/app.py
      from fastapi import FastAPI\napp = FastAPI()\n@app.get('/')\ndef read_root():\nreturn {'message': 'Ol\u00e1 Mundo!'}\n

      Agora, podemos iniciar nosso servidor FastAPI com o seguinte comando:

      $ Execu\u00e7\u00e3o no terminal!
      poetry shell  # Para ativar o ambiente virtual\nuvicorn fast_zero.app:app --reload\n

      Acesse http://localhost:8000 no seu navegador, e voc\u00ea deve ver a mensagem \"Hello, World!\" em formato JSON.

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

      As ferramentas de desenvolvimento escolhidas podem variar de acordo com a prefer\u00eancia pessoal. Nesta aula, utilizaremos algumas que s\u00e3o particularmente \u00fateis para demonstrar certos conceitos:

      • taskipy: Ferramenta para automatizar alguns comandos e simplificar o fluxo
      • ruff: Um linter, para dizer se n\u00e3o estamos fazendo nada esquisito no c\u00f3digo
      • blue: Um formatador de c\u00f3digo bastante amig\u00e1vel
      • isort: Uma ferramenta para ordenar os imports em ordem alfab\u00e9tica
      • pytest: Ferramenta para executar testes

      Para instalar as depend\u00eancias, podemos usar um grupo do poetry focado nelas, para n\u00e3o serem usadas em produ\u00e7\u00e3o:

      $ Execu\u00e7\u00e3o no terminal!
      poetry add --group dev pytest pytest-cov taskipy blue ruff httpx isort\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 depend\u00eancias, vamos precisar configurar todas as ferramentas de desenvolvimento no arquivo pyproject.toml.

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

      Vamos usar o ruff na configura\u00e7\u00e3o padr\u00e3o. As \u00fanicas altera\u00e7\u00f5es que faremos ser\u00e3o para que ele siga a PEP-8, usando 80 caracteres por linha e ignorando o ambiente virtual e pasta de migra\u00e7\u00f5es do banco de dados:

      pyproject.toml
      [tool.ruff]\nline-length = 79\nexclude = ['.venv', 'migrations']\n
      "},{"location":"01/#isort","title":"isort","text":"

      Vamos configurar o isort para o formato do Black, que coloca uma instru\u00e7\u00e3o import por linha para evitar conflitos no git e tamb\u00e9m usar a base da PEP-8 para 80 caracteres:

      pyproject.toml
      [tool.isort]\nprofile = \"black\"\nline_length = 79\n
      "},{"location":"01/#pytest","title":"pytest","text":"

      Configuraremos o pytest para reconhecer o caminho base para execu\u00e7\u00e3o dos testes na raiz do projeto .:

      pyproject.toml
      [tool.pytest.ini_options]\npythonpath = \".\"\n
      "},{"location":"01/#blue","title":"blue","text":"

      Configuraremos o blue para excluir o caminho das migra\u00e7\u00f5es quando essas forem utilizadas:

      pyproject.toml
      [tool.blue]\nextend-exclude = '(migrations/)'\n
      "},{"location":"01/#taskipy","title":"Taskipy","text":"

      Para simplificar a execu\u00e7\u00e3o de certos comandos, vamos criar algumas tarefas com o Taskipy.

      pyproject.toml
      [tool.taskipy.tasks]\nlint = 'ruff . && blue --check . --diff'\nformat = 'blue .  && isort .'\nrun = 'uvicorn fast_zero.app:app --reload'\npre_test = 'task lint'\ntest = 'pytest -s -x --cov=fast_zero -vv'\npost_test = 'coverage html'\n

      Os comandos definidos fazem o seguinte:

      • lint: executa o ruff para ver se n\u00e3o temos nenhum problema com o c\u00f3digo e checa se estamos de acordo com a PEP-8
      • format: formata o c\u00f3digo usando blue e isort
      • run: executa o servidor de desenvolvimento do FastAPI
      • pre_test: executa a camada de lint antes de executar os teste
      • test: executa os testes com pytest de forma verbosa (-vv) e adiciona nosso c\u00f3digo como base de cobertura
      • post_test: gera um report de cobertura ap\u00f3s os testes

      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 = [\"dunossauro <mendesxeduardo@gmail.com>\"]\nreadme = \"README.md\"\npackages = [{include = \"fast_zero\"}]\n[tool.poetry.dependencies]\npython = \"3.11.*\"\nfastapi = \"^0.100.0\"\nuvicorn = \"^0.22.0\"\n[tool.poetry.group.dev.dependencies]\npytest = \"^7.4.0\"\npytest-cov = \"^4.1.0\"\ntaskipy = \"^1.11.0\"\nblue = \"^0.9.1\"\nruff = \"^0.0.278\"\nhttpx = \"^0.24.1\"\nisort = \"^5.12.0\"\n[tool.ruff]\nline-length = 79\nexclude = ['.venv', 'migrations']\n[tool.isort]\nprofile = \"black\"\nline_length = 79\n[tool.pytest.ini_options]\npythonpath = \".\"\n[tool.blue]\nextend-exclude = '(migrations/)'\n[tool.taskipy.tasks]\nlint = 'ruff . && blue --check . --diff'\nformat = 'blue .  && isort .'\nrun = 'uvicorn fast_zero.app:app --reload'\npre_test = 'task lint'\ntest = 'pytest -s -x --cov=fast_zero -vv'\npost_test = 'coverage html'\n[build-system]\nrequires = [\"poetry-core\"]\nbuild-backend = \"poetry.core.masonry.api\"\n
      "},{"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 blue nos informar\u00e1 que dever\u00edamos ter adicionado duas linhas antes de uma defini\u00e7\u00e3o de fun\u00e7\u00e3o:

      --- fast_zero/app.py    2023-07-12 21:40:14.590616 +0000\n+++ fast_zero/app.py    2023-07-12 21:48:17.017190 +0000\n@@ -1,7 +1,8 @@\nfrom fastapi import FastAPI\n\napp = FastAPI()\n\n+\n@app.get('/')\n def read_root():\n     return {'message': 'Ol\u00e1 Mundo!'}\nwould reformat fast_zero/app.py\n\nOh no! \ud83d\udca5 \ud83d\udc94 \ud83d\udca5\n1 file would be reformatted, 2 files would be left unchanged.\n

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

      ComandoResultado $ Execu\u00e7\u00e3o no terminal!
      task format\nreformatted fast_zero/app.py\n\nAll done! \u2728 \ud83c\udf70 \u2728\n1 file reformatted, 2 files left unchanged.\nSkipped 2 files\n
      fast_zero/app.py
      from fastapi import FastAPI\napp = FastAPI()\n@app.get('/')\ndef read_root():\nreturn {'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. Um bom lugar para come\u00e7ar isso \u00e9 analisando a cobertura. Vamos executar os testes.

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

      Teremos uma resposta como essa:

      $ Execu\u00e7\u00e3o no terminal!
      All done! \u2728 \ud83c\udf70 \u2728\n3 files would be left unchanged.\n=========================== test session starts ===========================\nplatform linux -- Python 3.11.3, pytest-7.4.0, pluggy-1.2.\ncachedir: .pytest_cache\nrootdir: /home/dunossauro/git/fast_zero\nconfigfile: pyproject.toml\nplugins: cov-4.1.0, anyio-3.7.1\ncollected 0 items\n\n/<path>/site-packages/coverage/control.py:860:\n  CoverageWarning: No data was collected. (no-data-collected)\nself._warn(\"No data was collected.\", slug=\"no-data-collected\")\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      5     0%\n-------------------------------------------\nTOTAL                       5      5     0%\n

      As primeiras duas linhas s\u00e3o referentes ao comando do taskipy pre_test que executa o blue e o ruff antes de cada teste. As linhas seguintes 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 ter encontrado 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, vamos escrever nosso primeiro teste com Pytest.

      Para testar o FastAPI, precisamos de um cliente de teste. Isso pode ser obtido 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\nfrom fast_zero.app import app\nclient = TestClient(app)\n

      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

      Devido ao fato de n\u00e3o ter coletado 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 do endpoint:

      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 fastapi.testclient import TestClient\nfrom fast_zero.app import app\nclient = TestClient(app)\ndef test_root_deve_retornar_200_e_ola_mundo():\nresponse = client.get('/')\nassert response.status_code == 200\nassert response.json() == {'message': 'Ol\u00e1 Mundo!'}\n

      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_200_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/#criando-nosso-repositorio-no-git","title":"Criando nosso reposit\u00f3rio no git","text":"

      Antes de concluirmos a aula, precisamos criar nosso reposit\u00f3rio no git e criar nosso arquivo .gitignore:

      $ Execu\u00e7\u00e3o no terminal!
      ignr -p python > .gitignore\ngit init .\ngh repo create\ngit add .\ngit commit -m \"Configura\u00e7\u00e3o inicial do projeto\"\ngit push\n
      "},{"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, vamos aprofundar na estrutura\u00e7\u00e3o da nossa aplica\u00e7\u00e3o FastAPI. At\u00e9 l\u00e1!

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

      Objetivos dessa aula:

      • Entendimento dos verbos HTTP, JSON e c\u00f3digos de resposta
      • Compreender a estrutura de um projeto FastAPI e como estruturar rotas CRUD (Criar, Ler, Atualizar, Deletar)
      • Aprender sobre a biblioteca Pydantic e sua utilidade na valida\u00e7\u00e3o e serializa\u00e7\u00e3o de dados
      • Implementa\u00e7\u00e3o de rotas CRUD em FastAPI
      • Escrita e execu\u00e7\u00e3o de testes para validar o comportamento das rotas
      Caso prefira ver a aula em v\u00eddeo

      Aula Slides C\u00f3digo

      Boas-vindas de volta \u00e0 nossa s\u00e9rie de cursos \"FastAPI do Zero: Criando um Projeto com Bancos de Dados, Testes e Deploy\". Hoje, na Aula 3, avan\u00e7aremos na estrutura\u00e7\u00e3o do nosso projeto FastAPI e implementar todas as rotas CRUD (Criar, Ler, Atualizar e Deletar) para o nosso recurso de usu\u00e1rio.

      "},{"location":"02/#o-que-e-uma-api","title":"O que \u00e9 uma API?","text":"

      Acr\u00f4nimo de Application Programming Interface (Interface de Programa\u00e7\u00e3o de Aplica\u00e7\u00f5es), uma API \u00e9 um conjunto de regras e protocolos que permitem a comunica\u00e7\u00e3o entre diferentes softwares. As APIs servem como uma ponte entre diferentes programas, permitindo que eles se comuniquem e compartilhem informa\u00e7\u00f5es de maneira eficiente e segura.

      No mundo moderno, as APIs geralmente comunicam usando o formato de dados JSON (JavaScript Object Notation), que \u00e9 uma maneira leve e eficiente de transmitir dados entre a API e o cliente.

      As APIs s\u00e3o fundamentais no mundo da programa\u00e7\u00e3o moderna, ao permitirem a intera\u00e7\u00e3o entre diferentes sistemas, independentemente de como foram projetados ou em que linguagem foram escritos.

      "},{"location":"02/#o-que-e-http","title":"O que \u00e9 HTTP?","text":"

      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.

      No contexto das APIs, o HTTP \u00e9 o protocolo que permite a comunica\u00e7\u00e3o entre o cliente (geralmente o navegador de um usu\u00e1rio, mas pode ser qualquer coisa que saiba como fazer solicita\u00e7\u00f5es HTTP) e o servidor onde a API est\u00e1 hospedada. As informa\u00e7\u00f5es entre o cliente e o servidor s\u00e3o trocadas na forma de JSON, tornando-se uma linguagem universal para a troca de informa\u00e7\u00f5es na web.

      O HTTP \u00e9 baseado no modelo de requisi\u00e7\u00e3o-resposta: o cliente faz uma requisi\u00e7\u00e3o para o servidor, e o servidor responde a essa requisi\u00e7\u00e3o. Essas requisi\u00e7\u00f5es e respostas s\u00e3o formatadas de acordo com as regras do protocolo HTTP.

      A seguir, vamos explorar como os verbos HTTP, os c\u00f3digos de resposta e os c\u00f3digos de erro s\u00e3o utilizados para gerenciar a comunica\u00e7\u00e3o entre o cliente e a API.

      "},{"location":"02/#compreendendo-os-verbos-http-codigos-de-resposta-e-codigos-de-erro","title":"Compreendendo os Verbos HTTP, C\u00f3digos de Resposta e C\u00f3digos de Erro","text":"

      Quando trabalhamos com APIs REST, o uso apropriado dos verbos HTTP, c\u00f3digos de resposta e c\u00f3digos de erro \u00e9 crucial para criar uma API clara e consistente.

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

      Os verbos HTTP indicam a a\u00e7\u00e3o desejada a ser executada em um determinado recurso. Os verbos mais comuns s\u00e3o:

      • GET: Recupera recursos. Deve retornar o status HTTP 200 (OK) se bem-sucedido.
      • POST: Cria um novo recurso. Deve retornar o status HTTP 201 (Criado) se bem-sucedido.
      • PUT: Atualiza um recurso existente. Deve retornar o status HTTP 200 (OK) se bem-sucedido.
      • DELETE: Exclui um recurso. Deve retornar o status HTTP 200 (OK) se bem-sucedido.
      "},{"location":"02/#codigos-de-resposta-http","title":"C\u00f3digos de Resposta HTTP","text":"

      Os c\u00f3digos de resposta HTTP informam ao cliente sobre o resultado de sua solicita\u00e7\u00e3o. Aqui est\u00e3o alguns dos c\u00f3digos de resposta mais comuns:

      • 200 OK: A solicita\u00e7\u00e3o foi bem-sucedida. O significado exato depende do m\u00e9todo HTTP utilizado na solicita\u00e7\u00e3o.
      • 201 Created: A solicita\u00e7\u00e3o foi bem-sucedida e um novo recurso foi criado como resultado.
      • 404 Not Found: O recurso solicitado n\u00e3o p\u00f4de ser encontrado. Este c\u00f3digo \u00e9 frequentemente usado quando o recurso \u00e9 inexistente.
      "},{"location":"02/#codigos-de-erro-http","title":"C\u00f3digos de Erro HTTP","text":"

      Os c\u00f3digos de erro HTTP indicam que houve um problema com a solicita\u00e7\u00e3o. Alguns c\u00f3digos de erro comuns incluem:

      • 400 Bad Request: A solicita\u00e7\u00e3o era inv\u00e1lida ou malformada.
      • 401 Unauthorized: A solicita\u00e7\u00e3o requer autentica\u00e7\u00e3o. Se a solicita\u00e7\u00e3o inclu\u00eda credenciais de autentica\u00e7\u00e3o e mesmo assim retornou 401, isso significa que as credenciais n\u00e3o foram aceitas.
      • 403 Forbidden: O cliente n\u00e3o tem permiss\u00f5es para acessar o recurso solicitado, apesar de estar autenticado.
      • 404 Not Found: O recurso solicitado n\u00e3o existe no servidor.
      • 422 Unprocessable Entity: A solicita\u00e7\u00e3o est\u00e1 bem formada, mas os par\u00e2metros sem\u00e2nticos s\u00e3o inv\u00e1lidos ou incompat\u00edveis.
      • 500 Internal Server Error: Ocorreu um erro inesperado no servidor.

      Ao trabalhar com APIs REST, \u00e9 importante lidar corretamente com esses c\u00f3digos de resposta e erro para proporcionar uma boa experi\u00eancia para os usu\u00e1rios da API.

      "},{"location":"02/#como-acontece-a-comunicacao-web-entre-cliente-e-servidor","title":"Como acontece a comunica\u00e7\u00e3o web entre cliente e servidor","text":"

      A comunica\u00e7\u00e3o entre cliente e servidor na web \u00e9 um processo que ocorre em v\u00e1rias etapas e \u00e9 governado por protocolos de comunica\u00e7\u00e3o espec\u00edficos. O protocolo mais comum \u00e9 o HTTP (Hypertext Transfer Protocol). Essa forma de comunica\u00e7\u00e3o \u00e9 geralmente descrita como stateless, o que significa que cada requisi\u00e7\u00e3o \u00e9 processada de forma independente, sem qualquer conhecimento das requisi\u00e7\u00f5es anteriores.

      A informa\u00e7\u00e3o \u00e9 trocada na forma de mensagens HTTP, que cont\u00eam dados e informa\u00e7\u00f5es sobre como esses dados devem ser processados. Um aspecto fundamental dessa comunica\u00e7\u00e3o \u00e9 a troca de dados na forma de objetos JSON, que s\u00e3o uma maneira eficiente e flex\u00edvel de representar dados estruturados.

      sequenceDiagram\n    participant Cliente\n    participant Servidor\n    Cliente->>Servidor: Requisi\u00e7\u00e3o HTTP (GET, POST, PUT, DELETE)\n    Note right of Servidor: Processa a requisi\u00e7\u00e3o\n    Servidor-->>Cliente: Resposta HTTP (C\u00f3digo de Status)\n    Note left of Cliente: Processa a resposta\n    Cliente->>Servidor: Requisi\u00e7\u00e3o HTTP com JSON (POST, PUT)\n    Note right of Servidor: Processa a requisi\u00e7\u00e3o e o JSON\n    Servidor-->>Cliente: Resposta HTTP com JSON\n    Note left of Cliente: Processa a resposta e o JSON

      Este diagrama representa a sequ\u00eancia b\u00e1sica de uma comunica\u00e7\u00e3o cliente-servidor usando HTTP e JSON:

      • O cliente, geralmente um navegador web ou uma aplica\u00e7\u00e3o, faz uma requisi\u00e7\u00e3o HTTP para o servidor. Essa requisi\u00e7\u00e3o pode ser de diferentes tipos, como GET (para recuperar dados), POST ou PUT (para enviar dados), ou DELETE (para excluir dados).
      • O servidor recebe a requisi\u00e7\u00e3o, processa-a e, em seguida, prepara a resposta. A resposta inclui um c\u00f3digo de status que indica se a requisi\u00e7\u00e3o foi bem-sucedida ou n\u00e3o.
      • O cliente recebe a resposta do servidor e processa-a. Se os dados foram solicitados, eles s\u00e3o exibidos ao usu\u00e1rio. Se foi uma opera\u00e7\u00e3o de envio de dados, a resposta do servidor indicar\u00e1 se a opera\u00e7\u00e3o foi bem-sucedida.
      • Para opera\u00e7\u00f5es que envolvem o envio de dados (como POST e PUT), o cliente inclui os dados na forma de um objeto JSON na requisi\u00e7\u00e3o. O JSON \u00e9 um formato de dados leve e f\u00e1cil de usar que permite a representa\u00e7\u00e3o de estruturas de dados complexas.
      • O servidor recebe a requisi\u00e7\u00e3o com os dados JSON, processa a requisi\u00e7\u00e3o e os dados, e envia uma resposta ao cliente. Esta resposta tamb\u00e9m pode incluir dados no formato JSON.
      • Finalmente, o cliente processa a resposta do servidor e quaisquer dados JSON inclu\u00eddos nela.

      Essa \u00e9 uma vis\u00e3o geral simplificada do processo. Na pr\u00e1tica, a comunica\u00e7\u00e3o entre cliente e servidor pode envolver muitas outras nuances, como autentica\u00e7\u00e3o, redirecionamento, cookies e muito mais.

      "},{"location":"02/#pydantic-e-a-validacao-de-dados","title":"Pydantic e a valida\u00e7\u00e3o de dados","text":"

      Antes de mergulharmos no c\u00f3digo, vamos entender alguns conceitos importantes.

      Caso esse seja seu primeiro contato com Pydantic

      Temos uma live de python exclusiva sobre esse assunto

      Link direto

      O Pydantic \u00e9 uma biblioteca Python que oferece valida\u00e7\u00e3o de dados e configura\u00e7\u00f5es usando anota\u00e7\u00f5es de tipos Python. Ela \u00e9 utilizada extensivamente em FastAPI para lidar com a valida\u00e7\u00e3o e serializa\u00e7\u00e3o/desserializa\u00e7\u00e3o de dados. O Pydantic tem um papel crucial ao trabalhar com JSON, pois permite a valida\u00e7\u00e3o dos dados recebidos neste formato, assim como sua convers\u00e3o para formatos nativos do Python e vice-versa.

      Esquemas: No contexto da programa\u00e7\u00e3o, um esquema \u00e9 uma representa\u00e7\u00e3o estrutural de um objeto ou entidade. Por exemplo, no nosso caso, um usu\u00e1rio pode ser representado por um esquema que cont\u00e9m campos para nome de usu\u00e1rio, e-mail e senha. Esquemas s\u00e3o \u00fateis porque permitem definir a estrutura de um objeto de uma maneira clara e reutiliz\u00e1vel.

      Valida\u00e7\u00e3o de dados: Este \u00e9 o processo de verificar se os dados recebidos est\u00e3o em conformidade com as regras e restri\u00e7\u00f5es definidas. Por exemplo, se esperamos que o campo \"email\" contenha um endere\u00e7o de e-mail v\u00e1lido, a valida\u00e7\u00e3o de dados garantir\u00e1 que os dados inseridos nesse campo de fato correspondam a um formato de e-mail v\u00e1lido.

      Agora, vamos falar sobre a biblioteca Pydantic.

      O Pydantic \u00e9 uma biblioteca Python que oferece valida\u00e7\u00e3o de dados e configura\u00e7\u00f5es usando anota\u00e7\u00f5es de tipos Python. Ela \u00e9 utilizada extensivamente em FastAPI para lidar com a valida\u00e7\u00e3o e serializa\u00e7\u00e3o/desserializa\u00e7\u00e3o de dados.

      O uso do Pydantic nos permite definir modelos de dados, ou \"esquemas\", com campos anotados com tipos de dados. O Pydantic garante que as inst\u00e2ncias desses modelos sempre estejam em conformidade com o esquema definido.

      Vamos considerar um exemplo onde recebemos o seguinte objeto JSON, representando um novo usu\u00e1rio que quer se registrar em nosso servi\u00e7o:

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

      Para lidar com esta entrada de dados, devemos definir um esquema Pydantic que corresponda \u00e0 estrutura deste objeto JSON. Usamos anota\u00e7\u00f5es de tipos Python para definir o tipo de dado de cada campo:

      from pydantic import BaseModel, EmailStr\nclass UserSchema(BaseModel):\nusername: str\nemail: EmailStr\npassword: str\n

      Neste exemplo, o campo username \u00e9 esperado como uma string, o campo email como uma string que valida o formato de um endere\u00e7o de email (gra\u00e7as \u00e0 anota\u00e7\u00e3o EmailStr do Pydantic), e o campo password tamb\u00e9m \u00e9 esperado como uma string.

      Ao usar este esquema, qualquer tentativa de criar um usu\u00e1rio com dados que n\u00e3o correspondam a este formato (por exemplo, um email que n\u00e3o \u00e9 v\u00e1lido, ou um campo de nome de usu\u00e1rio que n\u00e3o \u00e9 uma string) resultar\u00e1 em um erro de valida\u00e7\u00e3o.

      "},{"location":"02/#suporte-a-emails","title":"Suporte a emails","text":"

      Para que o Pydantic suporte a valida\u00e7\u00e3o de emails, \u00e9 necess\u00e1rio instalar o pydantic[email]

      Ademais, se tentarmos criar um usu\u00e1rio com um email inv\u00e1lido, o Pydantic ir\u00e1 automaticamente validar o campo e retornar um erro \u00fatil. Isso nos poupa muito trabalho de valida\u00e7\u00e3o manual e ajuda a manter nossa API robusta e confi\u00e1vel.

      "},{"location":"02/#implementando-as-rotas-crud","title":"Implementando as Rotas CRUD","text":"

      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:

      • Create (Criar): Adicionar novos registros ao banco de dados. No HTTP, essa a\u00e7\u00e3o geralmente \u00e9 associada ao verbo POST.
      • Read (Ler): Recuperar registros existentes do banco de dados. No HTTP, essa a\u00e7\u00e3o geralmente \u00e9 associada ao verbo GET.
      • Update (Atualizar): Modificar registros existentes no banco de dados. No HTTP, essa a\u00e7\u00e3o geralmente \u00e9 associada ao verbo PUT ou PATCH.
      • Delete (Excluir): Remover registros existentes do banco de dados. No HTTP, essa a\u00e7\u00e3o geralmente \u00e9 associada ao verbo DELETE.

      Os c\u00f3digos de status HTTP s\u00e3o usados para indicar o resultado de cada opera\u00e7\u00e3o CRUD. Por exemplo, uma solicita\u00e7\u00e3o POST bem-sucedida (create) retorna o status HTTP 201 (Criado), enquanto uma solicita\u00e7\u00e3o GET bem-sucedida (read) retorna o status HTTP 200 (OK).

      \u00c9 importante notar que, ao trabalhar com FastAPI e Pydantic, nossos esquemas desempenham um papel vital na opera\u00e7\u00e3o de \"Create\" (criar). Ao usar a opera\u00e7\u00e3o POST para adicionar um novo registro ao nosso banco de dados, vamos aproveitar a valida\u00e7\u00e3o de dados do Pydantic para garantir que o novo registro esteja em conformidade com o esquema do nosso modelo de dados. Se os dados enviados na solicita\u00e7\u00e3o POST n\u00e3o passarem na valida\u00e7\u00e3o do Pydantic, nossa API retornar\u00e1 um c\u00f3digo de status HTTP 422 (Unprocessable Entity), indicando que os dados fornecidos s\u00e3o inv\u00e1lidos ou incompletos.

      Agora que temos uma compreens\u00e3o clara do que \u00e9 o CRUD, como se relaciona com os verbos HTTP, os c\u00f3digos de status e a valida\u00e7\u00e3o do Pydantic, podemos passar para a implementa\u00e7\u00e3o dessas opera\u00e7\u00f5es em nossa API FastAPI.

      Na nossa API, vamos criar rotas correspondentes para cada opera\u00e7\u00e3o CRUD, come\u00e7ando com a opera\u00e7\u00e3o \"create\" (criar), que ser\u00e1 implementada pela rota POST.

      "},{"location":"02/#implementando-a-rota-post","title":"Implementando a Rota POST","text":"

      A rota POST \u00e9 usada para criar um novo usu\u00e1rio em nosso sistema. Lembrando, o verbo HTTP POST est\u00e1 relacionado \u00e0 opera\u00e7\u00e3o \"Create\" do CRUD. Se tudo ocorrer como esperado e um novo usu\u00e1rio for criado com sucesso, a rota deve retornar o status HTTP 201 (Criado).

      Para a cria\u00e7\u00e3o dessa rota, vamos usar de base o JSON que criamos anteriormente. Para que a pessoa se cadastre na nossa plataforma, ela precisa enviar os dados de nome de usu\u00e1rio, email e senha:

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

      Para isso, vamos criar um esquema Pydantic equivalente em um arquivo de esquemas: fast_zero/schemas.py:

      fast_zero/schemas.py
      from pydantic import BaseModel, EmailStr\nclass UserSchema(BaseModel):\nusername: str\nemail: EmailStr\npassword: str\n

      Agora vamos criar nosso endpoint que esperar\u00e1 receber esse esquema Pydantic e retornar\u00e1 201, caso o JSON enviado seja v\u00e1lido:

      fast_zero/app.py
      from fastapi import FastAPI\nfrom fast_zero.schemas import UserSchema\n# C\u00f3digo da nossa rota de ol\u00e1 mundo omitido\n@app.post('/users/', status_code=201)\ndef create_user(user: UserSchema):\nreturn user\n

      Com esse endpoint criado, podemos executar a nossa aplica\u00e7\u00e3o:

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

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

      Dessa forma, podemos testar de forma simplificada a nossa API, enviando o JSON e realizando alguns testes.

      Entretanto, precisamos prestar aten\u00e7\u00e3o a um detalhe: nosso modelo retorna a senha do usu\u00e1rio, o que \u00e9 uma p\u00e9ssima pr\u00e1tica de seguran\u00e7a.

      Para evitar isso, podemos criar um novo modelo que ser\u00e1 usado somente para resposta. Dessa forma, n\u00e3o expomos os dados que n\u00e3o queremos na API:

      fast_zero/schemas.py
      class UserPublic(BaseModel):\nusername: str\nemail: EmailStr\n

      Precisamos tamb\u00e9m dizer ao FastAPI que esse ser\u00e1 o modelo de resposta, e converter nosso user em UserPublic:

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

      Note que somente adicionando o response_model, o FastAPI j\u00e1 faz a convers\u00e3o de UserSchema em UserPublic

      Agora, se fizermos de novo a chamada no Swagger, receberemos o mesmo objeto, mas sem expor a senha.

      Caso nunca tenha usado o swagger

      Temos uma live focada em OpenAPI que s\u00e3o as especifica\u00e7\u00f5es do swagger

      Link direto

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

      Finalmente, para brincar com essas rotas, podemos criar uma lista provis\u00f3ria para simular um banco de dados. Assim, podemos adicionar nossos dados e entender como o FastAPI funciona. Para isso, adicionamos uma lista provis\u00f3ria para o \"banco\" e alteramos nosso endpoint para inserir nossos modelos do Pydantic nessa lista:

      fast_zero/app.py
      from fast_zero.schemas import UserSchema, UserPublic, UserDB\n# c\u00f3digo omitido\ndatabase = []  # provis\u00f3rio para estudo!\n@app.post('/users/', status_code=201, response_model=UserPublic)\ndef create_user(user: UserSchema):\nuser_with_id = UserDB(**user.model_dump(), id=len(database) + 1)\ndatabase.append(user_with_id)\nreturn user_with_id\n

      Se queremos uma simula\u00e7\u00e3o de banco de dados, precisamos ter um ID para cada usu\u00e1rio registrado no nosso \"banco\". Sendo assim, vamos alterar nosso modelo de resposta p\u00fablica (UserPublic) para que ele forne\u00e7a o ID de cria\u00e7\u00e3o do usu\u00e1rio. Vamos tamb\u00e9m criar um novo modelo que represente o usu\u00e1rio com sua senha e identificador, que chamaremos de UserDB:

      fast_zero/schemas.py
      class UserPublic(BaseModel):\nid: int\nusername: str\nemail: EmailStr\nclass UserDB(UserSchema):\nid: int\n

      Dessa forma, nada muda. No entanto, podemos prosseguir com a constru\u00e7\u00e3o dos outros endpoints. E lembre-se, \u00e9 importante testar esse endpoint para garantir que tudo esteja funcionando corretamente.

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

      Antes de criar o teste de fato, vamos execut\u00e1-los para ver como anda a nossa cobertura:

      $ Execu\u00e7\u00e3o no terminal!
      task test\n# parte da resposta foi omitida\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.

      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. N\u00f3s 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():\nresponse = client.post(\n'/users/',\njson={\n'username': 'alice',\n'email': 'alice@example.com',\n'password': 'secret',\n},\n)\nassert response.status_code == 201\nassert 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# parte da resposta foi omitida\ntests/test_app.py::test_root_deve_retornar_200_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

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

      "},{"location":"02/#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):\nusers: 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 UserSchema, UserPublic, UserDB, UserList\n# c\u00f3digo omitido\n@app.get('/users/', response_model=UserList)\ndef read_users():\nreturn {'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":"02/#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. N\u00f3s 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():\nresponse = client.get('/users/')\nassert response.status_code == 200\nassert 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. Vamos implementar a pr\u00f3xima opera\u00e7\u00e3o CRUD: Update.

      "},{"location":"02/#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).

      fast_zero/app.py
      from fastapi import FastAPI, HTTPException\n# ...\n@app.put('/users/{user_id}', response_model=UserPublic)\ndef update_user(user_id: int, user: UserSchema):\nif user_id > len(database) or user_id < 1:\nraise HTTPException(status_code=404, detail='User not found')\nuser_with_id = UserDB(**user.model_dump(), id=user_id)\ndatabase[user_id - 1] = user_with_id\nreturn user_with_id\n
      "},{"location":"02/#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. N\u00f3s 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():\nresponse = client.put(\n'/users/1',\njson={\n'username': 'bob',\n'email': 'bob@example.com',\n'password': 'mynewpassword',\n},\n)\nassert response.status_code == 200\nassert 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":"02/#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).

      Para transmitir uma mensagem de sucesso ou falha na opera\u00e7\u00e3o de exclus\u00e3o, podemos criar um modelo chamado Message. Esse modelo ser\u00e1 respons\u00e1vel por embalar uma mensagem que ser\u00e1 retornada na nossa API.

      fast_zero/schemas.py
      class Message(BaseModel):\ndetail: str\n

      Agora podemos criar nosso endpoint DELETE. 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
      from fast_zero.schemas import UserSchema, UserPublic, UserDB, UserList, Message\n# ...\n@app.delete('/users/{user_id}', response_model=Message)\ndef delete_user(user_id: int):\nif user_id > len(database) or user_id < 1:\nraise HTTPException(status_code=404, detail='User not found')\ndel database[user_id - 1]\nreturn {'detail': '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":"02/#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. N\u00f3s 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():\nresponse = client.delete('/users/1')\nassert response.status_code == 200\nassert response.json() == {'detail': 'User deleted'}\n
      "},{"location":"02/#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, vamos verificar 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, vamos adicionar 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 outras pessoas (ou n\u00f3s mesmos, no futuro) possam entender 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":"02/#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 API robusta e 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.

      No pr\u00f3ximo t\u00f3pico, vamos explorar uma das partes mais cr\u00edticas de qualquer aplicativo - a conex\u00e3o e intera\u00e7\u00e3o com um banco de dados. Vamos aprender 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.

      "},{"location":"03/","title":"Configurando o banco de dados e gerenciando migra\u00e7\u00f5es com Alembic","text":""},{"location":"03/#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:

      • Introdu\u00e7\u00e3o ao SQLAlchemy e Alembic
      • Instalando SQLAlchemy e Alembic
      • Configurando e criando o banco de dados
      • Criando e localizando tabelas utilizando SQLAlchemy
      • Testando a cria\u00e7\u00e3o de tabelas
      • Gerenciando migra\u00e7\u00f5es do banco de dados com Alembic
      Caso prefira ver a aula em v\u00eddeo

      Aula Slides C\u00f3digo

      Ol\u00e1 a todos! Se voc\u00ea est\u00e1 chegando agora, recomendamos verificar as aulas anteriores de nosso curso \"FastAPI do Zero: Criando um Projeto com Bancos de Dados, Testes e Deploy\". Hoje, vamos mergulhar no SQLAlchemy e no Alembic, e come\u00e7aremos a configurar nosso banco de dados.

      Antes de mergulharmos na instala\u00e7\u00e3o e configura\u00e7\u00e3o, vamos esclarecer alguns conceitos.

      "},{"location":"03/#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:

      • Abstra\u00e7\u00e3o de banco de dados: ORMs permitem que voc\u00ea mude de um tipo de banco de dados para outro com poucas altera\u00e7\u00f5es no c\u00f3digo.

      • Seguran\u00e7a: ORMs geralmente lidam com escapar de consultas e prevenir inje\u00e7\u00f5es SQL, um tipo comum de vulnerabilidade de seguran\u00e7a.

      • Efici\u00eancia no desenvolvimento: ORMs podem gerar automaticamente esquemas, realizar migra\u00e7\u00f5es e outras tarefas que seriam demoradas para fazer manualmente.

      "},{"location":"03/#configuracoes-de-ambiente-e-as-12-fatores","title":"Configura\u00e7\u00f5es de ambiente e as 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, vamos come\u00e7ar instalando as bibliotecas que vamos usar. O primeiro passo \u00e9 instalar o SQLAlchemy, um ORM que nos permite trabalhar com bancos de dados SQL de maneira Pythonic. 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":"03/#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).

      • Core: O Core do SQLAlchemy disponibiliza uma interface SQL abstrata, que possibilita a manipula\u00e7\u00e3o de bancos de dados relacionais de maneira segura, alinhada com as conven\u00e7\u00f5es do Python. Atrav\u00e9s do Core, \u00e9 poss\u00edvel construir, analisar e executar instru\u00e7\u00f5es SQL, al\u00e9m de conectar-se a diversos tipos de bancos de dados utilizando a mesma API.

      • ORM: ORM, ou Mapeamento Objeto-Relacional, \u00e9 uma t\u00e9cnica que facilita a comunica\u00e7\u00e3o entre o c\u00f3digo orientado a objetos e bancos de dados relacionais. Com o ORM do SQLAlchemy, os desenvolvedores podem interagir com o banco de dados utilizando classes e objetos Python, eliminando a necessidade de escrever instru\u00e7\u00f5es SQL diretamente.

      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":"03/#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":"03/#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":"03/#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 herdam de uma classe base comum. A classe base \u00e9 criada a partir de DeclarativeBase.

      Cada classe que herda da classe base \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 que foram declaradas. Este objeto \u00e9 utilizado para gerenciar opera\u00e7\u00f5es como cria\u00e7\u00e3o, modifica\u00e7\u00e3o e exclus\u00e3o de tabelas.

      Vamos agora definir nosso modelo User. No diret\u00f3rio fast_zero, crie um novo arquivo chamado models.py.

      $ Execu\u00e7\u00e3o no terminal!
      touch fast_zero/models.py\n

      Inclua o seguinte c\u00f3digo no arquivo models.py:

      fast_zero/models.py
      from sqlalchemy.orm import DeclarativeBase\nfrom sqlalchemy.orm import Mapped\nfrom sqlalchemy.orm import mapped_column\nclass Base(DeclarativeBase):\npass\nclass User(Base):\n__tablename__ = 'users'\nid: Mapped[int] = mapped_column(primary_key=True)\nusername: Mapped[str]\npassword: Mapped[str]\nemail: Mapped[str]\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 com o banco de dados.

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

      Ao desenvolver software, \u00e9 uma boa pr\u00e1tica criar testes para cada nova funcionalidade implementada. Ap\u00f3s a cria\u00e7\u00e3o do nosso modelo de usu\u00e1rio, precisamos garantir seu funcionamento conforme esperado. Para isso, vamos criar dois arquivos de teste: conftest.py e test_db.py.

      O arquivo conftest.py \u00e9 utilizado pelo pytest para definir preparativos necess\u00e1rios antes da execu\u00e7\u00e3o dos testes ou configura\u00e7\u00f5es comuns a diversos testes. Ele centraliza as chamadas para as fixtures.

      Se fixtures s\u00e3o uma novidade para voc\u00ea

      Existe uma live de Python onde discutimos especificamente sobre fixtures

      Link direto

      Por sua vez, o arquivo test_db.py ser\u00e1 onde escreveremos os testes para nosso modelo de usu\u00e1rio. Isso ajudar\u00e1 a garantir que nossas tabelas estejam sendo criadas corretamente e que possam armazenar e recuperar informa\u00e7\u00f5es conforme esperado.

      Para criar esses arquivos, execute o seguinte comando:

      $ Execu\u00e7\u00e3o no terminal!
      touch tests/test_db.py\ntouch tests/conftest.py\n
      "},{"location":"03/#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]

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

      "},{"location":"03/#criando-uma-fixture-para-o-banco-de-dados","title":"Criando uma Fixture para o Banco de Dados","text":"

      No arquivo conftest.py, vamos introduzir uma fixture chamada \"session\" aos nossos testes pytest. Uma fixture \u00e9 uma fun\u00e7\u00e3o que permite definir e reutilizar configura\u00e7\u00f5es de prepara\u00e7\u00e3o para testes ou outras fun\u00e7\u00f5es. Elas s\u00e3o \u00fateis quando se tem partes do c\u00f3digo que precisam ser reutilizadas em v\u00e1rios testes.

      As fixtures s\u00e3o declaradas com a anota\u00e7\u00e3o @pytest.fixture. Uma vez declarada, a fixture pode ser injetada em fun\u00e7\u00f5es de teste, m\u00e9todos de classe de teste, outras fixtures ou at\u00e9 mesmo m\u00f3dulos, passando-as como par\u00e2metros.

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

      tests/conftest.py
      import pytest\nfrom sqlalchemy import create_engine, select\nfrom sqlalchemy.orm import sessionmaker\nfrom sqlalchemy.pool import StaticPool\nfrom fast_zero.models import Base\n@pytest.fixture\ndef session():\nengine = create_engine(\n'sqlite:///:memory:',\nconnect_args={'check_same_thread': False},\npoolclass=StaticPool,\n)\nSession = sessionmaker(autocommit=False, autoflush=False, bind=engine)\nBase.metadata.create_all(engine)\nyield Session()\nBase.metadata.drop_all(engine)\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:', connect_args={'check_same_thread': False}, poolclass=StaticPool): 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. Usamos connect_args={'check_same_thread': False} devido a algumas limita\u00e7\u00f5es do SQLite quando usado em threads diferentes. E poolclass=StaticPool para que a mesma conex\u00e3o seja reutilizada durante todo o teste.

      2. Session = sessionmaker(autocommit=False, autoflush=False, bind=engine): cria uma f\u00e1brica de sess\u00f5es para criar sess\u00f5es de banco de dados para nossos testes. Definimos autocommit=False para controlar quando as transa\u00e7\u00f5es s\u00e3o commitadas e autoflush=False para que SQLAlchemy n\u00e3o tente automaticamente sincronizar o estado da sess\u00e3o com o banco de dados a cada opera\u00e7\u00e3o.

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

      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. Base.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":"03/#criando-um-teste-para-a-nossa-tabela","title":"Criando um Teste para a Nossa Tabela","text":"

      Agora, no arquivo test_db.py, vamos escrever 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\nfrom fast_zero.models import User\ndef test_create_user(session):\nnew_user = User(username='alice', password='secret', email='teste@test')\nsession.add(new_user)\nsession.commit()\nuser = session.scalar(select(User).where(User.username == 'alice'))\nassert user.username == 'alice'\n
      "},{"location":"03/#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, vamos executar 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# ...\ntests/test_app.py::test_root_deve_retornar_200_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":"03/#configuracao-do-ambiente-do-banco-de-dados","title":"Configura\u00e7\u00e3o do ambiente do banco de dados","text":"

      Por fim, vamos configurar nosso banco de dados. Primeiro, vamos criar 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.

      $ Execu\u00e7\u00e3o no terminal!
      touch fast_zero/settings.py\n

      No arquivo settings.py, a classe Settings \u00e9 definida como:

      fast_zero/settings.py
      from pydantic_settings import BaseSettings, SettingsConfigDict\nclass Settings(BaseSettings):\nmodel_config = SettingsConfigDict(\nenv_file='.env', env_file_encoding='utf-8'\n)\nDATABASE_URL: str\n

      Agora, vamos definir 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":"03/#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, vamos come\u00e7ar 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  \u2514\u2500\u2500 schemas.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":"03/#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, vamos fazer algumas altera\u00e7\u00f5es no arquivo migrations/env.py.

      Neste arquivo, precisamos:

      1. Importar as Settings do nosso arquivo settings.py e a Base 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 Base.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 alembic import context\nfrom fast_zero.settings import Settings\nfrom fast_zero.models import Base\nconfig = context.config\nconfig.set_main_option('sqlalchemy.url', Settings().DATABASE_URL)\nif config.config_file_name is not None:\nfileConfig(config.config_file_name)\ntarget_metadata = Base.metadata\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":"03/#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\nRevision ID: e018397cecf4\nRevises:\nCreate Date: 2023-07-13 03:43:03.730534\n\"\"\"\nfrom alembic import op\nimport sqlalchemy as sa\n# revision identifiers, used by Alembic.\nrevision = 'e018397cecf4'\ndown_revision = None\nbranch_labels = None\ndepends_on = None\ndef upgrade() -> None:\n# ### commands auto generated by Alembic - please adjust! ###\nop.create_table('users',\nsa.Column('id', sa.Integer(), nullable=False),\nsa.Column('username', sa.String(), nullable=False),\nsa.Column('password', sa.String(), nullable=False),\nsa.Column('email', sa.String(), nullable=False),\nsa.PrimaryKeyConstraint('id')\n)\n# ### end Alembic commands ###\ndef downgrade() -> None:\n# ### commands auto generated by Alembic - please adjust! ###\nop.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.

      Apesar desta migra\u00e7\u00e3o ter sido criada, ela ainda n\u00e3o foi aplicada ao nosso banco de dados. No entanto, o Alembic j\u00e1 criou um arquivo database.db, conforme especificamos no arquivo .env e que foi lido pela classe Settings do Pydantic. Al\u00e9m disso, ele criou uma tabela alembic_version no banco de dados para controlar as vers\u00f5es das migra\u00e7\u00f5es que foram aplicadas.

      $ Execu\u00e7\u00e3o no terminal!
      sqlite3 database.db\nSQLite version 3.42.0 2023-05-16 12:36:15\nEnter \".help\" for usage hints.\nsqlite> .schema\nCREATE TABLE alembic_version (\nversion_num VARCHAR(32) NOT NULL,\nCONSTRAINT alembic_version_pkc PRIMARY KEY (version_num)\n);\nsqlite> .exit\n

      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\nINFO  [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

      Agora, se examinarmos nosso banco de dados novamente, veremos que a tabela users foi criada:

      sqlite3 database.db\nSQLite version 3.42.0 2023-05-16 12:36:15\nEnter \".help\" for usage hints.\nsqlite> .schema\nCREATE TABLE alembic_version (\nversion_num VARCHAR(32) NOT NULL,\nCONSTRAINT alembic_version_pkc PRIMARY KEY (version_num)\n);\nCREATE TABLE users (\nid INTEGER NOT NULL,\nusername VARCHAR NOT NULL,\npassword VARCHAR NOT NULL,\nemail VARCHAR NOT NULL,\nPRIMARY KEY (id)\n);\nsqlite> .exit\n

      Finalmente, lembre-se de que todas essas mudan\u00e7as que fizemos s\u00f3 existem localmente no seu ambiente de trabalho at\u00e9 agora. Para que sejam compartilhadas com outras pessas, precisamos fazer commit dessas mudan\u00e7as no nosso sistema de controle de vers\u00e3o.

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

      Primeiro, vamos verificar 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 que foram 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, vamos adicionar 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. Vamos fornecer 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, vamos enviar 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 git.

      "},{"location":"03/#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, em conformidade com 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.

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

      Objetivos dessa aula:

      • Integrando SQLAlchemy \u00e0 nossa aplica\u00e7\u00e3o FastAPI
      • Utilizando a fun\u00e7\u00e3o Depends para gerenciar depend\u00eancias
      • Modificando endpoints para interagir com o banco de dados
      • Testando os novos endpoints com Pytest e fixtures
      Caso prefira ver a aula em v\u00eddeo

      Aula Slides C\u00f3digo

      Ap\u00f3s a cria\u00e7\u00e3o de nossos modelos e migra\u00e7\u00f5es na aula passada, chegou o momento de dar um passo significativo: integrar o banco de dados \u00e0 nossa aplica\u00e7\u00e3o FastAPI. Vamos deixar de lado o banco de dados fict\u00edcio que criamos anteriormente e mergulhar na implementa\u00e7\u00e3o de um banco de dados real e funcional.

      "},{"location":"04/#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

      Temos diversas lives de Python focadas nesse assunto.

      Esta sobre o ORM em espec\u00edfico:

      Link direto

      Essa sobre as novidades da vers\u00e3o 1.4 e do estilo de programa\u00e7\u00e3o da vers\u00e3o 2.0

      Link direto

      E finalmente uma focada no processo de migra\u00e7\u00f5es com o SQLalchemy + Alembic (que veremos nessa aula)

      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 que a loja saiba 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, vamos configurar 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\nfrom fast_zero.settings import Settings\nengine = create_engine(Settings().DATABASE_URL)\ndef get_session():\nwith Session(engine) as session:\nyield session\n
      "},{"location":"04/#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) que \u00e9 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":"04/#modificando-o-endpoint-post-users","title":"Modificando o Endpoint POST /users","text":"

      Agora que temos a nossa sess\u00e3o de banco de dados sendo gerenciada por meio do FastAPI e da inje\u00e7\u00e3o de depend\u00eancias, vamos atualizar nossos endpoints para que possam 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 vamos fazer 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 fastapi import Depends, FastAPI, HTTPException\nfrom sqlalchemy import select\nfrom sqlalchemy.orm import Session\nfrom fast_zero.models import User\nfrom fast_zero.database import get_session\nfrom fast_zero.schemas import UserSchema, UserPublic, UserDB, UserList, Message\n# ...\n@app.post('/users/', response_model=UserPublic, status_code=201)\ndef create_user(user: UserSchema, session: Session = Depends(get_session)):\ndb_user = session.scalar(\nselect(User).where(User.username == user.username)\n)\nif db_user:\nraise HTTPException(\nstatus_code=400, detail='Username already registered'\n)\ndb_user = User(\nusername=user.username, password=user.password, email=user.email\n)\nsession.add(db_user)\nsession.commit()\nsession.refresh(db_user)\nreturn db_user\n

      Nesse c\u00f3digo, a fun\u00e7\u00e3o create_user recebe um objeto do tipo UserSchema e uma sess\u00e3o SQLAlchemy, que \u00e9 injetada automaticamente pelo FastAPI usando o Depends. O c\u00f3digo verifica se j\u00e1 existe um usu\u00e1rio com o mesmo nome no banco de dados e, caso n\u00e3o exista, cria um novo usu\u00e1rio, adiciona-o \u00e0 sess\u00e3o e confirma a transa\u00e7\u00e3o.

      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:7:55: F401 [*] `fast_zero.schemas.UserDB` imported but unused\nFound 1 error.\n

      Isso ocorre porque a rota POST era a \u00fanica que estava utilizando UserDB, e agora que modificamos esta rota, podemos remover UserDB dos nossos imports.

      "},{"location":"04/#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 que possamos injetar a sess\u00e3o de banco de dados de teste.

      Vamos criar 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
      from fastapi.testclient import TestClient\nfrom fast_zero.app import app\nfrom fast_zero.database import get_session\n# ...\n@pytest.fixture\ndef client(session):\ndef get_session_override():\nreturn session\nwith TestClient(app) as client:\napp.dependency_overrides[get_session] = get_session_override\nyield client\napp.dependency_overrides.clear()\n

      Com isso, quando o FastAPI tentar injetar a sess\u00e3o em nossos endpoints, ele vai injetar 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):\nresponse = client.post(\n'/users',\njson={\n'username': 'alice',\n'email': 'alice@example.com',\n'password': 'secret',\n},\n)\nassert response.status_code == 201\nassert response.json() == {\n'username': 'alice',\n'email': 'alice@example.com',\n'id': 1,\n}\n

      Agora que temos a nossa fixture configurada, vamos atualizar 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# ...\ntests/test_app.py::test_root_deve_retornar_200_e_ola_mundo PASSED\ntests/test_app.py::test_create_user PASSED\ntests/test_app.py::test_read_users FAILED\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. 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":"04/#modificando-o-endpoint-get-users","title":"Modificando o Endpoint GET /users","text":"

      {dizer que agora vamos atualizar o nosso endpoint de GET para funcionar no modelo real do banco de dados}

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

      {explicar o que s\u00e3o as coisas que adicionamos como offset e limit e porque elas s\u00e3o importantes agora nesses casos}

      "},{"location":"04/#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\u00e3o usu\u00e1rios no banco. Para verificar se o nosso endpoint est\u00e1 funcionando corretamente, vamos criar um novo teste que solicita uma lista de usu\u00e1rios de um banco vazio:

      tests/test_app.py
      def test_read_users(client):\nresponse = client.get('/users')\nassert response.status_code == 200\nassert 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# ...\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, vamos criar uma nova fixture que cria um usu\u00e1rio em nosso banco de dados de teste.

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

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

      tests/conftest.py
      from fast_zero.models import Base, User\n# ...\n@pytest.fixture\ndef user(session):\nuser = User(username='Teste', email='teste@test.com', password='testtest')\nsession.add(user)\nsession.commit()\nsession.refresh(user)\nreturn 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# ...\ndef test_read_users_with_users(client, user):\nuser_schema = UserPublic.model_validate(user).model_dump()\nresponse = client.get('/users/')\nassert 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# ...\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. Vamos resolver isso agora.

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

      {dizer que precisamos alterar o UserPublic que usamos no teste para que ele reconhe\u00e7a o ORM}

      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, vamos adicionar a linha model_config = ConfigDict(from_attributes=True) ao nosso esquema:

      fast_zero/schemas.py
      from pydantic import BaseModel, EmailStr, ConfigDict\n# ...\nclass UserPublic(BaseModel):\nid: int\nusername: str\nemail: EmailStr\nmodel_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# ...\ntests/test_app.py::test_root_deve_retornar_200_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":"04/#modificando-o-endpoint-put-users","title":"Modificando o Endpoint PUT /users","text":"

      Agora, vamos modificar o endpoint PUT de forma semelhante ao que fizemos com os endpoints POST e GET:

      fast_zero/app.py
      @app.put('/users/{user_id}', response_model=UserPublic)\ndef update_user(\nuser_id: int, user: UserSchema, session: Session = Depends(get_session)\n):\ndb_user = session.scalar(select(User).where(User.id == user_id))\nif db_user is None:\nraise HTTPException(status_code=404, detail='User not found')\ndb_user.username = user.username\ndb_user.password = user.password\ndb_user.email = user.email\nsession.commit()\nsession.refresh(db_user)\nreturn 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.

      "},{"location":"04/#adicionando-o-teste-do-put","title":"Adicionando o teste do PUT","text":"

      Tamb\u00e9m precisamos adicionar um teste para o nosso novo endpoint PUT:

      tests/test_app.py
      def test_update_user(client, user):\nresponse = client.put(\n'/users/1',\njson={\n'username': 'bob',\n'email': 'bob@example.com',\n'password': 'mynewpassword',\n},\n)\nassert response.status_code == 200\nassert response.json() == {\n'username': 'bob',\n'email': 'bob@example.com',\n'id': 1,\n}\n
      "},{"location":"04/#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)):\ndb_user = session.scalar(select(User).where(User.id == user_id))\nif db_user is None:\nraise HTTPException(status_code=404, detail='User not found')\nsession.delete(db_user)\nsession.commit()\nreturn {'detail': 'User deleted'}\n

      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":"04/#adicionando-testes-para-delete","title":"Adicionando testes para DELETE","text":"

      Assim como para o endpoint PUT, precisamos adicionar um teste para o nosso endpoint DELETE:

      tests/test_app.py
      def test_delete_user(client, user):\nresponse = client.delete('/users/1')\nassert response.status_code == 200\nassert response.json() == {'detail': 'User deleted'}\n
      "},{"location":"04/#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\u00edcio 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":"04/#commit","title":"Commit","text":"

      Agora que terminamos a atualiza\u00e7\u00e3o dos nossos endpoints, vamos fazer 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":"04/#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.

      Tamb\u00e9m 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.

      "},{"location":"05/","title":"Autentica\u00e7\u00e3o e Autoriza\u00e7\u00e3o com JWT","text":""},{"location":"05/#autenticacao-e-autorizacao-com-jwt","title":"Autentica\u00e7\u00e3o e Autoriza\u00e7\u00e3o com JWT","text":"

      Objetivos da Aula:

      • Um entendimento b\u00e1sico sobre JWT
      • Implementar autentica\u00e7\u00e3o de usu\u00e1rios com JWT.
      • Adicionar l\u00f3gica de autoriza\u00e7\u00e3o aos endpoints de atualiza\u00e7\u00e3o e dele\u00e7\u00e3o.
      • Utilizar a biblioteca Bcrypt para encriptar as senhas dos usu\u00e1rios.
      Caso prefira ver a aula em v\u00eddeo

      Aula Slides C\u00f3digo

      "},{"location":"05/#introducao","title":"Introdu\u00e7\u00e3o","text":"

      Nesta aula, vamos abordar 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. Vamos corrigir isso utilizando a biblioteca Bcrypt para encriptar as senhas.

      "},{"location":"05/#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 tipicamente consiste 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 (ou declara\u00e7\u00f5es) 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":"05/#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":"05/#gerando-tokens-jwt","title":"Gerando tokens JWT","text":"

      Para gerar tokens JWT, precisamos de duas bibliotecas extras: python-jose e passlib. 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 python-jose[cryptography] passlib[bcrypt]\n

      Agora, vamos criar uma fun\u00e7\u00e3o para gerar nossos tokens JWT. Criaremos um novo arquivo para gerenciar a seguran\u00e7a: security.py. Nesse arquivo vamos iniciar a gera\u00e7\u00e3o dos tokens:

      fast_zero/security.py
      from datetime import datetime, timedelta\nfrom jose import jwt\nfrom passlib.context import CryptContext\nSECRET_KEY = 'your-secret-key'  # Isso \u00e9 provis\u00f3rio, vamos ajustar!\nALGORITHM = 'HS256'\nACCESS_TOKEN_EXPIRE_MINUTES = 30\npwd_context = CryptContext(schemes=['bcrypt'], deprecated='auto')\ndef create_access_token(data: dict):\nto_encode = data.copy()\nexpire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)\nto_encode.update({'exp': expire})\nencoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)\nreturn 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 playload do JWT. Em seguida usa a biblioteca jose 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":"05/#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 em que consigamos ver os tokens gerados pelo jose e interagirmos com ele.

      Com isso vamos criar um arquivo chamado tests/test_security.py para efetuar esse teste:

      tests/test_security.py
      from jose import jwt\nfrom fast_zero.security import create_access_token, SECRET_KEY\ndef test_jwt():\ndata = {'test': 'test'}\ntoken = create_access_token(data)\ndecoded = jwt.decode(token, SECRET_KEY, algorithms=['HS256'])\nassert decoded['test'] == data['test']\nassert decoded['exp']  # Testa se o valor de exp foi adicionado ao token\n

      Na pr\u00f3xima se\u00e7\u00e3o, vamos ver como podemos usar a biblioteca passlib para tratar as senhas dos usu\u00e1rios.

      "},{"location":"05/#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.

      Vamos implementar essa funcionalidade usando a biblioteca passlib. Vamos criar 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):\nreturn pwd_context.hash(password)\ndef verify_password(plain_password: str, hashed_password: str):\nreturn 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 passlib.

      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, vamos modificar nossos endpoints para fazer uso dessas fun\u00e7\u00f5es.

      "},{"location":"05/#modificando-os-endpoints-para-usar-a-autenticacao","title":"Modificando os Endpoints para Usar a Autentica\u00e7\u00e3o","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 autentica\u00e7\u00e3o.

      Primeiro, vamos modificar a fun\u00e7\u00e3o create_user para criar um hash da senha antes de armazen\u00e1-la no banco de dados.

      fast_zero/app.py
      from fast_zero.security import get_password_hash\n# ...\n@app.post('/users/', response_model=UserPublic, status_code=201)\ndef create_user(user: UserSchema, session: Session = Depends(get_session)):\ndb_user = session.scalar(select(User).where(User.email == user.email))\nif db_user:\nraise HTTPException(status_code=400, detail='Email already registered')\nhashed_password = get_password_hash(user.password)\ndb_user = User(\nemail=user.email,\nusername=user.username,\npassword=hashed_password,\n)\nsession.add(db_user)\nsession.commit()\nsession.refresh(db_user)\nreturn db_user\n

      Nesse novo c\u00f3digo, estamos usando a fun\u00e7\u00e3o get_password_hash para criar um hash da senha antes de armazen\u00e1-la no banco de dados. Al\u00e9m disso, estamos armazenando o hash da senha, em vez da senha em texto plano, em nosso objeto User.

      "},{"location":"05/#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# ...\ntests/test_app.py::test_root_deve_retornar_200_e_ola_mundo PASSED\ntests/test_app.py::test_create_user PASSED\n
      "},{"location":"05/#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):\naccess_token: str\ntoken_type: str\n
      "},{"location":"05/#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, o que facilita 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":"05/#criando-um-endpoint-de-geracao-do-token_1","title":"Criando um endpoint de gera\u00e7\u00e3o do token","text":"

      Agora vamos criar 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 (\ncreate_access_token,\nget_password_hash,\nverify_password,\n)\n# ...\n@app.post('/token', response_model=Token)\ndef login_for_access_token(\nform_data: OAuth2PasswordRequestForm = Depends(),\nsession: Session = Depends(get_session),\n):\nuser = session.scalar(select(User).where(User.email == form_data.username))\nif not user:\nraise HTTPException(\nstatus_code=400, detail='Incorrect email or password'\n)\nif not verify_password(form_data.password, user.password):\nraise HTTPException(\nstatus_code=400, detail='Incorrect email or password'\n)\naccess_token = create_access_token(data={'sub': user.email})\nreturn {'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":"05/#testando-token","title":"Testando /token","text":"

      Agora vamos escrever um teste para verificar se o nosso novo endpoint est\u00e1 funcionando corretamente.

      tests/test_app.py
      def test_get_token(client, user):\nresponse = client.post(\n'/token',\ndata={'username': user.email, 'password': user.password},\n)\ntoken = response.json()\nassert response.status_code == 200\nassert 'access_token' in token\nassert '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# ...\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/confitest.py
      from fast_zero.security import get_password_hash\n# ...\n@pytest.fixture\ndef user(session):\nuser = User(\nusername='Teste',\nemail='teste@test.com',\npassword=get_password_hash('testtest'),\n)\nsession.add(user)\nsession.commit()\nsession.refresh(user)\nreturn user\n

      Vamos rodar 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# ...\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/confitest.py
      @pytest.fixture\ndef user(session):\npassword = 'testtest'\nuser = User(\nusername='Teste',\nemail='teste@test.com',\npassword=get_password_hash(password),\n)\nsession.add(user)\nsession.commit()\nsession.refresh(user)\nuser.clean_password = 'testtest'\nreturn 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):\nresponse = client.post(\n'/token',\ndata={'username': user.email, 'password': user.clean_password},\n)\ntoken = response.json()\nassert response.status_code == 200\nassert 'access_token' in token\nassert 'token_type' in token\n

      E agora todos os testes devem passar normalmente:

      $ Execu\u00e7\u00e3o no terminal!
      task test\n# ...\ntests/test_app.py::test_root_deve_retornar_200_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, iremos implementar a autoriza\u00e7\u00e3o nos endpoints.

      "},{"location":"05/#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, vamos adicionar 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):\nusername: 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 finalmente obter 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. Vamos cria-l\u00e1 no arquivo security.py:

      fast_zero/security.py
      from datetime import datetime, timedelta\nfrom fastapi import Depends, HTTPException, status\nfrom fastapi.security import OAuth2PasswordBearer\nfrom jose import JWTError, jwt\nfrom passlib.context import CryptContext\nfrom sqlalchemy import select\nfrom sqlalchemy.orm import Session\nfrom fast_zero.database import get_session\nfrom fast_zero.models import User\nfrom fast_zero.schemas import TokenData\n# ...\noauth2_scheme = OAuth2PasswordBearer(tokenUrl=\"token\")\nasync def get_current_user(\nsession: Session = Depends(get_session),\ntoken: str = Depends(oauth2_scheme),\n):\ncredentials_exception = HTTPException(\nstatus_code=status.HTTP_401_UNAUTHORIZED,\ndetail='Could not validate credentials',\nheaders={'WWW-Authenticate': 'Bearer'},\n)\ntry:\npayload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])\nusername: str = payload.get('sub')\nif not username:\nraise credentials_exception\ntoken_data = TokenData(username=username)\nexcept JWTError:\nraise credentials_exception\nuser = session.scalar(\nselect(User).where(User.email == token_data.username)\n)\nif user is None:\nraise credentials_exception\nreturn user\n

      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":"05/#aplicacao-da-protecao-ao-endpoint","title":"Aplica\u00e7\u00e3o da prote\u00e7\u00e3o ao endpoint","text":"

      Primeiro, vamos aplicar 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 (\ncreate_access_token,\nget_current_user,\nget_password_hash,\nverify_password,\n)\n# ...\n@app.put('/users/{user_id}', response_model=UserPublic)\ndef update_user(\nuser_id: int,\nuser: UserSchema,\nsession: Session = Depends(get_session),\ncurrent_user: User = Depends(get_current_user),\n):\nif current_user.id != user_id:\nraise HTTPException(status_code=400, detail='Not enough permissions')\ncurrent_user.username = user.username\ncurrent_user.password = user.password\ncurrent_user.email = user.email\nsession.commit()\nsession.refresh(current_user)\nreturn 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, vamos aplicar 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(\nuser_id: int,\nsession: Session = Depends(get_session),\ncurrent_user: User = Depends(get_current_user),\n):\nif current_user.id != user_id:\nraise HTTPException(status_code=400, detail='Not enough permissions')\nsession.delete(current_user)\nsession.commit()\nreturn {'detail': '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":"05/#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):\nresponse = client.post(\n'/token',\ndata={'username': user.email, 'password': user.clean_password},\n)\nreturn 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):\nresponse = client.put(\nf'/users/{user.id}',\nheaders={'Authorization': f'Bearer {token}'},\njson={\n'username': 'bob',\n'email': 'bob@example.com',\n'password': 'mynewpassword',\n},\n)\nassert response.status_code == 200\nassert response.json() == {\n'username': 'bob',\n'email': 'bob@example.com',\n'id': 1,\n}\ndef test_delete_user(client, user, token):\nresponse = client.delete(\nf'/users/{user.id}',\nheaders={'Authorization': f'Bearer {token}'},\n)\nassert response.status_code == 200\nassert response.json() == {'detail': 'User deleted'}\n

      Finalmente, podemos rodar todos os testes para garantir que tudo esteja funcionando corretamente.

      $ Execu\u00e7\u00e3o no terminal!
      task test\n# ...\ntests/test_app.py::test_root_deve_retornar_200_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":"05/#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":"05/#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!

      "},{"location":"06/","title":"Refatorando a Estrutura do Projeto","text":""},{"location":"06/#refatorando-a-estrutura-do-projeto","title":"Refatorando a Estrutura do Projeto","text":"

      Objetivos da Aula:

      • Reestruturar o projeto para facilitar sua manuten\u00e7\u00e3o
      • Mover coisas de autentica\u00e7\u00e3o para um arquivo chamado fast_zero/auth.py
      • Deixando em fast_zero/secutiry.py somente as valida\u00e7\u00f5es de senha
      • Remover constantes usados em c\u00f3digo (SECRET_KEY, ALGORITHM e ACCESS_TOKEN_EXPIRE_MINUTES) usando a classe Settings do arquivo fast_zero/settings/py que j\u00e1 temos e movendo para vari\u00e1veis de ambiente no arquivo .env
      • Criar routers espec\u00edficos para rotas que tratam das funcionalidades de usu\u00e1rios e para as de autentica\u00e7\u00e3o
      Caso prefira ver a aula em v\u00eddeo

      Aula Slides C\u00f3digo

      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: vamos refatorar 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":"06/#criando-routers","title":"Criando Routers","text":"

      O FastAPI nos fornece um recurso \u00fatil chamado routers, que nos permite organizar e agrupar diferentes rotas em nossa aplica\u00e7\u00e3o. Em outras palavras, um router \u00e9 um \"subaplicativo\" FastAPI que pode ser montado em uma aplica\u00e7\u00e3o principal.

      Ao usar routers, podemos manter nosso c\u00f3digo mais organizado e leg\u00edvel, especialmente \u00e0 medida que nossa aplica\u00e7\u00e3o cresce e adicionamos mais rotas.

      Vamos come\u00e7ar criando uma nova estrutura de diret\u00f3rios dentro de fast_zero chamada routes, onde teremos um arquivo users.py que ser\u00e1 respons\u00e1vel por todas as rotas relacionadas a usu\u00e1rios. Isso nos ajudar\u00e1 a manter o c\u00f3digo relacionado aos usu\u00e1rios centralizado em um \u00fanico lugar.

      "},{"location":"06/#criando-um-router-para-users","title":"Criando um router para Users","text":"fast_zero/routes/users.py
      from fastapi import APIRouter, Depends, HTTPException\nfrom sqlalchemy import select\nfrom sqlalchemy.orm import Session\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 (\nget_current_user,\nget_password_hash,\n)\nrouter = APIRouter(prefix='/users', tags=['users'])\n

      Neste bloco de c\u00f3digo, criamos uma inst\u00e2ncia do APIRouter com o prefixo '/users'. Isso nos permitir\u00e1 definir as rotas relacionadas aos usu\u00e1rios neste router, em vez de no aplicativo principal. Agora, ao inv\u00e9s de usarmos @app, usaremos @router para definir nossas rotas. O par\u00e2metro tags ajuda na organiza\u00e7\u00e3o e documenta\u00e7\u00e3o das rotas, associando-as a uma tag espec\u00edfica (neste caso, 'users'), que ser\u00e1 exibida na interface de documenta\u00e7\u00e3o autom\u00e1tica gerada pelo FastAPI.

      fast_zero/routes/users.py
      @router.post('/', response_model=UserPublic, status_code=201)\n@router.get('/', response_model=UserList)\n@router.put('/{user_id}', response_model=UserPublic)\n@router.delete('/{user_id}', response_model=Message)\n

      Gra\u00e7as ao uso do prefixo no router, podemos simplificar os paths de nossos endpoints. Por exemplo, em vez de definir o path como '/users/{user_id}', podemos simplesmente usar '/{user_id}'.

      Exemplo do arquivo fast_zero/routes/users.py completo fast_zero/routes/users.py
      from fastapi import APIRouter, Depends, HTTPException\nfrom sqlalchemy import select\nfrom sqlalchemy.orm import Session\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 (\nget_current_user,\nget_password_hash,\n)\nrouter = APIRouter(prefix='/users')\n@router.post('/', response_model=UserPublic, status_code=201)\ndef create_user(user: UserSchema, session: Session = Depends(get_session)):\ndb_user = session.scalar(select(User).where(User.email == user.email))\nif db_user:\nraise HTTPException(status_code=400, detail='Email already registered')\nhashed_password = get_password_hash(user.password)\ndb_user = User(\nemail=user.email,\nusername=user.username,\npassword=hashed_password,\n)\nsession.add(db_user)\nsession.commit()\nsession.refresh(db_user)\nreturn db_user\n@router.get('/', response_model=UserList)\ndef read_users(\nskip: int = 0, limit: int = 100, session: Session = Depends(get_session)\n):\nusers = session.scalars(select(User).offset(skip).limit(limit)).all()\nreturn {'users': users}\n@router.put('/{user_id}', response_model=UserPublic)\ndef update_user(\nuser_id: int,\nuser: UserSchema,\nsession: Session = Depends(get_session),\ncurrent_user: User = Depends(get_current_user),\n):\nif current_user.id != user_id:\nraise HTTPException(status_code=400, detail='Not enough permissions')\ncurrent_user.username = user.username\ncurrent_user.password = user.password\ncurrent_user.email = user.email\nsession.commit()\nsession.refresh(current_user)\nreturn current_user\n@router.delete('/{user_id}', response_model=Message)\ndef delete_user(\nuser_id: int,\nsession: Session = Depends(get_session),\ncurrent_user: User = Depends(get_current_user),\n):\nif current_user.id != user_id:\nraise HTTPException(status_code=400, detail='Not enough permissions')\nsession.delete(current_user)\nsession.commit()\nreturn {'detail': 'User deleted'}\n

      Por termos criados as tags, isso reflete na organiza\u00e7\u00e3o do swagger

      "},{"location":"06/#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. Vamos dar 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 fastapi import APIRouter, Depends, HTTPException\nfrom fastapi.security import OAuth2PasswordRequestForm\nfrom sqlalchemy import select\nfrom sqlalchemy.orm import Session\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\nrouter = APIRouter(tags=['token'])\n@router.post('/token', response_model=Token)\ndef login_for_access_token(\nform_data: OAuth2PasswordRequestForm = Depends(),\nsession: Session = Depends(get_session),\n):\nuser = session.scalar(select(User).where(User.email == form_data.username))\nif not user:\nraise HTTPException(\nstatus_code=400, detail='Incorrect email or password'\n)\nif not verify_password(form_data.password, user.password):\nraise HTTPException(\nstatus_code=400, detail='Incorrect email or password'\n)\naccess_token = create_access_token(data={'sub': user.email})\nreturn {'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.

      Essa estrutura oferece algumas vantagens. Em primeiro lugar, nosso c\u00f3digo se torna mais organizado e mais f\u00e1cil de manter. Em segundo lugar, a separa\u00e7\u00e3o de responsabilidades (o chamado princ\u00edpio SoC) torna o c\u00f3digo mais f\u00e1cil de entender e testar. Por fim, nos permite escalar nosso aplicativo de maneira mais eficaz, pois podemos facilmente adicionar mais routers conforme o projeto cresce.

      "},{"location":"06/#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 fastapi import FastAPI\nfrom fast_zero.routes import auth, users\napp = FastAPI()\napp.include_router(users.router)\napp.include_router(auth.router)\n@app.get('/')\ndef read_root():\nreturn {'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":"06/#executando-os-testes","title":"Executando os testes","text":"

      Depois de refatorar nosso c\u00f3digo, \u00e9 crucial verificar se tudo ainda est\u00e1 funcionando como esperado. Para isso, executamos nossos testes novamente.

      $ Execu\u00e7\u00e3o no terminal!
      task test\n# ...\ntests/test_app.py::test_root_deve_retornar_200_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":"06/#reestruturando-os-arquivos-de-testes","title":"Reestruturando os arquivos de testes","text":"

      Agora que o nosso projeto foi reestruturado, \u00e9 importante manter a organiza\u00e7\u00e3o nos arquivos de teste tamb\u00e9m. Agora, temos 3 arquivos de teste diferentes:

      • /tests/test_app.py: Para testes relacionados ao aplicativo em geral
      • /tests/test_auth.py: Para testes relacionados \u00e0 autentica\u00e7\u00e3o e token
      • /tests/test_users.py: Para testes relacionados \u00e0s rotas de usu\u00e1rios

      Vamos adaptar os testes para se encaixarem nessa nova estrutura.

      "},{"location":"06/#ajustando-os-testes-para-auth","title":"Ajustando os testes para Auth","text":"

      Vamos come\u00e7ar 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
      def test_get_token(client, user):\nresponse = client.post(\n'/token',\ndata={'username': user.email, 'password': user.clean_password},\n)\ntoken = response.json()\nassert response.status_code == 200\nassert 'access_token' in token\nassert 'token_type' in token\n
      "},{"location":"06/#ajustando-os-testes-para-user","title":"Ajustando os testes para User","text":"

      Em seguida, vamos mover os testes relacionados ao usu\u00e1rio para o arquivo /tests/test_users.py.

      /tests/test_users.py
      from fast_zero.schemas import UserPublic\ndef test_create_user(client):\nresponse = client.post(\n'/users/',\njson={\n'username': 'alice',\n'email': 'alice@example.com',\n'password': 'secret',\n},\n)\nassert response.status_code == 201\nassert response.json() == {\n'username': 'alice',\n'email': 'alice@example.com',\n'id': 1,\n}\ndef test_read_users(client):\nresponse = client.get('/users/')\nassert response.status_code == 200\nassert response.json() == {'users': []}\ndef test_read_users_with_users(client, user):\nuser_schema = UserPublic.model_validate(user).model_dump()\nresponse = client.get('/users/')\nassert response.json() == {'users': [user_schema]}\ndef test_update_user(client, user, token):\nresponse = client.put(\nf'/users/{user.id}',\nheaders={'Authorization': f'Bearer {token}'},\njson={\n'username': 'bob',\n'email': 'bob@example.com',\n'password': 'mynewpassword',\n},\n)\nassert response.status_code == 200\nassert response.json() == {\n'username': 'bob',\n'email': 'bob@example.com',\n'id': 1,\n}\ndef test_delete_user(client, user, token):\nresponse = client.delete(\nf'/users/{user.id}',\nheaders={'Authorization': f'Bearer {token}'},\n)\nassert response.status_code == 200\nassert response.json() == {'detail': 'User deleted'}\n

      Essa organiza\u00e7\u00e3o facilita a manuten\u00e7\u00e3o dos testes, tornando-os mais claros e alinhados com a estrutura do nosso projeto.

      "},{"location":"06/#executando-os-testes_1","title":"Executando os testes","text":"

      Ap\u00f3s essa reestrutura\u00e7\u00e3o, \u00e9 importante garantir que tudo ainda est\u00e1 funcionando corretamente. Vamos executar os testes novamente para confirmar isso.

      $ Execu\u00e7\u00e3o no terminal!
      task test\n# ...\ntests/test_app.py::test_root_deve_retornar_200_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":"06/#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/routes/users.py
      from typing import Annotated\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/routes/users.py
      @router.post('/', response_model=UserPublic, status_code=201)\ndef create_user(user: UserSchema, session: Session):\n# ...\n@router.get('/', response_model=UserList)\ndef read_users(session: Session, skip: int = 0, limit: int = 100):\n# ...\n@router.put('/{user_id}', response_model=UserPublic)\ndef update_user(\nuser_id: int,\nuser: UserSchema,\nsession: Session,\ncurrent_user: CurrentUser\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# ...\nOAuth2Form = Annotated[OAuth2PasswordRequestForm, Depends()]\nSession = Annotated[Session, Depends(get_session)]\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":"06/#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":"06/#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. Vamos alterar essa classe para incluir estas constantes.

      fast_zero/settings.py
      from pydantic_settings import BaseSettings, SettingsConfigDict\nclass Settings(BaseSettings):\nmodel_config = SettingsConfigDict(\nenv_file='.env', env_file_encoding='utf-8'\n)\nDATABASE_URL: str\nSECRET_KEY: str\nALGORITHM: str\nACCESS_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":"06/#removendo-as-constantes-do-codigo","title":"Removendo as constantes do c\u00f3digo","text":"

      Primeiramente, vamos carregar 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\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, vamos alterar para usar as constantes da classe Settings:

      fast_zero/security.py
      def create_access_token(data: dict):\nto_encode = data.copy()\nexpire = datetime.utcnow() + timedelta(\nminutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES\n)\nto_encode.update({'exp': expire})\nencoded_jwt = jwt.encode(\nto_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM\n)\nreturn 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.

      "},{"location":"06/#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, vamos rodar todos os testes que temos at\u00e9 agora.

      $ Execu\u00e7\u00e3o no terminal!
      task test\n# ...\ntests/test_app.py::test_root_deve_retornar_200_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":"06/#commit","title":"Commit","text":"

      Para finalizar, vamos criar 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":"06/#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 hoje, estamos em uma boa posi\u00e7\u00e3o para continuar a expandir nossa API no futuro.

      Na pr\u00f3xima aula, vamos explorar mais sobre autentica\u00e7\u00e3o e como gerenciar tokens de acesso e de atualiza\u00e7\u00e3o em nossa API FastAPI. Fique ligado!

      "},{"location":"07/","title":"[OK] Tornando o sistema de autentica\u00e7\u00e3o robusto","text":""},{"location":"07/#ok-tornando-o-sistema-de-autenticacao-robusto","title":"[OK] Tornando o sistema de autentica\u00e7\u00e3o robusto","text":"

      Objetivos da Aula:

      • Testar os casos de autentica\u00e7\u00e3o de forma correta
      • Implementar o refresh do token
      • Introduzir testes que param o tempo com freezegun
      • Introduzir gera\u00e7\u00e3o de modelos autom\u00e1tica com factory-boy
      Caso prefira ver a aula em v\u00eddeo

      Aula Slides C\u00f3digo

      Na aula de hoje, vamos aprofundar nosso sistema de autentica\u00e7\u00e3o. J\u00e1 vimos em aulas anteriores como criar um sistema de autentica\u00e7\u00e3o b\u00e1sico, mas h\u00e1 muitas \u00e1reas em que podemos torn\u00e1-lo mais robusto. Por exemplo, como podemos lidar com situa\u00e7\u00f5es em que as coisas d\u00e3o errado? Como podemos garantir que nosso sistema seja seguro mesmo em cen\u00e1rios adversos? Essas s\u00e3o algumas das quest\u00f5es que vamos explorar hoje.

      Vamos come\u00e7ar examinando mais de perto os testes para autentica\u00e7\u00e3o. At\u00e9 agora, s\u00f3 testamos os casos que d\u00e3o certo - ou seja, quando o usu\u00e1rio sempre existe. Mas \u00e9 igualmente importante testar o que acontece quando as coisas d\u00e3o errado. Afinal, n\u00e3o podemos simplesmente assumir que tudo sempre vai correr bem. Por isso, vamos aprender como testar esses casos negativos.

      Em seguida, vamos implementar um recurso importante em qualquer sistema de autentica\u00e7\u00e3o: o refresh do token. Isso nos permite manter a sess\u00e3o do usu\u00e1rio ativa, mesmo se o token original expirar.

      "},{"location":"07/#testes-para-autenticacao","title":"Testes para autentica\u00e7\u00e3o","text":"

      Antes de mergulharmos nos testes, vamos falar 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=400, 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":"07/#testando-a-alteracao-de-um-usuario-nao-autorizado","title":"Testando a altera\u00e7\u00e3o de um usu\u00e1rio n\u00e3o autorizado","text":"

      Agora, vamos come\u00e7ar a escrever 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, vamos criar um novo teste chamado test_update_user_with_wrong_user.

      tests/test_users.py
      def test_update_user_with_wrong_user(client, user, token):\nresponse = client.put(\nf'/users/{user.id}',\nheaders={'Authorization': f'Bearer {token}'},\njson={\n'username': 'bob',\n'email': 'bob@example.com',\n'password': 'mynewpassword',\n},\n)\nassert response.status_code == 400\nassert 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":"07/#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

      Depois de 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# ...\nclass UserFactory(factory.Factory):\nclass Meta:\nmodel = User\nid = factory.Sequence(lambda n: n)\nusername = factory.LazyAttribute(lambda obj: f'test{obj.id}')\nemail = factory.LazyAttribute(lambda obj: f'{obj.username}@test.com')\npassword = factory.LazyAttribute(lambda obj: f'{obj.username}@example.com')\n

      Explicando linha a linha, esse c\u00f3digo faz o seguinte:

      • class UserFactory(factory.Factory): Define uma f\u00e1brica para o modelo User, herdando de factory.Factory.
      • class Meta: Uma classe interna Meta \u00e9 usada para configurar a f\u00e1brica.
      • model = User: Define o modelo para o qual a f\u00e1brica est\u00e1 construindo inst\u00e2ncias. No caso, estamos referenciando a classe User, que deve ser um modelo de banco de dados, como o SQLAlchemy.
      • id = factory.Sequence(lambda n: n): Define um campo id que recebe uma sequ\u00eancia. A cada chamada da f\u00e1brica, o valor n \u00e9 incrementado, ent\u00e3o cada inst\u00e2ncia gerada ter\u00e1 um id \u00fanico.
      • username = factory.LazyAttribute(lambda obj: f'test{obj.id}'): Define o campo username usando um atributo pregui\u00e7oso, baseado no id.
      • email = factory.LazyAttribute(lambda obj: f'{obj.username}@test.com'): Define o campo email gerado a partir do username.
      • password = factory.LazyAttribute(lambda obj: f'{obj.username}@example.com'): Define o campo password similar ao campo email.

      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):\npassword = 'testtest'\nuser = UserFactory(password=get_password_hash(password))\nsession.add(user)\nsession.commit()\nsession.refresh(user)\nuser.clean_password = 'testtest'\nreturn user\n@pytest.fixture\ndef other_user(session):\npassword = 'testtest'\nuser = UserFactory(password=get_password_hash(password))\nsession.add(user)\nsession.commit()\nsession.refresh(user)\nuser.clean_password = 'testtest'\nreturn 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):\nresponse = client.put(\nf'/users/{other_user.id}',\nheaders={'Authorization': f'Bearer {token}'},\njson={\n'username': 'bob',\n'email': 'bob@example.com',\n'password': 'mynewpassword',\n},\n)\nassert response.status_code == 400\nassert response.json() == {'detail': 'Not enough permissions'}\n

      Neste caso, n\u00e3o estamos usando a fixture user porque queremos simular um cen\u00e1rio em que 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# ...\ntests/test_app.py::test_root_deve_retornar_200_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":"07/#testando-o-delete-com-o-usuario-errado","title":"Testando o DELETE com o usu\u00e1rio errado","text":"

      Continuando nossos testes, agora vamos testar 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 vamos usar:

      tests/test_users.py
      def test_delete_user_wrong_user(client, other_user, token):\nresponse = client.delete(\nf'/users/{other_user.id}',\nheaders={'Authorization': f'Bearer {token}'},\n)\nassert response.status_code == 400\nassert 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# ...\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, vamos passar 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. Vamos ver isso na pr\u00f3xima se\u00e7\u00e3o.

      "},{"location":"07/#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, vamos usar uma biblioteca chamada freezegun. freezegun \u00e9 uma biblioteca Python que nos permite controlar o tempo durante nossos testes, o que \u00e9 perfeito para testar a expira\u00e7\u00e3o do token. {aqui voc\u00ea deve expandir esse t\u00f3pico explicando um pouco mais sobre o freezegun}

      Primeiro, vamos precisar instalar a biblioteca:

      poetry add --group dev freezegun\n

      Agora vamos criar nosso teste. Vamos come\u00e7ar 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.

      tests/test_auth.py
      from freezegun import freeze_time\n# ...\ndef test_token_expiry(client, user):\nwith freeze_time('2023-07-14 12:00:00'):\nresponse = client.post(\n'/auth/token',\ndata={'username': user.email, 'password': user.clean_password},\n)\nassert response.status_code == 200\ntoken = response.json()['access_token']\nwith freeze_time('2023-07-14 13:00:00'):\nresponse = client.post(\n'/auth/refresh_token',\nheaders={'Authorization': f'Bearer {token}'},\n)\nassert response.status_code == 401\nassert 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, vamos executar nosso teste e ver o que acontece:

      $ Execu\u00e7\u00e3o no terminal!
      task test\n# ...\ntests/test_users.py::test_token_expiry PASSED\n

      \u00d3timo, nosso teste passou! Isso confirma que nosso sistema est\u00e1 lidando corretamente com a expira\u00e7\u00e3o dos tokens.

      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. Vamos ver como fazer isso na pr\u00f3xima se\u00e7\u00e3o.

      "},{"location":"07/#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. Vamos abordar 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":"07/#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):\nresponse = client.post(\n'/token',\ndata={'username': 'no_user@no_domain.com', 'password': 'testtest'},\n)\nassert response.status_code == 400\nassert response.json() == {'detail': 'Incorrect email or password'}\n
      "},{"location":"07/#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):\nresponse = client.post(\n'/token', data={'username': user.email, 'password': 'wrong_password'}\n)\nassert response.status_code == 400\nassert 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":"07/#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 vamos implementar a fun\u00e7\u00e3o de renova\u00e7\u00e3o de token em nosso c\u00f3digo.

      fast_zero/routes/auth.py
      from fast_zero.security import (\ncreate_access_token,\nget_current_user,\nverify_password,\n)\n# ...\n@router.post('/refresh_token', response_model=Token)\ndef refresh_access_token(\nuser: User = Depends(get_current_user),\n):\nnew_access_token = create_access_token(data={'sub': user.email})\nreturn {'access_token': new_access_token, 'token_type': 'bearer'}\n

      Vamos tamb\u00e9m implementar 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):\nresponse = client.post(\n'/refresh_token',\nheaders={'Authorization': f'Bearer {token}'},\n)\ndata = response.json()\nassert response.status_code == 200\nassert 'access_token' in data\nassert 'token_type' in data\nassert response.json()['token_type'] == 'bearer'\n

      Ainda \u00e9 importante garantir que nosso sistema trate corretamente os tokens expirados. Para isso, vamos adicionar um teste que verifica se um token expirado n\u00e3o pode ser usado para renovar um token.

      tests/test_auth.py
      def test_token_expiry(client, user):\nwith freeze_time('2023-07-14 12:00:00'):\nresponse = client.post(\n'/token',\ndata={'username': user.email, 'password': user.clean_password},\n)\nassert response.status_code == 200\ntoken = response.json()['access_token']\nwith freeze_time('2023-07-14 13:00:00'):\nresponse = client.post(\n'/refresh_token',\nheaders={'Authorization': f'Bearer {token}'},\n)\nassert response.status_code == 401\nassert 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# ...\ntests/test_app.py::test_root_deve_retornar_200_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_expiry 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_expiry 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":"07/#commit","title":"Commit","text":"

      Agora, vamos fazer um commit com as altera\u00e7\u00f5es que fizemos.

      $ Execu\u00e7\u00e3o no terminal!
      git add .\ngit commit -m \"Implement refresh token and add relevant tests\"\n
      "},{"location":"07/#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 hoje vai al\u00e9m do b\u00e1sico, mostrando como lidar com cen\u00e1rios complexos e realistas.

      A implementa\u00e7\u00e3o e os testes que fizemos hoje nos levam um passo adiante no desenvolvimento da nossa aplica\u00e7\u00e3o, deixando-a mais pr\u00f3xima de estar pronta para um ambiente de produ\u00e7\u00e3o.

      Na pr\u00f3xima aula, vamos utilizar a infraestrutura de autentica\u00e7\u00e3o que criamos hoje 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. Esperamos ver voc\u00ea na pr\u00f3xima aula!

      "},{"location":"08/","title":"[WIP] Criando Rotas CRUD para Gerenciamento de Tarefas em FastAPI","text":""},{"location":"08/#wip-criando-rotas-crud-para-gerenciamento-de-tarefas-em-fastapi","title":"[WIP] Criando Rotas CRUD para Gerenciamento de Tarefas em FastAPI","text":"

      Objetivos da Aula:

      • Cria\u00e7\u00e3o das rotas para as opera\u00e7\u00f5es CRUD das tarefas
      • Fazer com s\u00f3 o usu\u00e1rio dono da tarefa possa acessar e modificar suas tarefas
      • Escrita e execu\u00e7\u00e3o dos testes para cada opera\u00e7\u00e3o das tarefas
      Caso prefira ver a aula em v\u00eddeo

      Aula Slides C\u00f3digo

      Ol\u00e1 a todos! Estamos de volta com mais uma aula. Hoje vamos mergulhar na cria\u00e7\u00e3o das rotas CRUD para as nossas tarefas utilizando FastAPI. Essas opera\u00e7\u00f5es s\u00e3o fundamentais para qualquer aplica\u00e7\u00e3o de gerenciamento de tarefas e s\u00e3o o cora\u00e7\u00e3o do nosso sistema. Al\u00e9m disso, garantiremos que apenas o usu\u00e1rio que criou a tarefa possa acess\u00e1-la e modific\u00e1-la, garantindo a seguran\u00e7a e a privacidade dos dados. Vamos come\u00e7ar!

      "},{"location":"08/#estrutura-inicial-do-codigo","title":"Estrutura inicial do c\u00f3digo","text":"

      Primeiro, vamos criar um novo arquivo chamado todos.py dentro do diret\u00f3rio de routes:

      fast_zero/routes/todos.py
      from fastapi import APIRouter\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.

      Depois de definir o roteador, precisamos inclu\u00ed-lo em nossa aplica\u00e7\u00e3o principal. Vamos atualizar o arquivo fast_zero/app.py para incluir as rotas de tarefas que iremos criar:

      fast_zero/app.py
      from fastapi import FastAPI\nfrom fast_zero.routes import auth, todos, users\napp = FastAPI()\napp.include_router(users.router)\napp.include_router(auth.router)\napp.include_router(todos.router)\n@app.get('/')\ndef read_root():\nreturn {'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":"08/#implementacao-da-tabela-no-banco-de-dados","title":"Implementa\u00e7\u00e3o da tabela no Banco de dados","text":"

      Agora, iremos implementar 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.

      from enum import Enum\nfrom sqlalchemy import ForeignKey\nfrom sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship\nclass TodoState(str, Enum):\ndraft = 'draft'\ntodo = 'todo'\ndoing = 'doing'\ndone = 'done'\ntrash = 'trash'\nclass Base(DeclarativeBase):\npass\nclass User(Base):\n__tablename__ = 'users'\nid: Mapped[int] = mapped_column(primary_key=True)\nusername: Mapped[str]\npassword: Mapped[str]\nemail: Mapped[str]\ntodos: Mapped[list['Todo']] = relationship(\nback_populates='user', cascade='all, delete-orphan'\n)\nclass Todo(Base):\n__tablename__ = 'todos'\nid: Mapped[int] = mapped_column(primary_key=True)\ntitle: Mapped[str]\ndescription: Mapped[str]\nstate: Mapped[TodoState]\nuser_id: Mapped[int] = mapped_column(ForeignKey('users.id'))\nuser: Mapped[User] = relationship(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":"08/#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: Session, user: User):\ntodo = Todo(\ntitle='Test Todo',\ndescription='Test Desc',\nstate='draft',\nuser_id=user.id,\n)\nsession.add(todo)\nsession.commit()\nsession.refresh(todo)\nuser = session.scalar(select(User).where(User.id == user.id))\nassert 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 vamos come\u00e7ar a criar os esquemas para esse modelo e, em seguida, os endpoints.

      "},{"location":"08/#schemas-para-todos","title":"Schemas para Todos","text":"

      Vamos criar dois esquemas para nosso modelo de tarefas (todos): TodoSchema e TodoPublic.

      fast_zero/schemas.py
      from fast_zero.models import TodoState\n#...\nclass TodoSchema(BaseModel):\ntitle: str\ndescription: str\nstate: TodoState\nclass TodoPublic(BaseModel):\nid: int\ntitle: str\ndescription: str\nstate: TodoState\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":"08/#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.

      from typing import Annotated\nfrom fastapi import APIRouter, Depends\nfrom sqlalchemy.orm import Session\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\nrouter = APIRouter()\nCurrentUser = Annotated[User, Depends(get_current_user)]\nrouter = APIRouter(prefix='/todos', tags=['todos'])\n@router.post('/', response_model=TodoPublic)\ndef create_todo(\ntodo: TodoSchema,\nuser: CurrentUser,\nsession: Session = Depends(get_session),\n):\ndb_todo: Todo = Todo(\ntitle=todo.title,\ndescription=todo.description,\nstate=todo.state,\nuser_id=user.id,\n)\nsession.add(db_todo)\nsession.commit()\nsession.refresh(db_todo)\nreturn 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":"08/#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):\nresponse = client.post(\n'/todos/',\nheaders={'Authorization': f'Bearer {token}'},\njson={\n'title': 'Test todo',\n'description': 'Test todo description',\n'state': 'draft',\n},\n)\nassert 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":"08/#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# ...\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! ###\nop.create_table('todos',\nsa.Column('id', sa.Integer(), nullable=False),\nsa.Column('title', sa.String(), nullable=False),\nsa.Column('description', sa.String(), nullable=False),\nsa.Column('state', sa.Enum('draft', 'todo', 'doing', 'done', 'trash', name='todostate'), nullable=False),\nsa.Column('user_id', sa.Integer(), nullable=False),\nsa.ForeignKeyConstraint(['user_id'], ['users.id'], ),\nsa.PrimaryKeyConstraint('id')\n)\n# ### end Alembic commands ###\ndef downgrade() -> None:\n# ### commands auto generated by Alembic - please adjust! ###\nop.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 (\nid INTEGER NOT NULL,\ntitle VARCHAR NOT NULL,\ndescription VARCHAR NOT NULL,\nstate VARCHAR(5) NOT NULL,\nuser_id INTEGER NOT NULL,\nPRIMARY KEY (id),\nFOREIGN 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":"08/#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 tarrefas \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, que s\u00e3o 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/routes/todos.py
      @router.get('/', response_model=ListTodos)\ndef list_todos(\nsession: Session,\nuser: CurrentUser,\ntitle: str = Query(None),\ndescription: str = Query(None),\nstate: str = Query(None),\noffset: int = Query(None),\nlimit: int = Query(None),\n):\nquery = select(Todo).where(Todo.user_id == user.id)\nif title:\nquery = query.filter(Todo.title.contains(title))\nif description:\nquery = query.filter(Todo.description.contains(description))\nif state:\nquery = query.filter(Todo.state == state)\ntodos = session.scalars(query.offset(offset).limit(limit)).all()\nreturn {'todos': todos}\n

      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.

      • Todo.title.contains(title): Verifica se o t\u00edtulo da tarefa cont\u00e9m a string fornecida.
      • Todo.description.contains(description): Verifica se a descri\u00e7\u00e3o da tarefa cont\u00e9m a string fornecida.
      • Todo.state == state: Compara o estado da tarefa com o valor fornecido.

      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":"08/#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 faz uso de 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\nfrom fast_zero.models import Todo, TodoState, User\n# ...\nclass TodoFactory(factory.Factory):\nclass Meta:\nmodel = Todo\ntitle = factory.Faker('text')\ndescription = factory.Faker('text')\nstate = factory.fuzzy.FuzzyChoice(TodoState)\nuser_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":"08/#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. Vamos separar os testes em pequenos blocos e explicar cada um deles.

      "},{"location":"08/#testando-a-listagem-de-todos","title":"Testando a Listagem de Todos","text":"

      Primeiro, vamos criar um teste b\u00e1sico que verifica se o endpoint est\u00e1 listando todos os objetos Todo.

      tests/test_todos.py
      def test_list_todos(session, client, user, token):\nsession.bulk_save_objects(TodoFactory.create_batch(5, user_id=user.id))\nsession.commit()\nresponse = client.get(\n'/todos/',\nheaders={'Authorization': f'Bearer {token}'},\n)\nassert len(response.json()['todos']) == 5\n

      Este teste valida que todos os 5 objetos Todo s\u00e3o retornados pelo endpoint.

      "},{"location":"08/#testando-a-paginacao","title":"Testando a Pagina\u00e7\u00e3o","text":"

      Em seguida, vamos testar a pagina\u00e7\u00e3o para garantir que o offset e o limite estejam funcionando corretamente.

      tests/test_todos.py
      def test_list_todos_pagination(session, user, client, token):\nsession.bulk_save_objects(TodoFactory.create_batch(5, user_id=user.id))\nsession.commit()\nresponse = client.get(\n'/todos/?offset=1&limit=2',\nheaders={'Authorization': f'Bearer {token}'},\n)\nassert len(response.json()['todos']) == 2\n

      Este teste verifica que, quando aplicado o offset de 1 e o limite de 2, apenas 2 objetos Todo s\u00e3o retornados.

      "},{"location":"08/#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(session, user, client, token):\nsession.bulk_save_objects(\nTodoFactory.create_batch(5, user_id=user.id, title='Test todo 1')\n)\nsession.commit()\nresponse = client.get(\n'/todos/?title=Test todo 1',\nheaders={'Authorization': f'Bearer {token}'},\n)\nassert len(response.json()['todos']) == 5\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":"08/#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(session, user, client, token):\nsession.bulk_save_objects(\nTodoFactory.create_batch(5, user_id=user.id, description='description')\n)\nsession.commit()\nresponse = client.get(\n'/todos/?description=desc',\nheaders={'Authorization': f'Bearer {token}'},\n)\nassert len(response.json()['todos']) == 5\n

      Este teste verifica que, quando filtramos pela descri\u00e7\u00e3o, apenas as tarefas com a descri\u00e7\u00e3o correspondente s\u00e3o retornadas.

      "},{"location":"08/#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(session, user, client, token):\nsession.bulk_save_objects(\nTodoFactory.create_batch(5, user_id=user.id, state=TodoState.draft)\n)\nsession.commit()\nresponse = client.get(\n'/todos/?state=draft',\nheaders={'Authorization': f'Bearer {token}'},\n)\nassert len(response.json()['todos']) == 5\n

      Este teste garante que quando filtramos pelo estado, apenas as tarefas com o estado correspondente s\u00e3o retornadas.

      "},{"location":"08/#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, vamos criar 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(session, user, client, token):\nsession.bulk_save_objects(\nTodoFactory.create_batch(\n5,\nuser_id=user.id,\ntitle='Test todo combined',\ndescription='combined description',\nstate=TodoState.done,\n)\n)\nsession.bulk_save_objects(\nTodoFactory.create_batch(\n3,\nuser_id=user.id,\ntitle='Other title',\ndescription='other description',\nstate=TodoState.todo,\n)\n)\nsession.commit()\nresponse = client.get(\n'/todos/?title=Test todo combined&description=combined&state=done',\nheaders={'Authorization': f'Bearer {token}'},\n)\nassert len(response.json()['todos']) == 5\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":"08/#executando-os-testes","title":"Executando os testes","text":"

      Importante para que n\u00e3o esque\u00e7amos \u00e9 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\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":"08/#endpoints-para-alteracao-e-delecao","title":"Endpoints para altera\u00e7\u00e3o e dele\u00e7\u00e3o","text":"

      {Aqui voc\u00ea deve dizer que para fazer o patch, precisamos de um modelo onde tudo seja opcional}

      fast_zero/schemas.py
      class TodoUpdate(BaseModel):\ntitle: str | None = None\ndescription: str | None = None\ncompleted: str | None = None\n
      fast_zero/routes/todos.py
      @router.delete('/{todo_id}', response_model=Message)\ndef delete_todo(todo_id: int, session: Session, user: CurrentUser):\ntodo = session.scalar(\nselect(Todo).where(Todo.user_id == user.id, Todo.id == todo_id)\n)\nif not todo:\nraise HTTPException(status_code=404, detail='Task not found.')\nsession.delete(todo)\nsession.commit()\nreturn {'detail': 'Task has been deleted successfully.'}\n@router.patch('/{todo_id}', response_model=TodoPublic)\ndef patch_todo(\ntodo_id: int, session: Session, user: CurrentUser, todo: TodoUpdate\n):\ndb_todo = session.scalar(\nselect(Todo).where(Todo.user_id == user.id, Todo.id == todo_id)\n)\nif not db_todo:\nraise HTTPException(status_code=404, detail='Task not found.')\nfor key, value in todo.model_dump(exclude_unset=True).items():\nsetattr(db_todo, key, value)\nsession.add(db_todo)\nsession.commit()\nsession.refresh(db_todo)\nreturn db_todo\n
      "},{"location":"08/#testes-para-esses-endpoints","title":"Testes para esses endpoints","text":"fast_zero/tests/test_todos.py
      def test_delete_todo(session, client, user, token):\ntodo = TodoFactory(id=1, user_id=user.id)\nsession.add(todo)\nsession.commit()\nresponse = client.delete(\nf'/todos/{todo.id}', headers={'Authorization': f'Bearer {token}'}\n)\nassert response.status_code == 200\nassert response.json() == {'detail': 'Task has been deleted successfully.'}\ndef test_delete_todo_error(client, token):\nresponse = client.delete(\nf'/todos/{10}', headers={'Authorization': f'Bearer {token}'}\n)\nassert response.status_code == 404\nassert response.json() == {'detail': 'Task not found.'}\ndef test_patch_todo_error(client, token):\nresponse = client.patch(\n'/todos/10',\njson={},\nheaders={'Authorization': f'Bearer {token}'},\n)\nassert response.status_code == 404\nassert response.json() == {'detail': 'Task not found.'}\ndef test_patch_todo(session, client, user, token):\ntodo = TodoFactory(user_id=user.id)\nsession.add(todo)\nsession.commit()\nresponse = client.patch(\nf'/todos/{todo.id}',\njson={'title': 'teste!'},\nheaders={'Authorization': f'Bearer {token}'},\n)\nassert response.status_code == 200\nassert response.json()['title'] == 'teste!'\n
      "},{"location":"08/#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 \"Implement DELETE and PATCH endpoints for todos\"
      "},{"location":"08/#conclusao","title":"Conclus\u00e3o","text":"

      Parab\u00e9ns! Voc\u00ea acabou de implementar os endpoints de DELETE e PATCH para suas tarefas. Isso permite que os usu\u00e1rios agora possam remover tarefas que n\u00e3o s\u00e3o mais necess\u00e1rias e atualizar os detalhes de tarefas existentes.

      Atrav\u00e9s dos testes, voc\u00ea assegurou que as opera\u00e7\u00f5es de DELETE e PATCH est\u00e3o funcionando corretamente. Al\u00e9m disso, voc\u00ea aprendeu como lidar com situa\u00e7\u00f5es onde a tarefa a ser alterada ou removida n\u00e3o existe.

      Na pr\u00f3xima aula, vamos abordar um t\u00f3pico extremamente relevante no desenvolvimento de aplica\u00e7\u00f5es modernas: a dockeriza\u00e7\u00e3o da nossa aplica\u00e7\u00e3o. Iremos aprender como colocar a nossa aplica\u00e7\u00e3o dentro de um container Docker, o que facilita o deploy e o escalonamento dela. At\u00e9 l\u00e1!

      "},{"location":"09/","title":"[OK] Dockerizando a nossa aplica\u00e7\u00e3o","text":""},{"location":"09/#ok-dockerizando-a-nossa-aplicacao","title":"[OK] Dockerizando a nossa aplica\u00e7\u00e3o","text":"

      Objetivos da aula:

      • Compreender os conceitos b\u00e1sicos do Docker
      • Aprender o que \u00e9 um Dockerfile e sua estrutura
      • Entender como criar uma imagem Docker para a nossa aplica\u00e7\u00e3o FastAPI
      • Aprender a rodar a aplica\u00e7\u00e3o utilizando Docker
      Caso prefira ver a aula em v\u00eddeo

      Aula Slides C\u00f3digo

      Depois de implementar nosso gerenciador de tarefas na aula anterior, temos uma vers\u00e3o est\u00e1vel da nossa aplica\u00e7\u00e3o. Agora, vamos aprender como podemos \"dockerizar\" nossa aplica\u00e7\u00e3o FastAPI. Essa etapa \u00e9 crucial para garantir que nossa aplica\u00e7\u00e3o seja facilmente transport\u00e1vel e possa ser executada em qualquer ambiente sem problemas.

      "},{"location":"09/#o-docker","title":"O Docker","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.

      "},{"location":"09/#criando-nosso-dockerfile","title":"Criando nosso Dockerfile","text":"

      O Dockerfile \u00e9 um arquivo de texto que cont\u00e9m todas as instru\u00e7\u00f5es para construir uma imagem Docker. 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.

      Aqui est\u00e1 o nosso Dockerfile para a aplica\u00e7\u00e3o FastAPI:

      FROM python:3.11-slim\nENV POETRY_VIRTUALENVS_CREATE=false\nRUN pip install poetry\n\nCOPY . .\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.
      3. RUN pip install poetry: Instala o Poetry, nosso gerenciador de pacotes.
      4. COPY . .: Copia todos os arquivos do diret\u00f3rio atual para o cont\u00eainer.
      5. RUN poetry config installer.max-workers 10: Configura o Poetry para usar at\u00e9 10 workers ao instalar pacotes.
      6. RUN poetry install --no-interaction --no-ansi: Instala as depend\u00eancias do nosso projeto sem intera\u00e7\u00e3o e sem cores no output.
      7. EXPOSE 8000: Informa ao Docker que o cont\u00eainer vai escutar na porta 8000.
      8. 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:

      • poetry run define o comando que ser\u00e1 executado dentro do ambiente virtual criado pelo Poetry
      • uvicorn \u00e9 o servidor ASGI que usamos para rodar nossa aplica\u00e7\u00e3o.
      • --host define o host que o servidor vai escutar
      • fast_zero.app:app define o <m\u00f3dulo python>:<objeto> que o servidor vai executar.
      "},{"location":"09/#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

      Este comando l\u00ea o Dockerfile no diret\u00f3rio atual (indicado pelo .) e cria uma imagem com a tag \"fast_zero\", (indicada pelo -t).

      Vamos ent\u00e3o verificar 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":"09/#rodando-o-container","title":"Rodando o container","text":"

      Para rodar o cont\u00eainer, usamos o comando docker run, indicando o nome da imagem que queremos executar e a tag que queremos usar <nome_da_imagem>:<tag> e utilizamos a flag -p 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 fast_zero:latest -p 8000:8000\n

      Este comando iniciar\u00e1 nossa aplica\u00e7\u00e3o dentro de um cont\u00eainer Docker, que estar\u00e1 escutando na porta 8000.

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

      Ap\u00f3s criar o Dockerfile e testar a imagem Docker, podemos fazer o commit das altera\u00e7\u00f5es no Git:

      1. Adicione o Dockerfile e quaisquer outros arquivos modificados ao stage do Git com git add .
      2. Fa\u00e7a o commit das altera\u00e7\u00f5es com git commit -m \"Dockerize application\"
      3. Empurre as altera\u00e7\u00f5es para o reposit\u00f3rio remoto com git push
      "},{"location":"09/#conclusao","title":"Conclus\u00e3o","text":"

      Dockerizar nossa aplica\u00e7\u00e3o FastAPI permite que ela seja executada de forma consistente em qualquer ambiente, facilitando a implanta\u00e7\u00e3o e a distribui\u00e7\u00e3o. Na pr\u00f3xima aula, vamos aprender como podemos levar nossa aplica\u00e7\u00e3o para o pr\u00f3ximo n\u00edvel ao implant\u00e1-la em produ\u00e7\u00e3o usando o Fly.io.

      "},{"location":"10/","title":"[OK] Automatizando os testes com Integra\u00e7\u00e3o Cont\u00ednua","text":""},{"location":"10/#ok-automatizando-os-testes-com-integracao-continua","title":"[OK] Automatizando os testes com Integra\u00e7\u00e3o Cont\u00ednua","text":"

      Objetivos da aula:

      • Compreender o conceito de Integra\u00e7\u00e3o Cont\u00ednua (CI)
      • Familiarizar-se com a ferramenta GitHub Actions
      • Aprender a configurar um pipeline de CI para testar a aplica\u00e7\u00e3o automaticamente
      • Entender a import\u00e2ncia de uma cobertura de testes abrangente
      • Conhecer a plataforma Codecov para monitoramento da cobertura de testes

      Na aula passada, preparamos nossa aplica\u00e7\u00e3o para execu\u00e7\u00e3o em containers Docker. Nesta aula, focaremos em garantir que nossa aplica\u00e7\u00e3o continue funcionando conforme o esperado a cada atualiza\u00e7\u00e3o de c\u00f3digo. Para isso, introduziremos o conceito de Integra\u00e7\u00e3o Cont\u00ednua (CI).

      "},{"location":"10/#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 frequente de c\u00f3digo ao projeto principal. Com cada integra\u00e7\u00e3o - geralmente um commit - \u00e9 disparado um processo automatizado que constr\u00f3i e testa o c\u00f3digo. Isso permite detectar e corrigir problemas rapidamente, contribuindo para a manuten\u00e7\u00e3o da qualidade do software.

      "},{"location":"10/#github-actions","title":"GitHub Actions","text":"

      GitHub Actions \u00e9 um servi\u00e7o fornecido pelo GitHub que permite a automatiza\u00e7\u00e3o de workflows, incluindo a execu\u00e7\u00e3o de testes e implanta\u00e7\u00e3o de software, diretamente em seu reposit\u00f3rio GitHub. Cada tarefa \u00e9 definida como uma \"a\u00e7\u00e3o\", e a\u00e7\u00f5es podem ser combinadas para criar um \"workflow\" que atende a necessidades espec\u00edficas de desenvolvimento.

      "},{"location":"10/#configurando-o-workflow-de-ci","title":"Configurando o workflow de CI","text":"

      Vamos configurar um workflow de CI para nossa aplica\u00e7\u00e3o utilizando o GitHub Actions. Crie um novo arquivo em seu reposit\u00f3rio, sob o diret\u00f3rio .github/workflows/, e copie o seguinte c\u00f3digo:

      .github/workflows/pipeline.yaml
      name: Pipeline\non: [push, pull_request]\njobs:\ntest:\nruns-on: ubuntu-latest\nsteps:\n- name: Copia os arquivos do repo\nuses: actions/checkout@v3\n- name: Instalar o python\nuses: actions/setup-python@v4\nwith:\npython-version: '3.11.1'\n- name: Instalar Poetry\nrun: pip install poetry\n- name: Instalar depend\u00eancias do projeto\nrun: poetry install\n- name: Rodar os testes\nrun: poetry run task test --cov-report=xml\n- name: Subir cobertura para o codecov\nuses: codecov/codecov-action@v3\nwith:\ntoken: ${{ secrets.CODECOV_TOKEN }}\n

      Vamos analisar este arquivo:

      • name: Define o nome do workflow.
      • on: Define os eventos que acionar\u00e3o o workflow (neste caso, push e pull requests).
      • jobs: Define os jobs que ser\u00e3o executados. Nesse caso, temos um job chamado \"test\".
      • runs-on: Define o ambiente em que o job ser\u00e1 executado (neste caso, a \u00faltima vers\u00e3o do Ubuntu).
      • steps: Define os passos do job:
      • actions/checkout@v3: Faz o checkout do reposit\u00f3rio.
      • actions/setup-python@v4: Instala a vers\u00e3o especificada do Python.
      • pip install poetry: Instala a ferramenta Poetry.
      • poetry install: Instala as depend\u00eancias do projeto.
      • poetry run task test --cov-report=xml: Executa os testes e gera um relat\u00f3rio de cobertura em formato xml.
      • codecov/codecov-action@v3: Faz o upload do relat\u00f3rio de cobertura para o Codecov. O token \u00e9 fornecido atrav\u00e9s de uma vari\u00e1vel de ambiente segura definida nas configura\u00e7\u00f5es do reposit\u00f3rio.
      "},{"location":"10/#commit","title":"Commit","text":"

      Ap\u00f3s adicionar e configurar o arquivo do workflow, voc\u00ea deve commitar as mudan\u00e7as em seu reposit\u00f3rio. Siga os passos:

      $ Execu\u00e7\u00e3o no terminal!
      git add .github/workflows/pipeline.yaml\ngit commit -m \"Add CI pipeline\"\ngit push\n
      "},{"location":"10/#conclusao","title":"Conclus\u00e3o","text":"

      A Integra\u00e7\u00e3o Cont\u00ednua \u00e9 uma pr\u00e1tica fundamental no desenvolvimento moderno de software, e o GitHub Actions \u00e9 uma ferramenta poderosa para implementar essa pr\u00e1tica. Ele n\u00e3o apenas ajuda a manter a qualidade do c\u00f3digo ao garantir que todos os testes sejam executados a cada commit, mas tamb\u00e9m permite detectar e corrigir problemas mais cedo no ciclo de desenvolvimento.

      Al\u00e9m disso, monitorar a cobertura de testes com o Codecov nos permite manter um alto padr\u00e3o de qualidade, garantindo que todas as partes do nosso c\u00f3digo sejam testadas.

      Na pr\u00f3xima aula, vamos levar nossa aplica\u00e7\u00e3o ao pr\u00f3ximo n\u00edvel, preparando-a para o deployment em produ\u00e7\u00e3o!

      "},{"location":"11/","title":"[WIP] Fazendo deploy no Fly.io e configurando o PostgreSQL","text":""},{"location":"11/#wip-fazendo-deploy-no-flyio-e-configurando-o-postgresql","title":"[WIP] Fazendo deploy no Fly.io e configurando o PostgreSQL","text":"

      Objetivos da aula:

      • Entender o que \u00e9 o Fly.io e como instalar sua CLI
      • Aprender a fazer o deploy de uma aplica\u00e7\u00e3o Docker no Fly.io
      • Configurar uma inst\u00e2ncia do PostgreSQL no Fly.io
      • Configurar as vari\u00e1veis de ambiente
      • Rodar as migra\u00e7\u00f5es do Alembic
      • Configurar o deploy cont\u00ednuo no Github Actions
      "},{"location":"11/#o-flyio-e-a-instalacao-da-cli","title":"O Fly.io e a instala\u00e7\u00e3o da CLI","text":"

      Na aula anterior, n\u00f3s automatizamos nossos testes e integramos tudo em um pipeline de integra\u00e7\u00e3o e deploy cont\u00ednuos. Agora, vamos aprender a fazer o deploy da nossa aplica\u00e7\u00e3o em um ambiente de produ\u00e7\u00e3o usando o Fly.io.

      O Fly.io \u00e9 uma plataforma de deploy que nos permite lan\u00e7ar nossas aplica\u00e7\u00f5es Docker na nuvem. Ele tamb\u00e9m fornece uma s\u00e9rie de recursos, como balanceamento de carga, cria\u00e7\u00e3o de inst\u00e2ncias de banco de dados e configura\u00e7\u00e3o de vari\u00e1veis de ambiente.

      Para iniciar, precisamos instalar a CLI do Fly.io, chamada flyctl. Voc\u00ea pode baix\u00e1-la no site oficial do Fly.io. Com o flyctl instalado, precisamos fazer login na nossa conta do Fly.io usando o comando:

      $ Execu\u00e7\u00e3o no terminal!
      fly auth login\n

      Este comando ir\u00e1 abrir o navegador para voc\u00ea entrar com suas credenciais do Fly.io.

      "},{"location":"11/#criando-a-aplicacao-no-flyio-e-fazendo-o-deploy","title":"Criando a aplica\u00e7\u00e3o no Fly.io e fazendo o deploy","text":"

      Depois de logado, podemos criar uma nova aplica\u00e7\u00e3o no Fly.io usando o comando:

      $ Execu\u00e7\u00e3o no terminal!
      fly launch\n

      Este comando ir\u00e1 perguntar algumas informa\u00e7\u00f5es sobre sua aplica\u00e7\u00e3o e ent\u00e3o criar\u00e1 uma nova aplica\u00e7\u00e3o no Fly.io. Com nossa aplica\u00e7\u00e3o criada, podemos agora fazer o deploy da nossa imagem Docker usando o comando:

      $ Execu\u00e7\u00e3o no terminal!
      fly deploy --local-only\n

      A op\u00e7\u00e3o --local-only diz para o flyctl construir a imagem Docker localmente e depois fazer o upload dela para o Fly.io.

      "},{"location":"11/#configurando-a-instancia-do-postgresql-no-flyio","title":"Configurando a inst\u00e2ncia do PostgreSQL no Fly.io","text":"

      Agora, vamos criar uma inst\u00e2ncia do PostgreSQL. O Fly.io fornece um servi\u00e7o PostgreSQL que podemos usar para criar uma nova inst\u00e2ncia do PostgreSQL com apenas alguns comandos.

      "},{"location":"11/#configurando-as-variaveis-de-ambiente-e-rodando-as-migracoes-do-alembic","title":"Configurando as vari\u00e1veis de ambiente e rodando as migra\u00e7\u00f5es do Alembic","text":"

      Com a inst\u00e2ncia criada, algumas vari\u00e1veis de ambiente ser\u00e3o automaticamente definidas para n\u00f3s. Para que o Alembic possa executar as migra\u00e7\u00f5es, precisamos configurar a vari\u00e1vel DATABASE_URL no nosso aplicativo para apontar para a inst\u00e2ncia do PostgreSQL do Fly.io.

      $ Execu\u00e7\u00e3o no terminal!
      fly secrets set DATABASE_URL=<value>\n

      Substitua <value> pela string de conex\u00e3o do seu banco de dados PostgreSQL.

      Finalmente, podemos executar nossas migra\u00e7\u00f5es Alembic. Usaremos a CLI do Fly.io para executar o comando dentro de um cont\u00eainer do nosso aplicativo:

      $ Execu\u00e7\u00e3o no terminal!
      fly ssh console --app <your-app-name> 'poetry run alembic upgrade head'\n

      Substitua <your-app-name> pelo nome do seu aplicativo no Fly.io.

      "},{"location":"11/#configurando-o-deploy-continuo-no-github-actions","title":"Configurando o deploy cont\u00ednuo no Github Actions","text":"

      Agora que temos nosso aplicativo funcionando no Fly.io, podemos configurar o Github Actions para fazer o deploy autom\u00e1tico sempre que fizermos um push no nosso reposit\u00f3rio. Para isso, precisaremos adicionar alguns passos ao nosso arquivo de pipeline do Github Actions:

      - name: Build and push Docker image to Fly.io\nrun: |\nflyctl deploy --local-only\nflyctl deploy\n

      Com isso, nossa aplica\u00e7\u00e3o est\u00e1 pronta para uso no Fly.io!

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

      Ao longo desta aula, n\u00f3s mergulhamos no mundo do deploy de aplica\u00e7\u00f5es com o Fly.io, uma plataforma que facilita imensamente a tarefa de colocar nossas aplica\u00e7\u00f5es para funcionar na nuvem. Al\u00e9m disso, tamb\u00e9m tivemos a chance de entender como gerenciar vari\u00e1veis de ambiente de forma segura e eficiente, permitindo a nossa aplica\u00e7\u00e3o se adaptar a diferentes contextos de execu\u00e7\u00e3o.

      Aprendemos como subir nossa imagem Docker no Fly.io e como este processo pode ser simplificado e automatizado. Tamb\u00e9m vimos como \u00e9 poss\u00edvel ter nosso banco de dados rodando no mesmo ambiente da nossa aplica\u00e7\u00e3o, facilitando a manuten\u00e7\u00e3o e a escalabilidade.

      Configuramos e utilizamos o PostgreSQL no Fly.io, o que nos deu uma vis\u00e3o pr\u00e1tica de como gerenciar bancos de dados em um ambiente de produ\u00e7\u00e3o. Ao fazer isso, pudemos integrar ainda mais a nossa aplica\u00e7\u00e3o ao ambiente em que ela est\u00e1 rodando.

      Al\u00e9m disso, exploramos a import\u00e2ncia das migra\u00e7\u00f5es e como elas podem ser gerenciadas usando o Alembic, que nos permitiu atualizar nosso banco de dados de forma controlada e rastre\u00e1vel.

      Finalmente, vimos como podemos automatizar todo o processo de deploy usando o Github Actions. Esta \u00e9 uma pr\u00e1tica extremamente \u00fatil e poderosa, pois permite que a nossa aplica\u00e7\u00e3o esteja sempre atualizada com as \u00faltimas altera\u00e7\u00f5es que fizemos, sem a necessidade de qualquer interven\u00e7\u00e3o manual.

      Com todas essas pe\u00e7as, temos agora uma aplica\u00e7\u00e3o robusta e pronta para escalar, com todos os elementos necess\u00e1rios para ser operada em um ambiente de produ\u00e7\u00e3o real. Estas s\u00e3o ferramentas e pr\u00e1ticas que est\u00e3o no cora\u00e7\u00e3o do desenvolvimento de software moderno, e domin\u00e1-las nos permitir\u00e1 construir aplica\u00e7\u00f5es cada vez mais complexas e eficientes.

      Na pr\u00f3xima aula, faremos uma recapitula\u00e7\u00e3o de tudo o que aprendemos neste curso e discutiremos os pr\u00f3ximos passos. Continue acompanhando para fortalecer ainda mais seus conhecimentos em desenvolvimento de aplica\u00e7\u00f5es com FastAPI, Docker, CI/CD e muito mais. At\u00e9 a pr\u00f3xima aula!

      "},{"location":"12/","title":"[WIP] Despedida","text":""},{"location":"12/#wip-despedida","title":"[WIP] Despedida","text":"

      Objetivos da aula:

      • Fazer uma revis\u00e3o geral dos conceitos e pr\u00e1ticas que abordamos ao longo do curso.
      • Encorajar a explora\u00e7\u00e3o futura e o aprofundamento em \u00e1reas espec\u00edficas.
      • Agradecer a todos que acompanharam esta s\u00e9rie de aulas.
      "},{"location":"12/#introducao","title":"Introdu\u00e7\u00e3o","text":"

      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":"12/#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:

      • FastAPI: Conhecemos e utilizamos o FastAPI, um moderno framework de desenvolvimento web para Python, que nos permite criar APIs de alto desempenho de forma eficiente e com menos c\u00f3digo.

      • Docker: Aprendemos a utilizar o Docker para criar um ambiente isolado e replic\u00e1vel para nossa aplica\u00e7\u00e3o, o que facilita tanto o desenvolvimento quanto o deploy em produ\u00e7\u00e3o.

      • Testes e TDD: Abordamos a import\u00e2ncia dos testes automatizados e da metodologia TDD (Test Driven Development) para garantir a qualidade e a confiabilidade do nosso c\u00f3digo.

      • Banco de dados e migra\u00e7\u00f5es: Trabalhamos com bancos de dados SQL, utilizando o SQLAlchemy para a comunica\u00e7\u00e3o com o banco de dados, e o Alembic para gerenciar as migra\u00e7\u00f5es de banco de dados.

      • Autentica\u00e7\u00e3o e autoriza\u00e7\u00e3o: Implementamos funcionalidades de autentica\u00e7\u00e3o e autoriza\u00e7\u00e3o em nossa aplica\u00e7\u00e3o, utilizando o padr\u00e3o JWT.

      • Integra\u00e7\u00e3o Cont\u00ednua (CI): Utilizamos o Github Actions para criar um pipeline de CI, garantindo que os testes s\u00e3o sempre executados e que o c\u00f3digo mant\u00e9m uma qualidade constante.

      • Deploy em produ\u00e7\u00e3o: Por fim, fizemos o deploy da nossa aplica\u00e7\u00e3o em um ambiente de produ\u00e7\u00e3o real, utilizando o Fly.io, e aprendemos a gerenciar e configurar esse ambiente.

      "},{"location":"12/#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 a pr\u00f3xima!

      "}]} \ No newline at end of file +{"config":{"lang":["pt"],"separator":"[\\s\\-]+","pipeline":["stopWordFilter"]},"docs":[{"location":"","title":"FastAPI do Zero!","text":""},{"location":"#wip-fastapi-do-zero","title":"[WIP] FastAPI do ZERO","text":"

      Esse material ainda est\u00e1 em fase de desenvolvimento. Caso encontre algum erro, ficarei extremamente feliz que voc\u00ea me notifique ou envie um Pull Request!

      Construindo um Projeto com Bancos de Dados, Testes e Deploy

      Boas vindas \u00e0 jornada de aprendizado mais empolgante que voc\u00ea vai embarcar: \"FastAPI do Zero: Criando um Projeto com Bancos de Dados, Testes e Deploy\". Este curso tem como objetivo oferecer um entendimento pr\u00e1tico do framework FastAPI, proporcionando as habilidades essenciais para o desenvolvimento eficiente de APIs. Como integra\u00e7\u00e3o com banco de dados e como fazer testes!

      "},{"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 alto desempenho com anota\u00e7\u00f5es de tipo Python facilita o desenvolvimento de APIs RESTful.

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

      Este curso visa oferecer uma experi\u00eancia pr\u00e1tica completa, desde a configura\u00e7\u00e3o do ambiente de desenvolvimento at\u00e9 o deploy de uma aplica\u00e7\u00e3o FastAPI completa. O objetivo \u00e9 que, ao final do curso, voc\u00ea tenha uma compreens\u00e3o profunda do uso do FastAPI e das melhores pr\u00e1ticas associadas.

      Para isso, construiremos um projeto pr\u00e1tico de gerenciamento de tarefas (uma lista de tarefas) do zero, incluindo a autentica\u00e7\u00e3o do usu\u00e1rio e opera\u00e7\u00f5es CRUD completas. Nesse projeto, faremos uso das ferramentas mais recentes dispon\u00edveis em 2023, incluindo a vers\u00e3o 0.100 do FastAPI, a vers\u00e3o 2.0 do Pydantic e a vers\u00e3o 2.0 do SQLAlchemy ORM. Tamb\u00e9m usaremos o Python 3.11 e o Alembic para gerenciamento de migra\u00e7\u00f5es.

      Este curso tamb\u00e9m d\u00e1 \u00eanfase \u00e0 pr\u00e1tica de testes, utilizando o pytest para garantir que as APIs que constru\u00edmos sejam 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 vamos abordar neste curso:

      1. Configurando um ambiente de desenvolvimento para FastAPI: Vamos come\u00e7ar do absoluto zero, criando e configurando nosso ambiente de desenvolvimento.

      2. Primeiros Passos com FastAPI e TDD: Depois de 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 um outro n\u00edvel.

      4. Autentica\u00e7\u00e3o e Autoriza\u00e7\u00e3o em FastAPI: Vamos construir 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.

      6. Dockerizando e Fazendo Deploy de sua Aplica\u00e7\u00e3o FastAPI: Por fim, vamos aprender 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 est\u00e1 em fase de desenvolvimento e todas as aulas estar\u00e3o dispon\u00edveis no meu canal do YouTube. Voc\u00ea pode conferir outros materiais dispon\u00edveis por l\u00e1 enquanto os v\u00eddeos n\u00e3o saem, ou se inscrever para ser notificado quando os v\u00eddeos sa\u00edrem!

      http://youtube.com/@dunossauro

      Aqui estar\u00e1 listada a playlist quando dispon\u00edvel!

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

      Para aproveitar ao m\u00e1ximo este curso, \u00e9 recomendado que voc\u00ea tenha algum conhecimento pr\u00e9vio de Python. Al\u00e9m disso, algum entendimento b\u00e1sico de desenvolvimento web e APIs RESTful ser\u00e1 \u00fatil, mas n\u00e3o essencial, pois {dizer que a ideia \u00e9 fazer um projeto com foco pr\u00e1tico, ent\u00e3o voc\u00ea pode entender como as coisas s\u00e3o constru\u00eddas at\u00e9 o fim do processo!}

      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. Estruturando seu Projeto e Criando Rotas CRUD
      3. Configurando Banco de Dados e Gerenciando Migra\u00e7\u00f5es com Alembic
      4. Integrando Banco de Dados a API
      5. Autentica\u00e7\u00e3o e Autoriza\u00e7\u00e3o
      6. Refatorando a Estrutura do Projeto
      7. Tornando o sistema de autentica\u00e7\u00e3o robusto
      8. Criando Rotas CRUD para Tarefas
      9. Dockerizando a aplica\u00e7\u00e3o
      10. Automatizando os testes com integra\u00e7\u00e3o cont\u00ednua
      11. Fazendo o deploy no fly.io
      12. Despedida
      "},{"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.

      Eu sou um programador Python muito empolgado e curioso. Toco um projeto pessoal chamado Live de Python a pouco mais de 6 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":"#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 quer dizer que:

      • Voc\u00ea pode copiar e reproduzir esse material em qualquer meio e em qualquer formato;
      • Voc\u00ea pode adaptar esse material e construir outros materiais usando esse material.

      Pontos de aten\u00e7\u00e3o:

      • Voc\u00ea precisa dar os devidos cr\u00e9ditos a esse material onde for usar ou adaptar;
      • Voc\u00ea n\u00e3o pode usar para fins comerciais. Como vender ou usar para obter vantagens comerciais;
      • Todo o material derivado desse material deve ser redistribu\u00eddo com a licen\u00e7a CC BY-NC-SA.
      "},{"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:

      • MkDocs: Para gera\u00e7\u00e3o das p\u00e1ginas est\u00e1ticas usando Markdown
      • Mkdocs-material: Tema para o MKDocs
      • pymdown-extensions: Extens\u00f5es para MKDocs, como emojis, diagramas e blocos estilizados de c\u00f3digo
      • Python-Markdown: Extens\u00e3o do Python para Markdown
      • Mkdocs-video: Extens\u00e3o para o MKDocs exibir os v\u00eddeos na p\u00e1gina
      • Mermaid.js: Constru\u00e7\u00e3o dos diagramas
      • Glaxnimate: Pra criar a anima\u00e7\u00e3o no topo dessa p\u00e1gina
      • Lottie-Player: Pra exibir a anima\u00e7\u00e3o do glaxnimate

      Para os slides:

      • marp: Onde preparei os slides
      • Rose-Pine: Tema que usei no marp
      "},{"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":"

      Estou animado para embarcar nesta jornada de aprendizado com voc\u00ea. Se voc\u00ea est\u00e1 pronto para aprender a construir APIs web eficientes e test\u00e1veis com FastAPI, este \u00e9 definitivamente o curso para voc\u00ea. Estou ansioso para come\u00e7ar a primeira aula, onde vamos configurar nosso ambiente de desenvolvimento e dar nossos primeiros passos com FastAPI e TDD!

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

      Perguntas frequentes que me fizeram ap\u00f3s os v\u00eddeos

      • Que papel de parede \u00e9 esse? \u00c9 uma foto do Liam Wong
      • Qual o tema no shell? Todo o meu tema do Gnome foi customizado com Gradience o tema \u00e9 Pretty In Purple
      • Qual o tema do seu editor? Rebecca
      "},{"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:

      • Introdu\u00e7\u00e3o ao ambiente de desenvolvimento (terminal, ferramentas, etc.)
      • Instala\u00e7\u00e3o do FastAPI e suas depend\u00eancias
      • Configura\u00e7\u00e3o das ferramentas de desenvolvimento
      • Execu\u00e7\u00e3o do primeiro \"Hello, World!\" com FastAPI com testes!
      Caso prefira ver a aula em v\u00eddeo

      Aula Slides C\u00f3digo

      Nesta aula pr\u00e1tica, vamos come\u00e7ar nossa jornada na constru\u00e7\u00e3o de uma API com FastAPI. Esse \u00e9 um moderno e r\u00e1pido (altamente perform\u00e1tico) framework web para constru\u00e7\u00e3o de APIs com Python 3.7+ baseado em Python type hints.

      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, Blue, Isort, pytest e Taskipy.

      Depois de 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 essa aula voc\u00ea vai precisar de algumas ferramentas.

      1. Um editor de texto a sua escolha (Eu vou usar o GNU/Emacs)
      2. Um terminal a sua escolha (Usarei o Terminator)
      3. A 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
      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
      6. Docker: Para criar um container da nossa aplica\u00e7\u00e3o (caso n\u00e3o tenha nenhum experi\u00eancia com docker a Linuxtips tem uma playlist completa e gr\u00e1tis sobre docker no canal deles no Youtube)
      7. OPCIONAL: O pipx pode te ajudar bastante nesses momentos de instala\u00e7\u00f5es
      8. OPCIONAL: O ignr para criar nosso gitignore
      9. OPCIONAL: O gh para criar o reposit\u00f3rio e fazer altera\u00e7\u00f5es sem precisar acessar a p\u00e1gina do Github
      "},{"location":"01/#instalacao-do-python-311","title":"Instala\u00e7\u00e3o do Python 3.11","text":"

      Se voc\u00ea precisar reconstruir o ambiente usado nesse curso, \u00e9 recomendado que voc\u00ea use o pyenv.

      Caso tenha problemas durante a instala\u00e7\u00e3o. O pyenv conta com dois assistentes simplificados para sua configura\u00e7\u00e3o. Para windows, use o pyenv-windows. Para GNU/Linux e MacOS, use o pyenv-installer.

      Navegue at\u00e9 o diret\u00f3rio onde far\u00e1 os exerc\u00edcios e executar\u00e1 os c\u00f3digos de exemplo no seu terminal e digite os seguintes comandos:

      $ Execu\u00e7\u00e3o no terminal!
      pyenv update\npyenv install 3.11:latest\n

      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\n  3.11.4\n  3.12.0b1\n

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

      "},{"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
      "},{"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.

      Vamos inicialmente criar um novo diret\u00f3rio para nosso projeto e navegar para ele:

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

      Ele criar\u00e1 uma estrutura 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

      Para que a vers\u00e3o que instalamos com 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.4  # Essa era a maior vers\u00e3o do 3.11 quando escrevi\n

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

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

      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 um novo projeto Python com Poetry e instalaremos as depend\u00eancias necess\u00e1rias - FastAPI e Uvicorn:

      $ Execu\u00e7\u00e3o no terminal!
      poetry install\npoetry add fastapi uvicorn\n
      "},{"location":"01/#primeira-execucao-de-um-hello-world","title":"Primeira Execu\u00e7\u00e3o de um \"Hello, World!\"","text":"

      Para garantir que tudo est\u00e1 configurado corretamente, vamos criar um pequeno programa \"Hello, World!\" com FastAPI. Em um novo arquivo chamado app.py no diret\u00f3rio fast_zero adicione o seguinte c\u00f3digo:

      fast_zero/app.py
      from fastapi import FastAPI\napp = FastAPI()\n@app.get('/')\ndef read_root():\nreturn {'message': 'Ol\u00e1 Mundo!'}\n

      Agora, podemos iniciar nosso servidor FastAPI com o seguinte comando:

      $ Execu\u00e7\u00e3o no terminal!
      poetry shell  # Para ativar o ambiente virtual\nuvicorn fast_zero.app:app --reload\n

      Acesse http://localhost:8000 no seu navegador, e voc\u00ea deve ver a mensagem \"Hello, World!\" em formato JSON.

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

      As ferramentas de desenvolvimento escolhidas podem variar de acordo com a prefer\u00eancia pessoal. Nesta aula, utilizaremos algumas que s\u00e3o particularmente \u00fateis para demonstrar certos conceitos:

      • taskipy: Ferramenta para automatizar alguns comandos e simplificar o fluxo
      • ruff: Um linter, para dizer se n\u00e3o estamos fazendo nada esquisito no c\u00f3digo
      • blue: Um formatador de c\u00f3digo bastante amig\u00e1vel
      • isort: Uma ferramenta para ordenar os imports em ordem alfab\u00e9tica
      • pytest: Ferramenta para executar testes

      Para instalar as depend\u00eancias, podemos usar um grupo do poetry focado nelas, para n\u00e3o serem usadas em produ\u00e7\u00e3o:

      $ Execu\u00e7\u00e3o no terminal!
      poetry add --group dev pytest pytest-cov taskipy blue ruff httpx isort\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 depend\u00eancias, vamos precisar configurar todas as ferramentas de desenvolvimento no arquivo pyproject.toml.

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

      Vamos usar o ruff na configura\u00e7\u00e3o padr\u00e3o. As \u00fanicas altera\u00e7\u00f5es que faremos ser\u00e3o para que ele siga a PEP-8, usando 80 caracteres por linha e ignorando o ambiente virtual e pasta de migra\u00e7\u00f5es do banco de dados:

      pyproject.toml
      [tool.ruff]\nline-length = 79\nexclude = ['.venv', 'migrations']\n
      "},{"location":"01/#isort","title":"isort","text":"

      Vamos configurar o isort para o formato do Black, que coloca uma instru\u00e7\u00e3o import por linha para evitar conflitos no git e tamb\u00e9m usar a base da PEP-8 para 80 caracteres:

      pyproject.toml
      [tool.isort]\nprofile = \"black\"\nline_length = 79\n
      "},{"location":"01/#pytest","title":"pytest","text":"

      Configuraremos o pytest para reconhecer o caminho base para execu\u00e7\u00e3o dos testes na raiz do projeto .:

      pyproject.toml
      [tool.pytest.ini_options]\npythonpath = \".\"\n
      "},{"location":"01/#blue","title":"blue","text":"

      Configuraremos o blue para excluir o caminho das migra\u00e7\u00f5es quando essas forem utilizadas:

      pyproject.toml
      [tool.blue]\nextend-exclude = '(migrations/)'\n
      "},{"location":"01/#taskipy","title":"Taskipy","text":"

      Para simplificar a execu\u00e7\u00e3o de certos comandos, vamos criar algumas tarefas com o Taskipy.

      pyproject.toml
      [tool.taskipy.tasks]\nlint = 'ruff . && blue --check . --diff'\nformat = 'blue .  && isort .'\nrun = 'uvicorn fast_zero.app:app --reload'\npre_test = 'task lint'\ntest = 'pytest -s -x --cov=fast_zero -vv'\npost_test = 'coverage html'\n

      Os comandos definidos fazem o seguinte:

      • lint: executa o ruff para ver se n\u00e3o temos nenhum problema com o c\u00f3digo e checa se estamos de acordo com a PEP-8
      • format: formata o c\u00f3digo usando blue e isort
      • run: executa o servidor de desenvolvimento do FastAPI
      • pre_test: executa a camada de lint antes de executar os teste
      • test: executa os testes com pytest de forma verbosa (-vv) e adiciona nosso c\u00f3digo como base de cobertura
      • post_test: gera um report de cobertura ap\u00f3s os testes

      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 = [\"dunossauro <mendesxeduardo@gmail.com>\"]\nreadme = \"README.md\"\npackages = [{include = \"fast_zero\"}]\n[tool.poetry.dependencies]\npython = \"3.11.*\"\nfastapi = \"^0.100.0\"\nuvicorn = \"^0.22.0\"\n[tool.poetry.group.dev.dependencies]\npytest = \"^7.4.0\"\npytest-cov = \"^4.1.0\"\ntaskipy = \"^1.11.0\"\nblue = \"^0.9.1\"\nruff = \"^0.0.278\"\nhttpx = \"^0.24.1\"\nisort = \"^5.12.0\"\n[tool.ruff]\nline-length = 79\nexclude = ['.venv', 'migrations']\n[tool.isort]\nprofile = \"black\"\nline_length = 79\n[tool.pytest.ini_options]\npythonpath = \".\"\n[tool.blue]\nextend-exclude = '(migrations/)'\n[tool.taskipy.tasks]\nlint = 'ruff . && blue --check . --diff'\nformat = 'blue .  && isort .'\nrun = 'uvicorn fast_zero.app:app --reload'\npre_test = 'task lint'\ntest = 'pytest -s -x --cov=fast_zero -vv'\npost_test = 'coverage html'\n[build-system]\nrequires = [\"poetry-core\"]\nbuild-backend = \"poetry.core.masonry.api\"\n
      "},{"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 blue nos informar\u00e1 que dever\u00edamos ter adicionado duas linhas antes de uma defini\u00e7\u00e3o de fun\u00e7\u00e3o:

      --- fast_zero/app.py    2023-07-12 21:40:14.590616 +0000\n+++ fast_zero/app.py    2023-07-12 21:48:17.017190 +0000\n@@ -1,7 +1,8 @@\nfrom fastapi import FastAPI\n\napp = FastAPI()\n\n+\n@app.get('/')\n def read_root():\n     return {'message': 'Ol\u00e1 Mundo!'}\nwould reformat fast_zero/app.py\n\nOh no! \ud83d\udca5 \ud83d\udc94 \ud83d\udca5\n1 file would be reformatted, 2 files would be left unchanged.\n

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

      ComandoResultado $ Execu\u00e7\u00e3o no terminal!
      task format\nreformatted fast_zero/app.py\n\nAll done! \u2728 \ud83c\udf70 \u2728\n1 file reformatted, 2 files left unchanged.\nSkipped 2 files\n
      fast_zero/app.py
      from fastapi import FastAPI\napp = FastAPI()\n@app.get('/')\ndef read_root():\nreturn {'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. Um bom lugar para come\u00e7ar isso \u00e9 analisando a cobertura. Vamos executar os testes.

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

      Teremos uma resposta como essa:

      $ Execu\u00e7\u00e3o no terminal!
      All done! \u2728 \ud83c\udf70 \u2728\n3 files would be left unchanged.\n=========================== test session starts ===========================\nplatform linux -- Python 3.11.3, pytest-7.4.0, pluggy-1.2.\ncachedir: .pytest_cache\nrootdir: /home/dunossauro/git/fast_zero\nconfigfile: pyproject.toml\nplugins: cov-4.1.0, anyio-3.7.1\ncollected 0 items\n\n/<path>/site-packages/coverage/control.py:860:\n  CoverageWarning: No data was collected. (no-data-collected)\nself._warn(\"No data was collected.\", slug=\"no-data-collected\")\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      5     0%\n-------------------------------------------\nTOTAL                       5      5     0%\n

      As primeiras duas linhas s\u00e3o referentes ao comando do taskipy pre_test que executa o blue e o ruff antes de cada teste. As linhas seguintes 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 ter encontrado 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, vamos escrever nosso primeiro teste com Pytest.

      Para testar o FastAPI, precisamos de um cliente de teste. Isso pode ser obtido 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\nfrom fast_zero.app import app\nclient = TestClient(app)\n

      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

      Devido ao fato de n\u00e3o ter coletado 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 do endpoint:

      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 fastapi.testclient import TestClient\nfrom fast_zero.app import app\ndef test_root_deve_retornar_200_e_ola_mundo():\nclient = TestClient(app)\nresponse = client.get('/')\nassert response.status_code == 200\nassert response.json() == {'message': 'Ol\u00e1 Mundo!'}\n

      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_200_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 teste de forma intuitiva, podemos entender o que cada passo do teste faz. Essa compreens\u00e3o \u00e9 vital, pois pode nos ajudar a escrever testes no futuro com mais confian\u00e7a e efic\u00e1cia. Para desvendar o m\u00e9todo por tr\u00e1s da nossa abordagem, vamos explorar a 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

      Vamos pegar esse teste que fizemos e entender os passos que fizemos para conseguir testar esse endpoint:

      tests/test_app.py
      from fastapi.testclient import TestClient\nfrom fast_zero.app import app\ndef test_root_deve_retornar_200_e_ola_mundo():\nclient = TestClient(app)  # Arrange\nresponse = client.get('/')  # Act\nassert response.status_code == 200  # Assert\nassert 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 criar nosso reposit\u00f3rio no git e criar nosso arquivo .gitignore:

      $ Execu\u00e7\u00e3o no terminal!
      ignr -p python > .gitignore\ngit init .\ngh repo create\ngit add .\ngit commit -m \"Configura\u00e7\u00e3o inicial do projeto\"\ngit push\n
      "},{"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, vamos aprofundar na estrutura\u00e7\u00e3o da nossa aplica\u00e7\u00e3o FastAPI. At\u00e9 l\u00e1!

      "},{"location":"02/","title":"Estruturando o Projeto e Criando Rotas CRUD","text":""},{"location":"02/#rev-estruturando-o-projeto-e-criando-rotas-crud","title":"[REV] Estruturando o Projeto e Criando Rotas CRUD","text":"

      Revis\u00e3o:

      • Introduzir a fixture para o client nessa aula (no segundo teste (o do get)

      Objetivos dessa aula:

      • Entendimento dos verbos HTTP, JSON e c\u00f3digos de resposta
      • Compreender a estrutura de um projeto FastAPI e como estruturar rotas CRUD (Criar, Ler, Atualizar, Deletar)
      • Aprender sobre a biblioteca Pydantic e sua utilidade na valida\u00e7\u00e3o e serializa\u00e7\u00e3o de dados
      • Implementa\u00e7\u00e3o de rotas CRUD em FastAPI
      • Escrita e execu\u00e7\u00e3o de testes para validar o comportamento das rotas
      Caso prefira ver a aula em v\u00eddeo

      Aula Slides C\u00f3digo

      Boas-vindas de volta \u00e0 nossa s\u00e9rie de cursos \"FastAPI do Zero: Criando um Projeto com Bancos de Dados, Testes e Deploy\". Hoje, na Aula 3, avan\u00e7aremos na estrutura\u00e7\u00e3o do nosso projeto FastAPI e implementar todas as rotas CRUD (Criar, Ler, Atualizar e Deletar) para o nosso recurso de usu\u00e1rio.

      "},{"location":"02/#o-que-e-uma-api","title":"O que \u00e9 uma API?","text":"

      Acr\u00f4nimo de Application Programming Interface (Interface de Programa\u00e7\u00e3o de Aplica\u00e7\u00f5es), uma API \u00e9 um conjunto de regras e protocolos que permitem a comunica\u00e7\u00e3o entre diferentes softwares. As APIs servem como uma ponte entre diferentes programas, permitindo que eles se comuniquem e compartilhem informa\u00e7\u00f5es de maneira eficiente e segura.

      No mundo moderno, as APIs geralmente comunicam usando o formato de dados JSON (JavaScript Object Notation), que \u00e9 uma maneira leve e eficiente de transmitir dados entre a API e o cliente.

      As APIs s\u00e3o fundamentais no mundo da programa\u00e7\u00e3o moderna, ao permitirem a intera\u00e7\u00e3o entre diferentes sistemas, independentemente de como foram projetados ou em que linguagem foram escritos.

      "},{"location":"02/#o-que-e-http","title":"O que \u00e9 HTTP?","text":"

      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.

      No contexto das APIs, o HTTP \u00e9 o protocolo que permite a comunica\u00e7\u00e3o entre o cliente (geralmente o navegador de um usu\u00e1rio, mas pode ser qualquer coisa que saiba como fazer solicita\u00e7\u00f5es HTTP) e o servidor onde a API est\u00e1 hospedada. As informa\u00e7\u00f5es entre o cliente e o servidor s\u00e3o trocadas na forma de JSON, tornando-se uma linguagem universal para a troca de informa\u00e7\u00f5es na web.

      O HTTP \u00e9 baseado no modelo de requisi\u00e7\u00e3o-resposta: o cliente faz uma requisi\u00e7\u00e3o para o servidor, e o servidor responde a essa requisi\u00e7\u00e3o. Essas requisi\u00e7\u00f5es e respostas s\u00e3o formatadas de acordo com as regras do protocolo HTTP.

      A seguir, vamos explorar como os verbos HTTP, os c\u00f3digos de resposta e os c\u00f3digos de erro s\u00e3o utilizados para gerenciar a comunica\u00e7\u00e3o entre o cliente e a API.

      "},{"location":"02/#compreendendo-os-verbos-http-codigos-de-resposta-e-codigos-de-erro","title":"Compreendendo os Verbos HTTP, C\u00f3digos de Resposta e C\u00f3digos de Erro","text":"

      Quando trabalhamos com APIs REST, o uso apropriado dos verbos HTTP, c\u00f3digos de resposta e c\u00f3digos de erro \u00e9 crucial para criar uma API clara e consistente.

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

      Os verbos HTTP indicam a a\u00e7\u00e3o desejada a ser executada em um determinado recurso. Os verbos mais comuns s\u00e3o:

      • GET: Recupera recursos. Deve retornar o status HTTP 200 (OK) se bem-sucedido.
      • POST: Cria um novo recurso. Deve retornar o status HTTP 201 (Criado) se bem-sucedido.
      • PUT: Atualiza um recurso existente. Deve retornar o status HTTP 200 (OK) se bem-sucedido.
      • DELETE: Exclui um recurso. Deve retornar o status HTTP 200 (OK) se bem-sucedido.
      "},{"location":"02/#codigos-de-resposta-http","title":"C\u00f3digos de Resposta HTTP","text":"

      Os c\u00f3digos de resposta HTTP informam ao cliente sobre o resultado de sua solicita\u00e7\u00e3o. Aqui est\u00e3o alguns dos c\u00f3digos de resposta mais comuns:

      • 200 OK: A solicita\u00e7\u00e3o foi bem-sucedida. O significado exato depende do m\u00e9todo HTTP utilizado na solicita\u00e7\u00e3o.
      • 201 Created: A solicita\u00e7\u00e3o foi bem-sucedida e um novo recurso foi criado como resultado.
      • 404 Not Found: O recurso solicitado n\u00e3o p\u00f4de ser encontrado. Este c\u00f3digo \u00e9 frequentemente usado quando o recurso \u00e9 inexistente.
      "},{"location":"02/#codigos-de-erro-http","title":"C\u00f3digos de Erro HTTP","text":"

      Os c\u00f3digos de erro HTTP indicam que houve um problema com a solicita\u00e7\u00e3o. Alguns c\u00f3digos de erro comuns incluem:

      • 400 Bad Request: A solicita\u00e7\u00e3o era inv\u00e1lida ou malformada.
      • 401 Unauthorized: A solicita\u00e7\u00e3o requer autentica\u00e7\u00e3o. Se a solicita\u00e7\u00e3o inclu\u00eda credenciais de autentica\u00e7\u00e3o e mesmo assim retornou 401, isso significa que as credenciais n\u00e3o foram aceitas.
      • 403 Forbidden: O cliente n\u00e3o tem permiss\u00f5es para acessar o recurso solicitado, apesar de estar autenticado.
      • 404 Not Found: O recurso solicitado n\u00e3o existe no servidor.
      • 422 Unprocessable Entity: A solicita\u00e7\u00e3o est\u00e1 bem formada, mas os par\u00e2metros sem\u00e2nticos s\u00e3o inv\u00e1lidos ou incompat\u00edveis.
      • 500 Internal Server Error: Ocorreu um erro inesperado no servidor.

      Ao trabalhar com APIs REST, \u00e9 importante lidar corretamente com esses c\u00f3digos de resposta e erro para proporcionar uma boa experi\u00eancia para os usu\u00e1rios da API.

      "},{"location":"02/#como-acontece-a-comunicacao-web-entre-cliente-e-servidor","title":"Como acontece a comunica\u00e7\u00e3o web entre cliente e servidor","text":"

      A comunica\u00e7\u00e3o entre cliente e servidor na web \u00e9 um processo que ocorre em v\u00e1rias etapas e \u00e9 governado por protocolos de comunica\u00e7\u00e3o espec\u00edficos. O protocolo mais comum \u00e9 o HTTP (Hypertext Transfer Protocol). Essa forma de comunica\u00e7\u00e3o \u00e9 geralmente descrita como stateless, o que significa que cada requisi\u00e7\u00e3o \u00e9 processada de forma independente, sem qualquer conhecimento das requisi\u00e7\u00f5es anteriores.

      A informa\u00e7\u00e3o \u00e9 trocada na forma de mensagens HTTP, que cont\u00eam dados e informa\u00e7\u00f5es sobre como esses dados devem ser processados. Um aspecto fundamental dessa comunica\u00e7\u00e3o \u00e9 a troca de dados na forma de objetos JSON, que s\u00e3o uma maneira eficiente e flex\u00edvel de representar dados estruturados.

      sequenceDiagram\n    participant Cliente\n    participant Servidor\n    Cliente->>Servidor: Requisi\u00e7\u00e3o HTTP (GET, POST, PUT, DELETE)\n    Note right of Servidor: Processa a requisi\u00e7\u00e3o\n    Servidor-->>Cliente: Resposta HTTP (C\u00f3digo de Status)\n    Note left of Cliente: Processa a resposta\n    Cliente->>Servidor: Requisi\u00e7\u00e3o HTTP com JSON (POST, PUT)\n    Note right of Servidor: Processa a requisi\u00e7\u00e3o e o JSON\n    Servidor-->>Cliente: Resposta HTTP com JSON\n    Note left of Cliente: Processa a resposta e o JSON

      Este diagrama representa a sequ\u00eancia b\u00e1sica de uma comunica\u00e7\u00e3o cliente-servidor usando HTTP e JSON:

      • O cliente, geralmente um navegador web ou uma aplica\u00e7\u00e3o, faz uma requisi\u00e7\u00e3o HTTP para o servidor. Essa requisi\u00e7\u00e3o pode ser de diferentes tipos, como GET (para recuperar dados), POST ou PUT (para enviar dados), ou DELETE (para excluir dados).
      • O servidor recebe a requisi\u00e7\u00e3o, processa-a e, em seguida, prepara a resposta. A resposta inclui um c\u00f3digo de status que indica se a requisi\u00e7\u00e3o foi bem-sucedida ou n\u00e3o.
      • O cliente recebe a resposta do servidor e processa-a. Se os dados foram solicitados, eles s\u00e3o exibidos ao usu\u00e1rio. Se foi uma opera\u00e7\u00e3o de envio de dados, a resposta do servidor indicar\u00e1 se a opera\u00e7\u00e3o foi bem-sucedida.
      • Para opera\u00e7\u00f5es que envolvem o envio de dados (como POST e PUT), o cliente inclui os dados na forma de um objeto JSON na requisi\u00e7\u00e3o. O JSON \u00e9 um formato de dados leve e f\u00e1cil de usar que permite a representa\u00e7\u00e3o de estruturas de dados complexas.
      • O servidor recebe a requisi\u00e7\u00e3o com os dados JSON, processa a requisi\u00e7\u00e3o e os dados, e envia uma resposta ao cliente. Esta resposta tamb\u00e9m pode incluir dados no formato JSON.
      • Finalmente, o cliente processa a resposta do servidor e quaisquer dados JSON inclu\u00eddos nela.

      Essa \u00e9 uma vis\u00e3o geral simplificada do processo. Na pr\u00e1tica, a comunica\u00e7\u00e3o entre cliente e servidor pode envolver muitas outras nuances, como autentica\u00e7\u00e3o, redirecionamento, cookies e muito mais.

      "},{"location":"02/#pydantic-e-a-validacao-de-dados","title":"Pydantic e a valida\u00e7\u00e3o de dados","text":"

      Antes de mergulharmos no c\u00f3digo, vamos entender alguns conceitos importantes.

      Caso esse seja seu primeiro contato com Pydantic

      Temos uma live de python exclusiva sobre esse assunto

      Link direto

      O Pydantic \u00e9 uma biblioteca Python que oferece valida\u00e7\u00e3o de dados e configura\u00e7\u00f5es usando anota\u00e7\u00f5es de tipos Python. Ela \u00e9 utilizada extensivamente em FastAPI para lidar com a valida\u00e7\u00e3o e serializa\u00e7\u00e3o/desserializa\u00e7\u00e3o de dados. O Pydantic tem um papel crucial ao trabalhar com JSON, pois permite a valida\u00e7\u00e3o dos dados recebidos neste formato, assim como sua convers\u00e3o para formatos nativos do Python e vice-versa.

      Esquemas: No contexto da programa\u00e7\u00e3o, um esquema \u00e9 uma representa\u00e7\u00e3o estrutural de um objeto ou entidade. Por exemplo, no nosso caso, um usu\u00e1rio pode ser representado por um esquema que cont\u00e9m campos para nome de usu\u00e1rio, e-mail e senha. Esquemas s\u00e3o \u00fateis porque permitem definir a estrutura de um objeto de uma maneira clara e reutiliz\u00e1vel.

      Valida\u00e7\u00e3o de dados: Este \u00e9 o processo de verificar se os dados recebidos est\u00e3o em conformidade com as regras e restri\u00e7\u00f5es definidas. Por exemplo, se esperamos que o campo \"email\" contenha um endere\u00e7o de e-mail v\u00e1lido, a valida\u00e7\u00e3o de dados garantir\u00e1 que os dados inseridos nesse campo de fato correspondam a um formato de e-mail v\u00e1lido.

      Agora, vamos falar sobre a biblioteca Pydantic.

      O Pydantic \u00e9 uma biblioteca Python que oferece valida\u00e7\u00e3o de dados e configura\u00e7\u00f5es usando anota\u00e7\u00f5es de tipos Python. Ela \u00e9 utilizada extensivamente em FastAPI para lidar com a valida\u00e7\u00e3o e serializa\u00e7\u00e3o/desserializa\u00e7\u00e3o de dados.

      O uso do Pydantic nos permite definir modelos de dados, ou \"esquemas\", com campos anotados com tipos de dados. O Pydantic garante que as inst\u00e2ncias desses modelos sempre estejam em conformidade com o esquema definido.

      Vamos considerar um exemplo onde recebemos o seguinte objeto JSON, representando um novo usu\u00e1rio que quer se registrar em nosso servi\u00e7o:

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

      Para lidar com esta entrada de dados, devemos definir um esquema Pydantic que corresponda \u00e0 estrutura deste objeto JSON. Usamos anota\u00e7\u00f5es de tipos Python para definir o tipo de dado de cada campo:

      from pydantic import BaseModel, EmailStr\nclass UserSchema(BaseModel):\nusername: str\nemail: EmailStr\npassword: str\n

      Neste exemplo, o campo username \u00e9 esperado como uma string, o campo email como uma string que valida o formato de um endere\u00e7o de email (gra\u00e7as \u00e0 anota\u00e7\u00e3o EmailStr do Pydantic), e o campo password tamb\u00e9m \u00e9 esperado como uma string.

      Ao usar este esquema, qualquer tentativa de criar um usu\u00e1rio com dados que n\u00e3o correspondam a este formato (por exemplo, um email que n\u00e3o \u00e9 v\u00e1lido, ou um campo de nome de usu\u00e1rio que n\u00e3o \u00e9 uma string) resultar\u00e1 em um erro de valida\u00e7\u00e3o.

      "},{"location":"02/#suporte-a-emails","title":"Suporte a emails","text":"

      Para que o Pydantic suporte a valida\u00e7\u00e3o de emails, \u00e9 necess\u00e1rio instalar o pydantic[email]

      Ademais, se tentarmos criar um usu\u00e1rio com um email inv\u00e1lido, o Pydantic ir\u00e1 automaticamente validar o campo e retornar um erro \u00fatil. Isso nos poupa muito trabalho de valida\u00e7\u00e3o manual e ajuda a manter nossa API robusta e confi\u00e1vel.

      "},{"location":"02/#implementando-as-rotas-crud","title":"Implementando as Rotas CRUD","text":"

      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:

      • Create (Criar): Adicionar novos registros ao banco de dados. No HTTP, essa a\u00e7\u00e3o geralmente \u00e9 associada ao verbo POST.
      • Read (Ler): Recuperar registros existentes do banco de dados. No HTTP, essa a\u00e7\u00e3o geralmente \u00e9 associada ao verbo GET.
      • Update (Atualizar): Modificar registros existentes no banco de dados. No HTTP, essa a\u00e7\u00e3o geralmente \u00e9 associada ao verbo PUT ou PATCH.
      • Delete (Excluir): Remover registros existentes do banco de dados. No HTTP, essa a\u00e7\u00e3o geralmente \u00e9 associada ao verbo DELETE.

      Os c\u00f3digos de status HTTP s\u00e3o usados para indicar o resultado de cada opera\u00e7\u00e3o CRUD. Por exemplo, uma solicita\u00e7\u00e3o POST bem-sucedida (create) retorna o status HTTP 201 (Criado), enquanto uma solicita\u00e7\u00e3o GET bem-sucedida (read) retorna o status HTTP 200 (OK).

      \u00c9 importante notar que, ao trabalhar com FastAPI e Pydantic, nossos esquemas desempenham um papel vital na opera\u00e7\u00e3o de \"Create\" (criar). Ao usar a opera\u00e7\u00e3o POST para adicionar um novo registro ao nosso banco de dados, vamos aproveitar a valida\u00e7\u00e3o de dados do Pydantic para garantir que o novo registro esteja em conformidade com o esquema do nosso modelo de dados. Se os dados enviados na solicita\u00e7\u00e3o POST n\u00e3o passarem na valida\u00e7\u00e3o do Pydantic, nossa API retornar\u00e1 um c\u00f3digo de status HTTP 422 (Unprocessable Entity), indicando que os dados fornecidos s\u00e3o inv\u00e1lidos ou incompletos.

      Agora que temos uma compreens\u00e3o clara do que \u00e9 o CRUD, como se relaciona com os verbos HTTP, os c\u00f3digos de status e a valida\u00e7\u00e3o do Pydantic, podemos passar para a implementa\u00e7\u00e3o dessas opera\u00e7\u00f5es em nossa API FastAPI.

      Na nossa API, vamos criar rotas correspondentes para cada opera\u00e7\u00e3o CRUD, come\u00e7ando com a opera\u00e7\u00e3o \"create\" (criar), que ser\u00e1 implementada pela rota POST.

      "},{"location":"02/#implementando-a-rota-post","title":"Implementando a Rota POST","text":"

      A rota POST \u00e9 usada para criar um novo usu\u00e1rio em nosso sistema. Lembrando, o verbo HTTP POST est\u00e1 relacionado \u00e0 opera\u00e7\u00e3o \"Create\" do CRUD. Se tudo ocorrer como esperado e um novo usu\u00e1rio for criado com sucesso, a rota deve retornar o status HTTP 201 (Criado).

      Para a cria\u00e7\u00e3o dessa rota, vamos usar de base o JSON que criamos anteriormente. Para que a pessoa se cadastre na nossa plataforma, ela precisa enviar os dados de nome de usu\u00e1rio, email e senha:

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

      Para isso, vamos criar um esquema Pydantic equivalente em um arquivo de esquemas: fast_zero/schemas.py:

      fast_zero/schemas.py
      from pydantic import BaseModel, EmailStr\nclass UserSchema(BaseModel):\nusername: str\nemail: EmailStr\npassword: str\n

      Agora vamos criar nosso endpoint que esperar\u00e1 receber esse esquema Pydantic e retornar\u00e1 201, caso o JSON enviado seja v\u00e1lido:

      fast_zero/app.py
      from fastapi import FastAPI\nfrom fast_zero.schemas import UserSchema\n# C\u00f3digo da nossa rota de ol\u00e1 mundo omitido\n@app.post('/users/', status_code=201)\ndef create_user(user: UserSchema):\nreturn user\n

      Com esse endpoint criado, podemos executar a nossa aplica\u00e7\u00e3o:

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

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

      Dessa forma, podemos testar de forma simplificada a nossa API, enviando o JSON e realizando alguns testes.

      Entretanto, precisamos prestar aten\u00e7\u00e3o a um detalhe: nosso modelo retorna a senha do usu\u00e1rio, o que \u00e9 uma p\u00e9ssima pr\u00e1tica de seguran\u00e7a.

      Para evitar isso, podemos criar um novo modelo que ser\u00e1 usado somente para resposta. Dessa forma, n\u00e3o expomos os dados que n\u00e3o queremos na API:

      fast_zero/schemas.py
      class UserPublic(BaseModel):\nusername: str\nemail: EmailStr\n

      Precisamos tamb\u00e9m dizer ao FastAPI que esse ser\u00e1 o modelo de resposta, e converter nosso user em UserPublic:

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

      Note que somente adicionando o response_model, o FastAPI j\u00e1 faz a convers\u00e3o de UserSchema em UserPublic

      Agora, se fizermos de novo a chamada no Swagger, receberemos o mesmo objeto, mas sem expor a senha.

      Caso nunca tenha usado o swagger

      Temos uma live focada em OpenAPI que s\u00e3o as especifica\u00e7\u00f5es do swagger

      Link direto

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

      Finalmente, para brincar com essas rotas, podemos criar uma lista provis\u00f3ria para simular um banco de dados. Assim, podemos adicionar nossos dados e entender como o FastAPI funciona. Para isso, adicionamos uma lista provis\u00f3ria para o \"banco\" e alteramos nosso endpoint para inserir nossos modelos do Pydantic nessa lista:

      fast_zero/app.py
      from fast_zero.schemas import UserSchema, UserPublic, UserDB\n# c\u00f3digo omitido\ndatabase = []  # provis\u00f3rio para estudo!\n@app.post('/users/', status_code=201, response_model=UserPublic)\ndef create_user(user: UserSchema):\nuser_with_id = UserDB(**user.model_dump(), id=len(database) + 1)\ndatabase.append(user_with_id)\nreturn user_with_id\n

      Se queremos uma simula\u00e7\u00e3o de banco de dados, precisamos ter um ID para cada usu\u00e1rio registrado no nosso \"banco\". Sendo assim, vamos alterar nosso modelo de resposta p\u00fablica (UserPublic) para que ele forne\u00e7a o ID de cria\u00e7\u00e3o do usu\u00e1rio. Vamos tamb\u00e9m criar um novo modelo que represente o usu\u00e1rio com sua senha e identificador, que chamaremos de UserDB:

      fast_zero/schemas.py
      class UserPublic(BaseModel):\nid: int\nusername: str\nemail: EmailStr\nclass UserDB(UserSchema):\nid: int\n

      Dessa forma, nada muda. No entanto, podemos prosseguir com a constru\u00e7\u00e3o dos outros endpoints. E lembre-se, \u00e9 importante testar esse endpoint para garantir que tudo esteja funcionando corretamente.

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

      Antes de criar o teste de fato, vamos execut\u00e1-los para ver como anda a nossa cobertura:

      $ Execu\u00e7\u00e3o no terminal!
      task test\n# parte da resposta foi omitida\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.

      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. N\u00f3s 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():\nresponse = client.post(\n'/users/',\njson={\n'username': 'alice',\n'email': 'alice@example.com',\n'password': 'secret',\n},\n)\nassert response.status_code == 201\nassert 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# parte da resposta foi omitida\ntests/test_app.py::test_root_deve_retornar_200_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

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

      "},{"location":"02/#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):\nusers: 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 UserSchema, UserPublic, UserDB, UserList\n# c\u00f3digo omitido\n@app.get('/users/', response_model=UserList)\ndef read_users():\nreturn {'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":"02/#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. N\u00f3s 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():\nresponse = client.get('/users/')\nassert response.status_code == 200\nassert 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. Vamos implementar a pr\u00f3xima opera\u00e7\u00e3o CRUD: Update.

      "},{"location":"02/#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).

      fast_zero/app.py
      from fastapi import FastAPI, HTTPException\n# ...\n@app.put('/users/{user_id}', response_model=UserPublic)\ndef update_user(user_id: int, user: UserSchema):\nif user_id > len(database) or user_id < 1:\nraise HTTPException(status_code=404, detail='User not found')\nuser_with_id = UserDB(**user.model_dump(), id=user_id)\ndatabase[user_id - 1] = user_with_id\nreturn user_with_id\n
      "},{"location":"02/#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. N\u00f3s 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():\nresponse = client.put(\n'/users/1',\njson={\n'username': 'bob',\n'email': 'bob@example.com',\n'password': 'mynewpassword',\n},\n)\nassert response.status_code == 200\nassert 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":"02/#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).

      Para transmitir uma mensagem de sucesso ou falha na opera\u00e7\u00e3o de exclus\u00e3o, podemos criar um modelo chamado Message. Esse modelo ser\u00e1 respons\u00e1vel por embalar uma mensagem que ser\u00e1 retornada na nossa API.

      fast_zero/schemas.py
      class Message(BaseModel):\ndetail: str\n

      Agora podemos criar nosso endpoint DELETE. 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
      from fast_zero.schemas import UserSchema, UserPublic, UserDB, UserList, Message\n# ...\n@app.delete('/users/{user_id}', response_model=Message)\ndef delete_user(user_id: int):\nif user_id > len(database) or user_id < 1:\nraise HTTPException(status_code=404, detail='User not found')\ndel database[user_id - 1]\nreturn {'detail': '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":"02/#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. N\u00f3s 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():\nresponse = client.delete('/users/1')\nassert response.status_code == 200\nassert response.json() == {'detail': 'User deleted'}\n
      "},{"location":"02/#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, vamos verificar 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, vamos adicionar 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 outras pessoas (ou n\u00f3s mesmos, no futuro) possam entender 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":"02/#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 API robusta e 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.

      No pr\u00f3ximo t\u00f3pico, vamos explorar uma das partes mais cr\u00edticas de qualquer aplicativo - a conex\u00e3o e intera\u00e7\u00e3o com um banco de dados. Vamos aprender 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.

      "},{"location":"03/","title":"Configurando o banco de dados e gerenciando migra\u00e7\u00f5es com Alembic","text":""},{"location":"03/#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:

      • Introdu\u00e7\u00e3o ao SQLAlchemy e Alembic
      • Instalando SQLAlchemy e Alembic
      • Configurando e criando o banco de dados
      • Criando e localizando tabelas utilizando SQLAlchemy
      • Testando a cria\u00e7\u00e3o de tabelas
      • Gerenciando migra\u00e7\u00f5es do banco de dados com Alembic
      Caso prefira ver a aula em v\u00eddeo

      Aula Slides C\u00f3digo

      Ol\u00e1 a todos! Se voc\u00ea est\u00e1 chegando agora, recomendamos verificar as aulas anteriores de nosso curso \"FastAPI do Zero: Criando um Projeto com Bancos de Dados, Testes e Deploy\". Hoje, vamos mergulhar no SQLAlchemy e no Alembic, e come\u00e7aremos a configurar nosso banco de dados.

      Antes de mergulharmos na instala\u00e7\u00e3o e configura\u00e7\u00e3o, vamos esclarecer alguns conceitos.

      "},{"location":"03/#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:

      • Abstra\u00e7\u00e3o de banco de dados: ORMs permitem que voc\u00ea mude de um tipo de banco de dados para outro com poucas altera\u00e7\u00f5es no c\u00f3digo.

      • Seguran\u00e7a: ORMs geralmente lidam com escapar de consultas e prevenir inje\u00e7\u00f5es SQL, um tipo comum de vulnerabilidade de seguran\u00e7a.

      • Efici\u00eancia no desenvolvimento: ORMs podem gerar automaticamente esquemas, realizar migra\u00e7\u00f5es e outras tarefas que seriam demoradas para fazer manualmente.

      "},{"location":"03/#configuracoes-de-ambiente-e-as-12-fatores","title":"Configura\u00e7\u00f5es de ambiente e as 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, vamos come\u00e7ar instalando as bibliotecas que vamos usar. O primeiro passo \u00e9 instalar o SQLAlchemy, um ORM que nos permite trabalhar com bancos de dados SQL de maneira Pythonic. 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":"03/#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).

      • Core: O Core do SQLAlchemy disponibiliza uma interface SQL abstrata, que possibilita a manipula\u00e7\u00e3o de bancos de dados relacionais de maneira segura, alinhada com as conven\u00e7\u00f5es do Python. Atrav\u00e9s do Core, \u00e9 poss\u00edvel construir, analisar e executar instru\u00e7\u00f5es SQL, al\u00e9m de conectar-se a diversos tipos de bancos de dados utilizando a mesma API.

      • ORM: ORM, ou Mapeamento Objeto-Relacional, \u00e9 uma t\u00e9cnica que facilita a comunica\u00e7\u00e3o entre o c\u00f3digo orientado a objetos e bancos de dados relacionais. Com o ORM do SQLAlchemy, os desenvolvedores podem interagir com o banco de dados utilizando classes e objetos Python, eliminando a necessidade de escrever instru\u00e7\u00f5es SQL diretamente.

      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":"03/#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":"03/#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":"03/#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 herdam de uma classe base comum. A classe base \u00e9 criada a partir de DeclarativeBase.

      Cada classe que herda da classe base \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 que foram declaradas. Este objeto \u00e9 utilizado para gerenciar opera\u00e7\u00f5es como cria\u00e7\u00e3o, modifica\u00e7\u00e3o e exclus\u00e3o de tabelas.

      Vamos agora definir nosso modelo User. No diret\u00f3rio fast_zero, crie um novo arquivo chamado models.py.

      $ Execu\u00e7\u00e3o no terminal!
      touch fast_zero/models.py\n

      Inclua o seguinte c\u00f3digo no arquivo models.py:

      fast_zero/models.py
      from sqlalchemy.orm import DeclarativeBase\nfrom sqlalchemy.orm import Mapped\nfrom sqlalchemy.orm import mapped_column\nclass Base(DeclarativeBase):\npass\nclass User(Base):\n__tablename__ = 'users'\nid: Mapped[int] = mapped_column(primary_key=True)\nusername: Mapped[str]\npassword: Mapped[str]\nemail: Mapped[str]\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 com o banco de dados.

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

      Ao desenvolver software, \u00e9 uma boa pr\u00e1tica criar testes para cada nova funcionalidade implementada. Ap\u00f3s a cria\u00e7\u00e3o do nosso modelo de usu\u00e1rio, precisamos garantir seu funcionamento conforme esperado. Para isso, vamos criar dois arquivos de teste: conftest.py e test_db.py.

      O arquivo conftest.py \u00e9 utilizado pelo pytest para definir preparativos necess\u00e1rios antes da execu\u00e7\u00e3o dos testes ou configura\u00e7\u00f5es comuns a diversos testes. Ele centraliza as chamadas para as fixtures.

      Se fixtures s\u00e3o uma novidade para voc\u00ea

      Existe uma live de Python onde discutimos especificamente sobre fixtures

      Link direto

      Por sua vez, o arquivo test_db.py ser\u00e1 onde escreveremos os testes para nosso modelo de usu\u00e1rio. Isso ajudar\u00e1 a garantir que nossas tabelas estejam sendo criadas corretamente e que possam armazenar e recuperar informa\u00e7\u00f5es conforme esperado.

      Para criar esses arquivos, execute o seguinte comando:

      $ Execu\u00e7\u00e3o no terminal!
      touch tests/test_db.py\ntouch tests/conftest.py\n
      "},{"location":"03/#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]

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

      "},{"location":"03/#criando-uma-fixture-para-o-banco-de-dados","title":"Criando uma Fixture para o Banco de Dados","text":"

      No arquivo conftest.py, vamos introduzir uma fixture chamada \"session\" aos nossos testes pytest. Uma fixture \u00e9 uma fun\u00e7\u00e3o que permite definir e reutilizar configura\u00e7\u00f5es de prepara\u00e7\u00e3o para testes ou outras fun\u00e7\u00f5es. Elas s\u00e3o \u00fateis quando se tem partes do c\u00f3digo que precisam ser reutilizadas em v\u00e1rios testes.

      As fixtures s\u00e3o declaradas com a anota\u00e7\u00e3o @pytest.fixture. Uma vez declarada, a fixture pode ser injetada em fun\u00e7\u00f5es de teste, m\u00e9todos de classe de teste, outras fixtures ou at\u00e9 mesmo m\u00f3dulos, passando-as como par\u00e2metros.

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

      tests/conftest.py
      import pytest\nfrom sqlalchemy import create_engine, select\nfrom sqlalchemy.orm import sessionmaker\nfrom sqlalchemy.pool import StaticPool\nfrom fast_zero.models import Base\n@pytest.fixture\ndef session():\nengine = create_engine(\n'sqlite:///:memory:',\nconnect_args={'check_same_thread': False},\npoolclass=StaticPool,\n)\nSession = sessionmaker(autocommit=False, autoflush=False, bind=engine)\nBase.metadata.create_all(engine)\nyield Session()\nBase.metadata.drop_all(engine)\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:', connect_args={'check_same_thread': False}, poolclass=StaticPool): 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. Usamos connect_args={'check_same_thread': False} devido a algumas limita\u00e7\u00f5es do SQLite quando usado em threads diferentes. E poolclass=StaticPool para que a mesma conex\u00e3o seja reutilizada durante todo o teste.

      2. Session = sessionmaker(autocommit=False, autoflush=False, bind=engine): cria uma f\u00e1brica de sess\u00f5es para criar sess\u00f5es de banco de dados para nossos testes. Definimos autocommit=False para controlar quando as transa\u00e7\u00f5es s\u00e3o commitadas e autoflush=False para que SQLAlchemy n\u00e3o tente automaticamente sincronizar o estado da sess\u00e3o com o banco de dados a cada opera\u00e7\u00e3o.

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

      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. Base.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":"03/#criando-um-teste-para-a-nossa-tabela","title":"Criando um Teste para a Nossa Tabela","text":"

      Agora, no arquivo test_db.py, vamos escrever 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\nfrom fast_zero.models import User\ndef test_create_user(session):\nnew_user = User(username='alice', password='secret', email='teste@test')\nsession.add(new_user)\nsession.commit()\nuser = session.scalar(select(User).where(User.username == 'alice'))\nassert user.username == 'alice'\n
      "},{"location":"03/#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, vamos executar 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# ...\ntests/test_app.py::test_root_deve_retornar_200_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":"03/#configuracao-do-ambiente-do-banco-de-dados","title":"Configura\u00e7\u00e3o do ambiente do banco de dados","text":"

      Por fim, vamos configurar nosso banco de dados. Primeiro, vamos criar 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.

      $ Execu\u00e7\u00e3o no terminal!
      touch fast_zero/settings.py\n

      No arquivo settings.py, a classe Settings \u00e9 definida como:

      fast_zero/settings.py
      from pydantic_settings import BaseSettings, SettingsConfigDict\nclass Settings(BaseSettings):\nmodel_config = SettingsConfigDict(\nenv_file='.env', env_file_encoding='utf-8'\n)\nDATABASE_URL: str\n

      Agora, vamos definir 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":"03/#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, vamos come\u00e7ar 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  \u2514\u2500\u2500 schemas.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":"03/#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, vamos fazer algumas altera\u00e7\u00f5es no arquivo migrations/env.py.

      Neste arquivo, precisamos:

      1. Importar as Settings do nosso arquivo settings.py e a Base 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 Base.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 alembic import context\nfrom fast_zero.settings import Settings\nfrom fast_zero.models import Base\nconfig = context.config\nconfig.set_main_option('sqlalchemy.url', Settings().DATABASE_URL)\nif config.config_file_name is not None:\nfileConfig(config.config_file_name)\ntarget_metadata = Base.metadata\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":"03/#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\nRevision ID: e018397cecf4\nRevises:\nCreate Date: 2023-07-13 03:43:03.730534\n\"\"\"\nfrom alembic import op\nimport sqlalchemy as sa\n# revision identifiers, used by Alembic.\nrevision = 'e018397cecf4'\ndown_revision = None\nbranch_labels = None\ndepends_on = None\ndef upgrade() -> None:\n# ### commands auto generated by Alembic - please adjust! ###\nop.create_table('users',\nsa.Column('id', sa.Integer(), nullable=False),\nsa.Column('username', sa.String(), nullable=False),\nsa.Column('password', sa.String(), nullable=False),\nsa.Column('email', sa.String(), nullable=False),\nsa.PrimaryKeyConstraint('id')\n)\n# ### end Alembic commands ###\ndef downgrade() -> None:\n# ### commands auto generated by Alembic - please adjust! ###\nop.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.

      Apesar desta migra\u00e7\u00e3o ter sido criada, ela ainda n\u00e3o foi aplicada ao nosso banco de dados. No entanto, o Alembic j\u00e1 criou um arquivo database.db, conforme especificamos no arquivo .env e que foi lido pela classe Settings do Pydantic. Al\u00e9m disso, ele criou uma tabela alembic_version no banco de dados para controlar as vers\u00f5es das migra\u00e7\u00f5es que foram aplicadas.

      $ Execu\u00e7\u00e3o no terminal!
      sqlite3 database.db\nSQLite version 3.42.0 2023-05-16 12:36:15\nEnter \".help\" for usage hints.\nsqlite> .schema\nCREATE TABLE alembic_version (\nversion_num VARCHAR(32) NOT NULL,\nCONSTRAINT alembic_version_pkc PRIMARY KEY (version_num)\n);\nsqlite> .exit\n

      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\nINFO  [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

      Agora, se examinarmos nosso banco de dados novamente, veremos que a tabela users foi criada:

      sqlite3 database.db\nSQLite version 3.42.0 2023-05-16 12:36:15\nEnter \".help\" for usage hints.\nsqlite> .schema\nCREATE TABLE alembic_version (\nversion_num VARCHAR(32) NOT NULL,\nCONSTRAINT alembic_version_pkc PRIMARY KEY (version_num)\n);\nCREATE TABLE users (\nid INTEGER NOT NULL,\nusername VARCHAR NOT NULL,\npassword VARCHAR NOT NULL,\nemail VARCHAR NOT NULL,\nPRIMARY KEY (id)\n);\nsqlite> .exit\n

      Finalmente, lembre-se de que todas essas mudan\u00e7as que fizemos s\u00f3 existem localmente no seu ambiente de trabalho at\u00e9 agora. Para que sejam compartilhadas com outras pessas, precisamos fazer commit dessas mudan\u00e7as no nosso sistema de controle de vers\u00e3o.

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

      Primeiro, vamos verificar 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 que foram 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, vamos adicionar 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. Vamos fornecer 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, vamos enviar 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 git.

      "},{"location":"03/#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, em conformidade com 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.

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

      Objetivos dessa aula:

      • Integrando SQLAlchemy \u00e0 nossa aplica\u00e7\u00e3o FastAPI
      • Utilizando a fun\u00e7\u00e3o Depends para gerenciar depend\u00eancias
      • Modificando endpoints para interagir com o banco de dados
      • Testando os novos endpoints com Pytest e fixtures
      Caso prefira ver a aula em v\u00eddeo

      Aula Slides C\u00f3digo

      Ap\u00f3s a cria\u00e7\u00e3o de nossos modelos e migra\u00e7\u00f5es na aula passada, chegou o momento de dar um passo significativo: integrar o banco de dados \u00e0 nossa aplica\u00e7\u00e3o FastAPI. Vamos deixar de lado o banco de dados fict\u00edcio que criamos anteriormente e mergulhar na implementa\u00e7\u00e3o de um banco de dados real e funcional.

      "},{"location":"04/#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

      Temos diversas lives de Python focadas nesse assunto.

      Esta sobre o ORM em espec\u00edfico:

      Link direto

      Essa sobre as novidades da vers\u00e3o 1.4 e do estilo de programa\u00e7\u00e3o da vers\u00e3o 2.0

      Link direto

      E finalmente uma focada no processo de migra\u00e7\u00f5es com o SQLalchemy + Alembic (que veremos nessa aula)

      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 que a loja saiba 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, vamos configurar 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\nfrom fast_zero.settings import Settings\nengine = create_engine(Settings().DATABASE_URL)\ndef get_session():\nwith Session(engine) as session:\nyield session\n
      "},{"location":"04/#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) que \u00e9 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":"04/#modificando-o-endpoint-post-users","title":"Modificando o Endpoint POST /users","text":"

      Agora que temos a nossa sess\u00e3o de banco de dados sendo gerenciada por meio do FastAPI e da inje\u00e7\u00e3o de depend\u00eancias, vamos atualizar nossos endpoints para que possam 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 vamos fazer 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 fastapi import Depends, FastAPI, HTTPException\nfrom sqlalchemy import select\nfrom sqlalchemy.orm import Session\nfrom fast_zero.models import User\nfrom fast_zero.database import get_session\nfrom fast_zero.schemas import UserSchema, UserPublic, UserDB, UserList, Message\n# ...\n@app.post('/users/', response_model=UserPublic, status_code=201)\ndef create_user(user: UserSchema, session: Session = Depends(get_session)):\ndb_user = session.scalar(\nselect(User).where(User.username == user.username)\n)\nif db_user:\nraise HTTPException(\nstatus_code=400, detail='Username already registered'\n)\ndb_user = User(\nusername=user.username, password=user.password, email=user.email\n)\nsession.add(db_user)\nsession.commit()\nsession.refresh(db_user)\nreturn db_user\n

      Nesse c\u00f3digo, a fun\u00e7\u00e3o create_user recebe um objeto do tipo UserSchema e uma sess\u00e3o SQLAlchemy, que \u00e9 injetada automaticamente pelo FastAPI usando o Depends. O c\u00f3digo verifica se j\u00e1 existe um usu\u00e1rio com o mesmo nome no banco de dados e, caso n\u00e3o exista, cria um novo usu\u00e1rio, adiciona-o \u00e0 sess\u00e3o e confirma a transa\u00e7\u00e3o.

      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:7:55: F401 [*] `fast_zero.schemas.UserDB` imported but unused\nFound 1 error.\n

      Isso ocorre porque a rota POST era a \u00fanica que estava utilizando UserDB, e agora que modificamos esta rota, podemos remover UserDB dos nossos imports.

      "},{"location":"04/#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 que possamos injetar a sess\u00e3o de banco de dados de teste.

      Vamos criar 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
      from fastapi.testclient import TestClient\nfrom fast_zero.app import app\nfrom fast_zero.database import get_session\n# ...\n@pytest.fixture\ndef client(session):\ndef get_session_override():\nreturn session\nwith TestClient(app) as client:\napp.dependency_overrides[get_session] = get_session_override\nyield client\napp.dependency_overrides.clear()\n

      Com isso, quando o FastAPI tentar injetar a sess\u00e3o em nossos endpoints, ele vai injetar 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):\nresponse = client.post(\n'/users',\njson={\n'username': 'alice',\n'email': 'alice@example.com',\n'password': 'secret',\n},\n)\nassert response.status_code == 201\nassert response.json() == {\n'username': 'alice',\n'email': 'alice@example.com',\n'id': 1,\n}\n

      Agora que temos a nossa fixture configurada, vamos atualizar 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# ...\ntests/test_app.py::test_root_deve_retornar_200_e_ola_mundo PASSED\ntests/test_app.py::test_create_user PASSED\ntests/test_app.py::test_read_users FAILED\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. 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":"04/#modificando-o-endpoint-get-users","title":"Modificando o Endpoint GET /users","text":"

      {dizer que agora vamos atualizar o nosso endpoint de GET para funcionar no modelo real do banco de dados}

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

      {explicar o que s\u00e3o as coisas que adicionamos como offset e limit e porque elas s\u00e3o importantes agora nesses casos}

      "},{"location":"04/#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\u00e3o usu\u00e1rios no banco. Para verificar se o nosso endpoint est\u00e1 funcionando corretamente, vamos criar um novo teste que solicita uma lista de usu\u00e1rios de um banco vazio:

      tests/test_app.py
      def test_read_users(client):\nresponse = client.get('/users')\nassert response.status_code == 200\nassert 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# ...\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, vamos criar uma nova fixture que cria um usu\u00e1rio em nosso banco de dados de teste.

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

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

      tests/conftest.py
      from fast_zero.models import Base, User\n# ...\n@pytest.fixture\ndef user(session):\nuser = User(username='Teste', email='teste@test.com', password='testtest')\nsession.add(user)\nsession.commit()\nsession.refresh(user)\nreturn 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# ...\ndef test_read_users_with_users(client, user):\nuser_schema = UserPublic.model_validate(user).model_dump()\nresponse = client.get('/users/')\nassert 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# ...\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. Vamos resolver isso agora.

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

      {dizer que precisamos alterar o UserPublic que usamos no teste para que ele reconhe\u00e7a o ORM}

      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, vamos adicionar a linha model_config = ConfigDict(from_attributes=True) ao nosso esquema:

      fast_zero/schemas.py
      from pydantic import BaseModel, EmailStr, ConfigDict\n# ...\nclass UserPublic(BaseModel):\nid: int\nusername: str\nemail: EmailStr\nmodel_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# ...\ntests/test_app.py::test_root_deve_retornar_200_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":"04/#modificando-o-endpoint-put-users","title":"Modificando o Endpoint PUT /users","text":"

      Agora, vamos modificar o endpoint PUT de forma semelhante ao que fizemos com os endpoints POST e GET:

      fast_zero/app.py
      @app.put('/users/{user_id}', response_model=UserPublic)\ndef update_user(\nuser_id: int, user: UserSchema, session: Session = Depends(get_session)\n):\ndb_user = session.scalar(select(User).where(User.id == user_id))\nif db_user is None:\nraise HTTPException(status_code=404, detail='User not found')\ndb_user.username = user.username\ndb_user.password = user.password\ndb_user.email = user.email\nsession.commit()\nsession.refresh(db_user)\nreturn 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.

      "},{"location":"04/#adicionando-o-teste-do-put","title":"Adicionando o teste do PUT","text":"

      Tamb\u00e9m precisamos adicionar um teste para o nosso novo endpoint PUT:

      tests/test_app.py
      def test_update_user(client, user):\nresponse = client.put(\n'/users/1',\njson={\n'username': 'bob',\n'email': 'bob@example.com',\n'password': 'mynewpassword',\n},\n)\nassert response.status_code == 200\nassert response.json() == {\n'username': 'bob',\n'email': 'bob@example.com',\n'id': 1,\n}\n
      "},{"location":"04/#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)):\ndb_user = session.scalar(select(User).where(User.id == user_id))\nif db_user is None:\nraise HTTPException(status_code=404, detail='User not found')\nsession.delete(db_user)\nsession.commit()\nreturn {'detail': 'User deleted'}\n

      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":"04/#adicionando-testes-para-delete","title":"Adicionando testes para DELETE","text":"

      Assim como para o endpoint PUT, precisamos adicionar um teste para o nosso endpoint DELETE:

      tests/test_app.py
      def test_delete_user(client, user):\nresponse = client.delete('/users/1')\nassert response.status_code == 200\nassert response.json() == {'detail': 'User deleted'}\n
      "},{"location":"04/#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\u00edcio 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":"04/#commit","title":"Commit","text":"

      Agora que terminamos a atualiza\u00e7\u00e3o dos nossos endpoints, vamos fazer 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":"04/#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.

      Tamb\u00e9m 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.

      "},{"location":"05/","title":"Autentica\u00e7\u00e3o e Autoriza\u00e7\u00e3o com JWT","text":""},{"location":"05/#autenticacao-e-autorizacao-com-jwt","title":"Autentica\u00e7\u00e3o e Autoriza\u00e7\u00e3o com JWT","text":"

      Objetivos da Aula:

      • Um entendimento b\u00e1sico sobre JWT
      • Implementar autentica\u00e7\u00e3o de usu\u00e1rios com JWT.
      • Adicionar l\u00f3gica de autoriza\u00e7\u00e3o aos endpoints de atualiza\u00e7\u00e3o e dele\u00e7\u00e3o.
      • Utilizar a biblioteca Bcrypt para encriptar as senhas dos usu\u00e1rios.
      Caso prefira ver a aula em v\u00eddeo

      Aula Slides C\u00f3digo

      "},{"location":"05/#introducao","title":"Introdu\u00e7\u00e3o","text":"

      Nesta aula, vamos abordar 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. Vamos corrigir isso utilizando a biblioteca Bcrypt para encriptar as senhas.

      "},{"location":"05/#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 tipicamente consiste 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 (ou declara\u00e7\u00f5es) 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":"05/#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":"05/#gerando-tokens-jwt","title":"Gerando tokens JWT","text":"

      Para gerar tokens JWT, precisamos de duas bibliotecas extras: python-jose e passlib. 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 python-jose[cryptography] passlib[bcrypt]\n

      Agora, vamos criar uma fun\u00e7\u00e3o para gerar nossos tokens JWT. Criaremos um novo arquivo para gerenciar a seguran\u00e7a: security.py. Nesse arquivo vamos iniciar a gera\u00e7\u00e3o dos tokens:

      fast_zero/security.py
      from datetime import datetime, timedelta\nfrom jose import jwt\nfrom passlib.context import CryptContext\nSECRET_KEY = 'your-secret-key'  # Isso \u00e9 provis\u00f3rio, vamos ajustar!\nALGORITHM = 'HS256'\nACCESS_TOKEN_EXPIRE_MINUTES = 30\npwd_context = CryptContext(schemes=['bcrypt'], deprecated='auto')\ndef create_access_token(data: dict):\nto_encode = data.copy()\nexpire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)\nto_encode.update({'exp': expire})\nencoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)\nreturn 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 playload do JWT. Em seguida usa a biblioteca jose 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":"05/#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 em que consigamos ver os tokens gerados pelo jose e interagirmos com ele.

      Com isso vamos criar um arquivo chamado tests/test_security.py para efetuar esse teste:

      tests/test_security.py
      from jose import jwt\nfrom fast_zero.security import create_access_token, SECRET_KEY\ndef test_jwt():\ndata = {'test': 'test'}\ntoken = create_access_token(data)\ndecoded = jwt.decode(token, SECRET_KEY, algorithms=['HS256'])\nassert decoded['test'] == data['test']\nassert decoded['exp']  # Testa se o valor de exp foi adicionado ao token\n

      Na pr\u00f3xima se\u00e7\u00e3o, vamos ver como podemos usar a biblioteca passlib para tratar as senhas dos usu\u00e1rios.

      "},{"location":"05/#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.

      Vamos implementar essa funcionalidade usando a biblioteca passlib. Vamos criar 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):\nreturn pwd_context.hash(password)\ndef verify_password(plain_password: str, hashed_password: str):\nreturn 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 passlib.

      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, vamos modificar nossos endpoints para fazer uso dessas fun\u00e7\u00f5es.

      "},{"location":"05/#modificando-os-endpoints-para-usar-a-autenticacao","title":"Modificando os Endpoints para Usar a Autentica\u00e7\u00e3o","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 autentica\u00e7\u00e3o.

      Primeiro, vamos modificar a fun\u00e7\u00e3o create_user para criar um hash da senha antes de armazen\u00e1-la no banco de dados.

      fast_zero/app.py
      from fast_zero.security import get_password_hash\n# ...\n@app.post('/users/', response_model=UserPublic, status_code=201)\ndef create_user(user: UserSchema, session: Session = Depends(get_session)):\ndb_user = session.scalar(select(User).where(User.email == user.email))\nif db_user:\nraise HTTPException(status_code=400, detail='Email already registered')\nhashed_password = get_password_hash(user.password)\ndb_user = User(\nemail=user.email,\nusername=user.username,\npassword=hashed_password,\n)\nsession.add(db_user)\nsession.commit()\nsession.refresh(db_user)\nreturn db_user\n

      Nesse novo c\u00f3digo, estamos usando a fun\u00e7\u00e3o get_password_hash para criar um hash da senha antes de armazen\u00e1-la no banco de dados. Al\u00e9m disso, estamos armazenando o hash da senha, em vez da senha em texto plano, em nosso objeto User.

      "},{"location":"05/#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# ...\ntests/test_app.py::test_root_deve_retornar_200_e_ola_mundo PASSED\ntests/test_app.py::test_create_user PASSED\n
      "},{"location":"05/#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):\naccess_token: str\ntoken_type: str\n
      "},{"location":"05/#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, o que facilita 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":"05/#criando-um-endpoint-de-geracao-do-token_1","title":"Criando um endpoint de gera\u00e7\u00e3o do token","text":"

      Agora vamos criar 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 (\ncreate_access_token,\nget_password_hash,\nverify_password,\n)\n# ...\n@app.post('/token', response_model=Token)\ndef login_for_access_token(\nform_data: OAuth2PasswordRequestForm = Depends(),\nsession: Session = Depends(get_session),\n):\nuser = session.scalar(select(User).where(User.email == form_data.username))\nif not user:\nraise HTTPException(\nstatus_code=400, detail='Incorrect email or password'\n)\nif not verify_password(form_data.password, user.password):\nraise HTTPException(\nstatus_code=400, detail='Incorrect email or password'\n)\naccess_token = create_access_token(data={'sub': user.email})\nreturn {'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":"05/#testando-token","title":"Testando /token","text":"

      Agora vamos escrever um teste para verificar se o nosso novo endpoint est\u00e1 funcionando corretamente.

      tests/test_app.py
      def test_get_token(client, user):\nresponse = client.post(\n'/token',\ndata={'username': user.email, 'password': user.password},\n)\ntoken = response.json()\nassert response.status_code == 200\nassert 'access_token' in token\nassert '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# ...\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/confitest.py
      from fast_zero.security import get_password_hash\n# ...\n@pytest.fixture\ndef user(session):\nuser = User(\nusername='Teste',\nemail='teste@test.com',\npassword=get_password_hash('testtest'),\n)\nsession.add(user)\nsession.commit()\nsession.refresh(user)\nreturn user\n

      Vamos rodar 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# ...\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/confitest.py
      @pytest.fixture\ndef user(session):\npassword = 'testtest'\nuser = User(\nusername='Teste',\nemail='teste@test.com',\npassword=get_password_hash(password),\n)\nsession.add(user)\nsession.commit()\nsession.refresh(user)\nuser.clean_password = 'testtest'\nreturn 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):\nresponse = client.post(\n'/token',\ndata={'username': user.email, 'password': user.clean_password},\n)\ntoken = response.json()\nassert response.status_code == 200\nassert 'access_token' in token\nassert 'token_type' in token\n

      E agora todos os testes devem passar normalmente:

      $ Execu\u00e7\u00e3o no terminal!
      task test\n# ...\ntests/test_app.py::test_root_deve_retornar_200_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, iremos implementar a autoriza\u00e7\u00e3o nos endpoints.

      "},{"location":"05/#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, vamos adicionar 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):\nusername: 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 finalmente obter 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. Vamos cria-l\u00e1 no arquivo security.py:

      fast_zero/security.py
      from datetime import datetime, timedelta\nfrom fastapi import Depends, HTTPException, status\nfrom fastapi.security import OAuth2PasswordBearer\nfrom jose import JWTError, jwt\nfrom passlib.context import CryptContext\nfrom sqlalchemy import select\nfrom sqlalchemy.orm import Session\nfrom fast_zero.database import get_session\nfrom fast_zero.models import User\nfrom fast_zero.schemas import TokenData\n# ...\noauth2_scheme = OAuth2PasswordBearer(tokenUrl=\"token\")\nasync def get_current_user(\nsession: Session = Depends(get_session),\ntoken: str = Depends(oauth2_scheme),\n):\ncredentials_exception = HTTPException(\nstatus_code=status.HTTP_401_UNAUTHORIZED,\ndetail='Could not validate credentials',\nheaders={'WWW-Authenticate': 'Bearer'},\n)\ntry:\npayload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])\nusername: str = payload.get('sub')\nif not username:\nraise credentials_exception\ntoken_data = TokenData(username=username)\nexcept JWTError:\nraise credentials_exception\nuser = session.scalar(\nselect(User).where(User.email == token_data.username)\n)\nif user is None:\nraise credentials_exception\nreturn user\n

      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":"05/#aplicacao-da-protecao-ao-endpoint","title":"Aplica\u00e7\u00e3o da prote\u00e7\u00e3o ao endpoint","text":"

      Primeiro, vamos aplicar 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 (\ncreate_access_token,\nget_current_user,\nget_password_hash,\nverify_password,\n)\n# ...\n@app.put('/users/{user_id}', response_model=UserPublic)\ndef update_user(\nuser_id: int,\nuser: UserSchema,\nsession: Session = Depends(get_session),\ncurrent_user: User = Depends(get_current_user),\n):\nif current_user.id != user_id:\nraise HTTPException(status_code=400, detail='Not enough permissions')\ncurrent_user.username = user.username\ncurrent_user.password = user.password\ncurrent_user.email = user.email\nsession.commit()\nsession.refresh(current_user)\nreturn 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, vamos aplicar 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(\nuser_id: int,\nsession: Session = Depends(get_session),\ncurrent_user: User = Depends(get_current_user),\n):\nif current_user.id != user_id:\nraise HTTPException(status_code=400, detail='Not enough permissions')\nsession.delete(current_user)\nsession.commit()\nreturn {'detail': '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":"05/#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):\nresponse = client.post(\n'/token',\ndata={'username': user.email, 'password': user.clean_password},\n)\nreturn 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):\nresponse = client.put(\nf'/users/{user.id}',\nheaders={'Authorization': f'Bearer {token}'},\njson={\n'username': 'bob',\n'email': 'bob@example.com',\n'password': 'mynewpassword',\n},\n)\nassert response.status_code == 200\nassert response.json() == {\n'username': 'bob',\n'email': 'bob@example.com',\n'id': 1,\n}\ndef test_delete_user(client, user, token):\nresponse = client.delete(\nf'/users/{user.id}',\nheaders={'Authorization': f'Bearer {token}'},\n)\nassert response.status_code == 200\nassert response.json() == {'detail': 'User deleted'}\n

      Finalmente, podemos rodar todos os testes para garantir que tudo esteja funcionando corretamente.

      $ Execu\u00e7\u00e3o no terminal!
      task test\n# ...\ntests/test_app.py::test_root_deve_retornar_200_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":"05/#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":"05/#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!

      "},{"location":"06/","title":"Refatorando a Estrutura do Projeto","text":""},{"location":"06/#refatorando-a-estrutura-do-projeto","title":"Refatorando a Estrutura do Projeto","text":"

      Objetivos da Aula:

      • Reestruturar o projeto para facilitar sua manuten\u00e7\u00e3o
      • Mover coisas de autentica\u00e7\u00e3o para um arquivo chamado fast_zero/auth.py
      • Deixando em fast_zero/secutiry.py somente as valida\u00e7\u00f5es de senha
      • Remover constantes usados em c\u00f3digo (SECRET_KEY, ALGORITHM e ACCESS_TOKEN_EXPIRE_MINUTES) usando a classe Settings do arquivo fast_zero/settings/py que j\u00e1 temos e movendo para vari\u00e1veis de ambiente no arquivo .env
      • Criar routers espec\u00edficos para rotas que tratam das funcionalidades de usu\u00e1rios e para as de autentica\u00e7\u00e3o
      Caso prefira ver a aula em v\u00eddeo

      Aula Slides C\u00f3digo

      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: vamos refatorar 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":"06/#criando-routers","title":"Criando Routers","text":"

      O FastAPI nos fornece um recurso \u00fatil chamado routers, que nos permite organizar e agrupar diferentes rotas em nossa aplica\u00e7\u00e3o. Em outras palavras, um router \u00e9 um \"subaplicativo\" FastAPI que pode ser montado em uma aplica\u00e7\u00e3o principal.

      Ao usar routers, podemos manter nosso c\u00f3digo mais organizado e leg\u00edvel, especialmente \u00e0 medida que nossa aplica\u00e7\u00e3o cresce e adicionamos mais rotas.

      Vamos come\u00e7ar criando uma nova estrutura de diret\u00f3rios dentro de fast_zero chamada routes, onde teremos um arquivo users.py que ser\u00e1 respons\u00e1vel por todas as rotas relacionadas a usu\u00e1rios. Isso nos ajudar\u00e1 a manter o c\u00f3digo relacionado aos usu\u00e1rios centralizado em um \u00fanico lugar.

      "},{"location":"06/#criando-um-router-para-users","title":"Criando um router para Users","text":"fast_zero/routes/users.py
      from fastapi import APIRouter, Depends, HTTPException\nfrom sqlalchemy import select\nfrom sqlalchemy.orm import Session\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 (\nget_current_user,\nget_password_hash,\n)\nrouter = APIRouter(prefix='/users', tags=['users'])\n

      Neste bloco de c\u00f3digo, criamos uma inst\u00e2ncia do APIRouter com o prefixo '/users'. Isso nos permitir\u00e1 definir as rotas relacionadas aos usu\u00e1rios neste router, em vez de no aplicativo principal. Agora, ao inv\u00e9s de usarmos @app, usaremos @router para definir nossas rotas. O par\u00e2metro tags ajuda na organiza\u00e7\u00e3o e documenta\u00e7\u00e3o das rotas, associando-as a uma tag espec\u00edfica (neste caso, 'users'), que ser\u00e1 exibida na interface de documenta\u00e7\u00e3o autom\u00e1tica gerada pelo FastAPI.

      fast_zero/routes/users.py
      @router.post('/', response_model=UserPublic, status_code=201)\n@router.get('/', response_model=UserList)\n@router.put('/{user_id}', response_model=UserPublic)\n@router.delete('/{user_id}', response_model=Message)\n

      Gra\u00e7as ao uso do prefixo no router, podemos simplificar os paths de nossos endpoints. Por exemplo, em vez de definir o path como '/users/{user_id}', podemos simplesmente usar '/{user_id}'.

      Exemplo do arquivo fast_zero/routes/users.py completo fast_zero/routes/users.py
      from fastapi import APIRouter, Depends, HTTPException\nfrom sqlalchemy import select\nfrom sqlalchemy.orm import Session\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 (\nget_current_user,\nget_password_hash,\n)\nrouter = APIRouter(prefix='/users')\n@router.post('/', response_model=UserPublic, status_code=201)\ndef create_user(user: UserSchema, session: Session = Depends(get_session)):\ndb_user = session.scalar(select(User).where(User.email == user.email))\nif db_user:\nraise HTTPException(status_code=400, detail='Email already registered')\nhashed_password = get_password_hash(user.password)\ndb_user = User(\nemail=user.email,\nusername=user.username,\npassword=hashed_password,\n)\nsession.add(db_user)\nsession.commit()\nsession.refresh(db_user)\nreturn db_user\n@router.get('/', response_model=UserList)\ndef read_users(\nskip: int = 0, limit: int = 100, session: Session = Depends(get_session)\n):\nusers = session.scalars(select(User).offset(skip).limit(limit)).all()\nreturn {'users': users}\n@router.put('/{user_id}', response_model=UserPublic)\ndef update_user(\nuser_id: int,\nuser: UserSchema,\nsession: Session = Depends(get_session),\ncurrent_user: User = Depends(get_current_user),\n):\nif current_user.id != user_id:\nraise HTTPException(status_code=400, detail='Not enough permissions')\ncurrent_user.username = user.username\ncurrent_user.password = user.password\ncurrent_user.email = user.email\nsession.commit()\nsession.refresh(current_user)\nreturn current_user\n@router.delete('/{user_id}', response_model=Message)\ndef delete_user(\nuser_id: int,\nsession: Session = Depends(get_session),\ncurrent_user: User = Depends(get_current_user),\n):\nif current_user.id != user_id:\nraise HTTPException(status_code=400, detail='Not enough permissions')\nsession.delete(current_user)\nsession.commit()\nreturn {'detail': 'User deleted'}\n

      Por termos criados as tags, isso reflete na organiza\u00e7\u00e3o do swagger

      "},{"location":"06/#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. Vamos dar 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 fastapi import APIRouter, Depends, HTTPException\nfrom fastapi.security import OAuth2PasswordRequestForm\nfrom sqlalchemy import select\nfrom sqlalchemy.orm import Session\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\nrouter = APIRouter(tags=['token'])\n@router.post('/token', response_model=Token)\ndef login_for_access_token(\nform_data: OAuth2PasswordRequestForm = Depends(),\nsession: Session = Depends(get_session),\n):\nuser = session.scalar(select(User).where(User.email == form_data.username))\nif not user:\nraise HTTPException(\nstatus_code=400, detail='Incorrect email or password'\n)\nif not verify_password(form_data.password, user.password):\nraise HTTPException(\nstatus_code=400, detail='Incorrect email or password'\n)\naccess_token = create_access_token(data={'sub': user.email})\nreturn {'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.

      Essa estrutura oferece algumas vantagens. Em primeiro lugar, nosso c\u00f3digo se torna mais organizado e mais f\u00e1cil de manter. Em segundo lugar, a separa\u00e7\u00e3o de responsabilidades (o chamado princ\u00edpio SoC) torna o c\u00f3digo mais f\u00e1cil de entender e testar. Por fim, nos permite escalar nosso aplicativo de maneira mais eficaz, pois podemos facilmente adicionar mais routers conforme o projeto cresce.

      "},{"location":"06/#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 fastapi import FastAPI\nfrom fast_zero.routes import auth, users\napp = FastAPI()\napp.include_router(users.router)\napp.include_router(auth.router)\n@app.get('/')\ndef read_root():\nreturn {'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":"06/#executando-os-testes","title":"Executando os testes","text":"

      Depois de refatorar nosso c\u00f3digo, \u00e9 crucial verificar se tudo ainda est\u00e1 funcionando como esperado. Para isso, executamos nossos testes novamente.

      $ Execu\u00e7\u00e3o no terminal!
      task test\n# ...\ntests/test_app.py::test_root_deve_retornar_200_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":"06/#reestruturando-os-arquivos-de-testes","title":"Reestruturando os arquivos de testes","text":"

      Agora que o nosso projeto foi reestruturado, \u00e9 importante manter a organiza\u00e7\u00e3o nos arquivos de teste tamb\u00e9m. Agora, temos 3 arquivos de teste diferentes:

      • /tests/test_app.py: Para testes relacionados ao aplicativo em geral
      • /tests/test_auth.py: Para testes relacionados \u00e0 autentica\u00e7\u00e3o e token
      • /tests/test_users.py: Para testes relacionados \u00e0s rotas de usu\u00e1rios

      Vamos adaptar os testes para se encaixarem nessa nova estrutura.

      "},{"location":"06/#ajustando-os-testes-para-auth","title":"Ajustando os testes para Auth","text":"

      Vamos come\u00e7ar 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
      def test_get_token(client, user):\nresponse = client.post(\n'/token',\ndata={'username': user.email, 'password': user.clean_password},\n)\ntoken = response.json()\nassert response.status_code == 200\nassert 'access_token' in token\nassert 'token_type' in token\n
      "},{"location":"06/#ajustando-os-testes-para-user","title":"Ajustando os testes para User","text":"

      Em seguida, vamos mover os testes relacionados ao usu\u00e1rio para o arquivo /tests/test_users.py.

      /tests/test_users.py
      from fast_zero.schemas import UserPublic\ndef test_create_user(client):\nresponse = client.post(\n'/users/',\njson={\n'username': 'alice',\n'email': 'alice@example.com',\n'password': 'secret',\n},\n)\nassert response.status_code == 201\nassert response.json() == {\n'username': 'alice',\n'email': 'alice@example.com',\n'id': 1,\n}\ndef test_read_users(client):\nresponse = client.get('/users/')\nassert response.status_code == 200\nassert response.json() == {'users': []}\ndef test_read_users_with_users(client, user):\nuser_schema = UserPublic.model_validate(user).model_dump()\nresponse = client.get('/users/')\nassert response.json() == {'users': [user_schema]}\ndef test_update_user(client, user, token):\nresponse = client.put(\nf'/users/{user.id}',\nheaders={'Authorization': f'Bearer {token}'},\njson={\n'username': 'bob',\n'email': 'bob@example.com',\n'password': 'mynewpassword',\n},\n)\nassert response.status_code == 200\nassert response.json() == {\n'username': 'bob',\n'email': 'bob@example.com',\n'id': 1,\n}\ndef test_delete_user(client, user, token):\nresponse = client.delete(\nf'/users/{user.id}',\nheaders={'Authorization': f'Bearer {token}'},\n)\nassert response.status_code == 200\nassert response.json() == {'detail': 'User deleted'}\n

      Essa organiza\u00e7\u00e3o facilita a manuten\u00e7\u00e3o dos testes, tornando-os mais claros e alinhados com a estrutura do nosso projeto.

      "},{"location":"06/#executando-os-testes_1","title":"Executando os testes","text":"

      Ap\u00f3s essa reestrutura\u00e7\u00e3o, \u00e9 importante garantir que tudo ainda est\u00e1 funcionando corretamente. Vamos executar os testes novamente para confirmar isso.

      $ Execu\u00e7\u00e3o no terminal!
      task test\n# ...\ntests/test_app.py::test_root_deve_retornar_200_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":"06/#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/routes/users.py
      from typing import Annotated\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/routes/users.py
      @router.post('/', response_model=UserPublic, status_code=201)\ndef create_user(user: UserSchema, session: Session):\n# ...\n@router.get('/', response_model=UserList)\ndef read_users(session: Session, skip: int = 0, limit: int = 100):\n# ...\n@router.put('/{user_id}', response_model=UserPublic)\ndef update_user(\nuser_id: int,\nuser: UserSchema,\nsession: Session,\ncurrent_user: CurrentUser\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# ...\nOAuth2Form = Annotated[OAuth2PasswordRequestForm, Depends()]\nSession = Annotated[Session, Depends(get_session)]\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":"06/#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":"06/#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. Vamos alterar essa classe para incluir estas constantes.

      fast_zero/settings.py
      from pydantic_settings import BaseSettings, SettingsConfigDict\nclass Settings(BaseSettings):\nmodel_config = SettingsConfigDict(\nenv_file='.env', env_file_encoding='utf-8'\n)\nDATABASE_URL: str\nSECRET_KEY: str\nALGORITHM: str\nACCESS_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":"06/#removendo-as-constantes-do-codigo","title":"Removendo as constantes do c\u00f3digo","text":"

      Primeiramente, vamos carregar 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\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, vamos alterar para usar as constantes da classe Settings:

      fast_zero/security.py
      def create_access_token(data: dict):\nto_encode = data.copy()\nexpire = datetime.utcnow() + timedelta(\nminutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES\n)\nto_encode.update({'exp': expire})\nencoded_jwt = jwt.encode(\nto_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM\n)\nreturn 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.

      "},{"location":"06/#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, vamos rodar todos os testes que temos at\u00e9 agora.

      $ Execu\u00e7\u00e3o no terminal!
      task test\n# ...\ntests/test_app.py::test_root_deve_retornar_200_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":"06/#commit","title":"Commit","text":"

      Para finalizar, vamos criar 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":"06/#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 hoje, estamos em uma boa posi\u00e7\u00e3o para continuar a expandir nossa API no futuro.

      Na pr\u00f3xima aula, vamos explorar mais sobre autentica\u00e7\u00e3o e como gerenciar tokens de acesso e de atualiza\u00e7\u00e3o em nossa API FastAPI. Fique ligado!

      "},{"location":"07/","title":"[OK] Tornando o sistema de autentica\u00e7\u00e3o robusto","text":""},{"location":"07/#ok-tornando-o-sistema-de-autenticacao-robusto","title":"[OK] Tornando o sistema de autentica\u00e7\u00e3o robusto","text":"

      Objetivos da Aula:

      • Testar os casos de autentica\u00e7\u00e3o de forma correta
      • Implementar o refresh do token
      • Introduzir testes que param o tempo com freezegun
      • Introduzir gera\u00e7\u00e3o de modelos autom\u00e1tica com factory-boy
      Caso prefira ver a aula em v\u00eddeo

      Aula Slides C\u00f3digo

      Na aula de hoje, vamos aprofundar nosso sistema de autentica\u00e7\u00e3o. J\u00e1 vimos em aulas anteriores como criar um sistema de autentica\u00e7\u00e3o b\u00e1sico, mas h\u00e1 muitas \u00e1reas em que podemos torn\u00e1-lo mais robusto. Por exemplo, como podemos lidar com situa\u00e7\u00f5es em que as coisas d\u00e3o errado? Como podemos garantir que nosso sistema seja seguro mesmo em cen\u00e1rios adversos? Essas s\u00e3o algumas das quest\u00f5es que vamos explorar hoje.

      Vamos come\u00e7ar examinando mais de perto os testes para autentica\u00e7\u00e3o. At\u00e9 agora, s\u00f3 testamos os casos que d\u00e3o certo - ou seja, quando o usu\u00e1rio sempre existe. Mas \u00e9 igualmente importante testar o que acontece quando as coisas d\u00e3o errado. Afinal, n\u00e3o podemos simplesmente assumir que tudo sempre vai correr bem. Por isso, vamos aprender como testar esses casos negativos.

      Em seguida, vamos implementar um recurso importante em qualquer sistema de autentica\u00e7\u00e3o: o refresh do token. Isso nos permite manter a sess\u00e3o do usu\u00e1rio ativa, mesmo se o token original expirar.

      "},{"location":"07/#testes-para-autenticacao","title":"Testes para autentica\u00e7\u00e3o","text":"

      Antes de mergulharmos nos testes, vamos falar 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=400, 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":"07/#testando-a-alteracao-de-um-usuario-nao-autorizado","title":"Testando a altera\u00e7\u00e3o de um usu\u00e1rio n\u00e3o autorizado","text":"

      Agora, vamos come\u00e7ar a escrever 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, vamos criar um novo teste chamado test_update_user_with_wrong_user.

      tests/test_users.py
      def test_update_user_with_wrong_user(client, user, token):\nresponse = client.put(\nf'/users/{user.id}',\nheaders={'Authorization': f'Bearer {token}'},\njson={\n'username': 'bob',\n'email': 'bob@example.com',\n'password': 'mynewpassword',\n},\n)\nassert response.status_code == 400\nassert 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":"07/#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

      Depois de 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# ...\nclass UserFactory(factory.Factory):\nclass Meta:\nmodel = User\nid = factory.Sequence(lambda n: n)\nusername = factory.LazyAttribute(lambda obj: f'test{obj.id}')\nemail = factory.LazyAttribute(lambda obj: f'{obj.username}@test.com')\npassword = factory.LazyAttribute(lambda obj: f'{obj.username}@example.com')\n

      Explicando linha a linha, esse c\u00f3digo faz o seguinte:

      • class UserFactory(factory.Factory): Define uma f\u00e1brica para o modelo User, herdando de factory.Factory.
      • class Meta: Uma classe interna Meta \u00e9 usada para configurar a f\u00e1brica.
      • model = User: Define o modelo para o qual a f\u00e1brica est\u00e1 construindo inst\u00e2ncias. No caso, estamos referenciando a classe User, que deve ser um modelo de banco de dados, como o SQLAlchemy.
      • id = factory.Sequence(lambda n: n): Define um campo id que recebe uma sequ\u00eancia. A cada chamada da f\u00e1brica, o valor n \u00e9 incrementado, ent\u00e3o cada inst\u00e2ncia gerada ter\u00e1 um id \u00fanico.
      • username = factory.LazyAttribute(lambda obj: f'test{obj.id}'): Define o campo username usando um atributo pregui\u00e7oso, baseado no id.
      • email = factory.LazyAttribute(lambda obj: f'{obj.username}@test.com'): Define o campo email gerado a partir do username.
      • password = factory.LazyAttribute(lambda obj: f'{obj.username}@example.com'): Define o campo password similar ao campo email.

      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):\npassword = 'testtest'\nuser = UserFactory(password=get_password_hash(password))\nsession.add(user)\nsession.commit()\nsession.refresh(user)\nuser.clean_password = 'testtest'\nreturn user\n@pytest.fixture\ndef other_user(session):\npassword = 'testtest'\nuser = UserFactory(password=get_password_hash(password))\nsession.add(user)\nsession.commit()\nsession.refresh(user)\nuser.clean_password = 'testtest'\nreturn 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):\nresponse = client.put(\nf'/users/{other_user.id}',\nheaders={'Authorization': f'Bearer {token}'},\njson={\n'username': 'bob',\n'email': 'bob@example.com',\n'password': 'mynewpassword',\n},\n)\nassert response.status_code == 400\nassert response.json() == {'detail': 'Not enough permissions'}\n

      Neste caso, n\u00e3o estamos usando a fixture user porque queremos simular um cen\u00e1rio em que 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# ...\ntests/test_app.py::test_root_deve_retornar_200_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":"07/#testando-o-delete-com-o-usuario-errado","title":"Testando o DELETE com o usu\u00e1rio errado","text":"

      Continuando nossos testes, agora vamos testar 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 vamos usar:

      tests/test_users.py
      def test_delete_user_wrong_user(client, other_user, token):\nresponse = client.delete(\nf'/users/{other_user.id}',\nheaders={'Authorization': f'Bearer {token}'},\n)\nassert response.status_code == 400\nassert 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# ...\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, vamos passar 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. Vamos ver isso na pr\u00f3xima se\u00e7\u00e3o.

      "},{"location":"07/#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, vamos usar uma biblioteca chamada freezegun. freezegun \u00e9 uma biblioteca Python que nos permite controlar o tempo durante nossos testes, o que \u00e9 perfeito para testar a expira\u00e7\u00e3o do token. {aqui voc\u00ea deve expandir esse t\u00f3pico explicando um pouco mais sobre o freezegun}

      Primeiro, vamos precisar instalar a biblioteca:

      poetry add --group dev freezegun\n

      Agora vamos criar nosso teste. Vamos come\u00e7ar 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.

      tests/test_auth.py
      from freezegun import freeze_time\n# ...\ndef test_token_expiry(client, user):\nwith freeze_time('2023-07-14 12:00:00'):\nresponse = client.post(\n'/auth/token',\ndata={'username': user.email, 'password': user.clean_password},\n)\nassert response.status_code == 200\ntoken = response.json()['access_token']\nwith freeze_time('2023-07-14 13:00:00'):\nresponse = client.post(\n'/auth/refresh_token',\nheaders={'Authorization': f'Bearer {token}'},\n)\nassert response.status_code == 401\nassert 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, vamos executar nosso teste e ver o que acontece:

      $ Execu\u00e7\u00e3o no terminal!
      task test\n# ...\ntests/test_users.py::test_token_expiry PASSED\n

      \u00d3timo, nosso teste passou! Isso confirma que nosso sistema est\u00e1 lidando corretamente com a expira\u00e7\u00e3o dos tokens.

      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. Vamos ver como fazer isso na pr\u00f3xima se\u00e7\u00e3o.

      "},{"location":"07/#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. Vamos abordar 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":"07/#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):\nresponse = client.post(\n'/token',\ndata={'username': 'no_user@no_domain.com', 'password': 'testtest'},\n)\nassert response.status_code == 400\nassert response.json() == {'detail': 'Incorrect email or password'}\n
      "},{"location":"07/#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):\nresponse = client.post(\n'/token', data={'username': user.email, 'password': 'wrong_password'}\n)\nassert response.status_code == 400\nassert 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":"07/#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 vamos implementar a fun\u00e7\u00e3o de renova\u00e7\u00e3o de token em nosso c\u00f3digo.

      fast_zero/routes/auth.py
      from fast_zero.security import (\ncreate_access_token,\nget_current_user,\nverify_password,\n)\n# ...\n@router.post('/refresh_token', response_model=Token)\ndef refresh_access_token(\nuser: User = Depends(get_current_user),\n):\nnew_access_token = create_access_token(data={'sub': user.email})\nreturn {'access_token': new_access_token, 'token_type': 'bearer'}\n

      Vamos tamb\u00e9m implementar 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):\nresponse = client.post(\n'/refresh_token',\nheaders={'Authorization': f'Bearer {token}'},\n)\ndata = response.json()\nassert response.status_code == 200\nassert 'access_token' in data\nassert 'token_type' in data\nassert response.json()['token_type'] == 'bearer'\n

      Ainda \u00e9 importante garantir que nosso sistema trate corretamente os tokens expirados. Para isso, vamos adicionar um teste que verifica se um token expirado n\u00e3o pode ser usado para renovar um token.

      tests/test_auth.py
      def test_token_expiry(client, user):\nwith freeze_time('2023-07-14 12:00:00'):\nresponse = client.post(\n'/token',\ndata={'username': user.email, 'password': user.clean_password},\n)\nassert response.status_code == 200\ntoken = response.json()['access_token']\nwith freeze_time('2023-07-14 13:00:00'):\nresponse = client.post(\n'/refresh_token',\nheaders={'Authorization': f'Bearer {token}'},\n)\nassert response.status_code == 401\nassert 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# ...\ntests/test_app.py::test_root_deve_retornar_200_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_expiry 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_expiry 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":"07/#commit","title":"Commit","text":"

      Agora, vamos fazer um commit com as altera\u00e7\u00f5es que fizemos.

      $ Execu\u00e7\u00e3o no terminal!
      git add .\ngit commit -m \"Implement refresh token and add relevant tests\"\n
      "},{"location":"07/#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 hoje vai al\u00e9m do b\u00e1sico, mostrando como lidar com cen\u00e1rios complexos e realistas.

      A implementa\u00e7\u00e3o e os testes que fizemos hoje nos levam um passo adiante no desenvolvimento da nossa aplica\u00e7\u00e3o, deixando-a mais pr\u00f3xima de estar pronta para um ambiente de produ\u00e7\u00e3o.

      Na pr\u00f3xima aula, vamos utilizar a infraestrutura de autentica\u00e7\u00e3o que criamos hoje 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. Esperamos ver voc\u00ea na pr\u00f3xima aula!

      "},{"location":"08/","title":"[WIP] Criando Rotas CRUD para Gerenciamento de Tarefas em FastAPI","text":""},{"location":"08/#wip-criando-rotas-crud-para-gerenciamento-de-tarefas-em-fastapi","title":"[WIP] Criando Rotas CRUD para Gerenciamento de Tarefas em FastAPI","text":"

      Objetivos da Aula:

      • Cria\u00e7\u00e3o das rotas para as opera\u00e7\u00f5es CRUD das tarefas
      • Fazer com s\u00f3 o usu\u00e1rio dono da tarefa possa acessar e modificar suas tarefas
      • Escrita e execu\u00e7\u00e3o dos testes para cada opera\u00e7\u00e3o das tarefas
      Caso prefira ver a aula em v\u00eddeo

      Aula Slides C\u00f3digo

      Ol\u00e1 a todos! Estamos de volta com mais uma aula. Hoje vamos mergulhar na cria\u00e7\u00e3o das rotas CRUD para as nossas tarefas utilizando FastAPI. Essas opera\u00e7\u00f5es s\u00e3o fundamentais para qualquer aplica\u00e7\u00e3o de gerenciamento de tarefas e s\u00e3o o cora\u00e7\u00e3o do nosso sistema. Al\u00e9m disso, garantiremos que apenas o usu\u00e1rio que criou a tarefa possa acess\u00e1-la e modific\u00e1-la, garantindo a seguran\u00e7a e a privacidade dos dados. Vamos come\u00e7ar!

      "},{"location":"08/#estrutura-inicial-do-codigo","title":"Estrutura inicial do c\u00f3digo","text":"

      Primeiro, vamos criar um novo arquivo chamado todos.py dentro do diret\u00f3rio de routes:

      fast_zero/routes/todos.py
      from fastapi import APIRouter\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.

      Depois de definir o roteador, precisamos inclu\u00ed-lo em nossa aplica\u00e7\u00e3o principal. Vamos atualizar o arquivo fast_zero/app.py para incluir as rotas de tarefas que iremos criar:

      fast_zero/app.py
      from fastapi import FastAPI\nfrom fast_zero.routes import auth, todos, users\napp = FastAPI()\napp.include_router(users.router)\napp.include_router(auth.router)\napp.include_router(todos.router)\n@app.get('/')\ndef read_root():\nreturn {'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":"08/#implementacao-da-tabela-no-banco-de-dados","title":"Implementa\u00e7\u00e3o da tabela no Banco de dados","text":"

      Agora, iremos implementar 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.

      from enum import Enum\nfrom sqlalchemy import ForeignKey\nfrom sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship\nclass TodoState(str, Enum):\ndraft = 'draft'\ntodo = 'todo'\ndoing = 'doing'\ndone = 'done'\ntrash = 'trash'\nclass Base(DeclarativeBase):\npass\nclass User(Base):\n__tablename__ = 'users'\nid: Mapped[int] = mapped_column(primary_key=True)\nusername: Mapped[str]\npassword: Mapped[str]\nemail: Mapped[str]\ntodos: Mapped[list['Todo']] = relationship(\nback_populates='user', cascade='all, delete-orphan'\n)\nclass Todo(Base):\n__tablename__ = 'todos'\nid: Mapped[int] = mapped_column(primary_key=True)\ntitle: Mapped[str]\ndescription: Mapped[str]\nstate: Mapped[TodoState]\nuser_id: Mapped[int] = mapped_column(ForeignKey('users.id'))\nuser: Mapped[User] = relationship(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":"08/#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: Session, user: User):\ntodo = Todo(\ntitle='Test Todo',\ndescription='Test Desc',\nstate='draft',\nuser_id=user.id,\n)\nsession.add(todo)\nsession.commit()\nsession.refresh(todo)\nuser = session.scalar(select(User).where(User.id == user.id))\nassert 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 vamos come\u00e7ar a criar os esquemas para esse modelo e, em seguida, os endpoints.

      "},{"location":"08/#schemas-para-todos","title":"Schemas para Todos","text":"

      Vamos criar dois esquemas para nosso modelo de tarefas (todos): TodoSchema e TodoPublic.

      fast_zero/schemas.py
      from fast_zero.models import TodoState\n#...\nclass TodoSchema(BaseModel):\ntitle: str\ndescription: str\nstate: TodoState\nclass TodoPublic(BaseModel):\nid: int\ntitle: str\ndescription: str\nstate: TodoState\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":"08/#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.

      from typing import Annotated\nfrom fastapi import APIRouter, Depends\nfrom sqlalchemy.orm import Session\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\nrouter = APIRouter()\nCurrentUser = Annotated[User, Depends(get_current_user)]\nrouter = APIRouter(prefix='/todos', tags=['todos'])\n@router.post('/', response_model=TodoPublic)\ndef create_todo(\ntodo: TodoSchema,\nuser: CurrentUser,\nsession: Session = Depends(get_session),\n):\ndb_todo: Todo = Todo(\ntitle=todo.title,\ndescription=todo.description,\nstate=todo.state,\nuser_id=user.id,\n)\nsession.add(db_todo)\nsession.commit()\nsession.refresh(db_todo)\nreturn 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":"08/#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):\nresponse = client.post(\n'/todos/',\nheaders={'Authorization': f'Bearer {token}'},\njson={\n'title': 'Test todo',\n'description': 'Test todo description',\n'state': 'draft',\n},\n)\nassert 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":"08/#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# ...\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! ###\nop.create_table('todos',\nsa.Column('id', sa.Integer(), nullable=False),\nsa.Column('title', sa.String(), nullable=False),\nsa.Column('description', sa.String(), nullable=False),\nsa.Column('state', sa.Enum('draft', 'todo', 'doing', 'done', 'trash', name='todostate'), nullable=False),\nsa.Column('user_id', sa.Integer(), nullable=False),\nsa.ForeignKeyConstraint(['user_id'], ['users.id'], ),\nsa.PrimaryKeyConstraint('id')\n)\n# ### end Alembic commands ###\ndef downgrade() -> None:\n# ### commands auto generated by Alembic - please adjust! ###\nop.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 (\nid INTEGER NOT NULL,\ntitle VARCHAR NOT NULL,\ndescription VARCHAR NOT NULL,\nstate VARCHAR(5) NOT NULL,\nuser_id INTEGER NOT NULL,\nPRIMARY KEY (id),\nFOREIGN 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":"08/#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 tarrefas \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, que s\u00e3o 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/routes/todos.py
      @router.get('/', response_model=ListTodos)\ndef list_todos(\nsession: Session,\nuser: CurrentUser,\ntitle: str = Query(None),\ndescription: str = Query(None),\nstate: str = Query(None),\noffset: int = Query(None),\nlimit: int = Query(None),\n):\nquery = select(Todo).where(Todo.user_id == user.id)\nif title:\nquery = query.filter(Todo.title.contains(title))\nif description:\nquery = query.filter(Todo.description.contains(description))\nif state:\nquery = query.filter(Todo.state == state)\ntodos = session.scalars(query.offset(offset).limit(limit)).all()\nreturn {'todos': todos}\n

      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.

      • Todo.title.contains(title): Verifica se o t\u00edtulo da tarefa cont\u00e9m a string fornecida.
      • Todo.description.contains(description): Verifica se a descri\u00e7\u00e3o da tarefa cont\u00e9m a string fornecida.
      • Todo.state == state: Compara o estado da tarefa com o valor fornecido.

      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":"08/#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 faz uso de 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\nfrom fast_zero.models import Todo, TodoState, User\n# ...\nclass TodoFactory(factory.Factory):\nclass Meta:\nmodel = Todo\ntitle = factory.Faker('text')\ndescription = factory.Faker('text')\nstate = factory.fuzzy.FuzzyChoice(TodoState)\nuser_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":"08/#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. Vamos separar os testes em pequenos blocos e explicar cada um deles.

      "},{"location":"08/#testando-a-listagem-de-todos","title":"Testando a Listagem de Todos","text":"

      Primeiro, vamos criar um teste b\u00e1sico que verifica se o endpoint est\u00e1 listando todos os objetos Todo.

      tests/test_todos.py
      def test_list_todos(session, client, user, token):\nsession.bulk_save_objects(TodoFactory.create_batch(5, user_id=user.id))\nsession.commit()\nresponse = client.get(\n'/todos/',\nheaders={'Authorization': f'Bearer {token}'},\n)\nassert len(response.json()['todos']) == 5\n

      Este teste valida que todos os 5 objetos Todo s\u00e3o retornados pelo endpoint.

      "},{"location":"08/#testando-a-paginacao","title":"Testando a Pagina\u00e7\u00e3o","text":"

      Em seguida, vamos testar a pagina\u00e7\u00e3o para garantir que o offset e o limite estejam funcionando corretamente.

      tests/test_todos.py
      def test_list_todos_pagination(session, user, client, token):\nsession.bulk_save_objects(TodoFactory.create_batch(5, user_id=user.id))\nsession.commit()\nresponse = client.get(\n'/todos/?offset=1&limit=2',\nheaders={'Authorization': f'Bearer {token}'},\n)\nassert len(response.json()['todos']) == 2\n

      Este teste verifica que, quando aplicado o offset de 1 e o limite de 2, apenas 2 objetos Todo s\u00e3o retornados.

      "},{"location":"08/#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(session, user, client, token):\nsession.bulk_save_objects(\nTodoFactory.create_batch(5, user_id=user.id, title='Test todo 1')\n)\nsession.commit()\nresponse = client.get(\n'/todos/?title=Test todo 1',\nheaders={'Authorization': f'Bearer {token}'},\n)\nassert len(response.json()['todos']) == 5\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":"08/#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(session, user, client, token):\nsession.bulk_save_objects(\nTodoFactory.create_batch(5, user_id=user.id, description='description')\n)\nsession.commit()\nresponse = client.get(\n'/todos/?description=desc',\nheaders={'Authorization': f'Bearer {token}'},\n)\nassert len(response.json()['todos']) == 5\n

      Este teste verifica que, quando filtramos pela descri\u00e7\u00e3o, apenas as tarefas com a descri\u00e7\u00e3o correspondente s\u00e3o retornadas.

      "},{"location":"08/#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(session, user, client, token):\nsession.bulk_save_objects(\nTodoFactory.create_batch(5, user_id=user.id, state=TodoState.draft)\n)\nsession.commit()\nresponse = client.get(\n'/todos/?state=draft',\nheaders={'Authorization': f'Bearer {token}'},\n)\nassert len(response.json()['todos']) == 5\n

      Este teste garante que quando filtramos pelo estado, apenas as tarefas com o estado correspondente s\u00e3o retornadas.

      "},{"location":"08/#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, vamos criar 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(session, user, client, token):\nsession.bulk_save_objects(\nTodoFactory.create_batch(\n5,\nuser_id=user.id,\ntitle='Test todo combined',\ndescription='combined description',\nstate=TodoState.done,\n)\n)\nsession.bulk_save_objects(\nTodoFactory.create_batch(\n3,\nuser_id=user.id,\ntitle='Other title',\ndescription='other description',\nstate=TodoState.todo,\n)\n)\nsession.commit()\nresponse = client.get(\n'/todos/?title=Test todo combined&description=combined&state=done',\nheaders={'Authorization': f'Bearer {token}'},\n)\nassert len(response.json()['todos']) == 5\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":"08/#executando-os-testes","title":"Executando os testes","text":"

      Importante para que n\u00e3o esque\u00e7amos \u00e9 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\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":"08/#endpoints-para-alteracao-e-delecao","title":"Endpoints para altera\u00e7\u00e3o e dele\u00e7\u00e3o","text":"

      {Aqui voc\u00ea deve dizer que para fazer o patch, precisamos de um modelo onde tudo seja opcional}

      fast_zero/schemas.py
      class TodoUpdate(BaseModel):\ntitle: str | None = None\ndescription: str | None = None\ncompleted: str | None = None\n
      fast_zero/routes/todos.py
      @router.delete('/{todo_id}', response_model=Message)\ndef delete_todo(todo_id: int, session: Session, user: CurrentUser):\ntodo = session.scalar(\nselect(Todo).where(Todo.user_id == user.id, Todo.id == todo_id)\n)\nif not todo:\nraise HTTPException(status_code=404, detail='Task not found.')\nsession.delete(todo)\nsession.commit()\nreturn {'detail': 'Task has been deleted successfully.'}\n@router.patch('/{todo_id}', response_model=TodoPublic)\ndef patch_todo(\ntodo_id: int, session: Session, user: CurrentUser, todo: TodoUpdate\n):\ndb_todo = session.scalar(\nselect(Todo).where(Todo.user_id == user.id, Todo.id == todo_id)\n)\nif not db_todo:\nraise HTTPException(status_code=404, detail='Task not found.')\nfor key, value in todo.model_dump(exclude_unset=True).items():\nsetattr(db_todo, key, value)\nsession.add(db_todo)\nsession.commit()\nsession.refresh(db_todo)\nreturn db_todo\n
      "},{"location":"08/#testes-para-esses-endpoints","title":"Testes para esses endpoints","text":"fast_zero/tests/test_todos.py
      def test_delete_todo(session, client, user, token):\ntodo = TodoFactory(id=1, user_id=user.id)\nsession.add(todo)\nsession.commit()\nresponse = client.delete(\nf'/todos/{todo.id}', headers={'Authorization': f'Bearer {token}'}\n)\nassert response.status_code == 200\nassert response.json() == {'detail': 'Task has been deleted successfully.'}\ndef test_delete_todo_error(client, token):\nresponse = client.delete(\nf'/todos/{10}', headers={'Authorization': f'Bearer {token}'}\n)\nassert response.status_code == 404\nassert response.json() == {'detail': 'Task not found.'}\ndef test_patch_todo_error(client, token):\nresponse = client.patch(\n'/todos/10',\njson={},\nheaders={'Authorization': f'Bearer {token}'},\n)\nassert response.status_code == 404\nassert response.json() == {'detail': 'Task not found.'}\ndef test_patch_todo(session, client, user, token):\ntodo = TodoFactory(user_id=user.id)\nsession.add(todo)\nsession.commit()\nresponse = client.patch(\nf'/todos/{todo.id}',\njson={'title': 'teste!'},\nheaders={'Authorization': f'Bearer {token}'},\n)\nassert response.status_code == 200\nassert response.json()['title'] == 'teste!'\n
      "},{"location":"08/#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 \"Implement DELETE and PATCH endpoints for todos\"
      "},{"location":"08/#conclusao","title":"Conclus\u00e3o","text":"

      Parab\u00e9ns! Voc\u00ea acabou de implementar os endpoints de DELETE e PATCH para suas tarefas. Isso permite que os usu\u00e1rios agora possam remover tarefas que n\u00e3o s\u00e3o mais necess\u00e1rias e atualizar os detalhes de tarefas existentes.

      Atrav\u00e9s dos testes, voc\u00ea assegurou que as opera\u00e7\u00f5es de DELETE e PATCH est\u00e3o funcionando corretamente. Al\u00e9m disso, voc\u00ea aprendeu como lidar com situa\u00e7\u00f5es onde a tarefa a ser alterada ou removida n\u00e3o existe.

      Na pr\u00f3xima aula, vamos abordar um t\u00f3pico extremamente relevante no desenvolvimento de aplica\u00e7\u00f5es modernas: a dockeriza\u00e7\u00e3o da nossa aplica\u00e7\u00e3o. Iremos aprender como colocar a nossa aplica\u00e7\u00e3o dentro de um container Docker, o que facilita o deploy e o escalonamento dela. At\u00e9 l\u00e1!

      "},{"location":"09/","title":"[OK] Dockerizando a nossa aplica\u00e7\u00e3o","text":""},{"location":"09/#ok-dockerizando-a-nossa-aplicacao","title":"[OK] Dockerizando a nossa aplica\u00e7\u00e3o","text":"

      Objetivos da aula:

      • Compreender os conceitos b\u00e1sicos do Docker
      • Aprender o que \u00e9 um Dockerfile e sua estrutura
      • Entender como criar uma imagem Docker para a nossa aplica\u00e7\u00e3o FastAPI
      • Aprender a rodar a aplica\u00e7\u00e3o utilizando Docker
      Caso prefira ver a aula em v\u00eddeo

      Aula Slides C\u00f3digo

      Depois de implementar nosso gerenciador de tarefas na aula anterior, temos uma vers\u00e3o est\u00e1vel da nossa aplica\u00e7\u00e3o. Agora, vamos aprender como podemos \"dockerizar\" nossa aplica\u00e7\u00e3o FastAPI. Essa etapa \u00e9 crucial para garantir que nossa aplica\u00e7\u00e3o seja facilmente transport\u00e1vel e possa ser executada em qualquer ambiente sem problemas.

      "},{"location":"09/#o-docker","title":"O Docker","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.

      "},{"location":"09/#criando-nosso-dockerfile","title":"Criando nosso Dockerfile","text":"

      O Dockerfile \u00e9 um arquivo de texto que cont\u00e9m todas as instru\u00e7\u00f5es para construir uma imagem Docker. 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.

      Aqui est\u00e1 o nosso Dockerfile para a aplica\u00e7\u00e3o FastAPI:

      FROM python:3.11-slim\nENV POETRY_VIRTUALENVS_CREATE=false\nRUN pip install poetry\n\nCOPY . .\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.
      3. RUN pip install poetry: Instala o Poetry, nosso gerenciador de pacotes.
      4. COPY . .: Copia todos os arquivos do diret\u00f3rio atual para o cont\u00eainer.
      5. RUN poetry config installer.max-workers 10: Configura o Poetry para usar at\u00e9 10 workers ao instalar pacotes.
      6. RUN poetry install --no-interaction --no-ansi: Instala as depend\u00eancias do nosso projeto sem intera\u00e7\u00e3o e sem cores no output.
      7. EXPOSE 8000: Informa ao Docker que o cont\u00eainer vai escutar na porta 8000.
      8. 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:

      • poetry run define o comando que ser\u00e1 executado dentro do ambiente virtual criado pelo Poetry
      • uvicorn \u00e9 o servidor ASGI que usamos para rodar nossa aplica\u00e7\u00e3o.
      • --host define o host que o servidor vai escutar
      • fast_zero.app:app define o <m\u00f3dulo python>:<objeto> que o servidor vai executar.
      "},{"location":"09/#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

      Este comando l\u00ea o Dockerfile no diret\u00f3rio atual (indicado pelo .) e cria uma imagem com a tag \"fast_zero\", (indicada pelo -t).

      Vamos ent\u00e3o verificar 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":"09/#rodando-o-container","title":"Rodando o container","text":"

      Para rodar o cont\u00eainer, usamos o comando docker run, indicando o nome da imagem que queremos executar e a tag que queremos usar <nome_da_imagem>:<tag> e utilizamos a flag -p 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 fast_zero:latest -p 8000:8000\n

      Este comando iniciar\u00e1 nossa aplica\u00e7\u00e3o dentro de um cont\u00eainer Docker, que estar\u00e1 escutando na porta 8000.

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

      Ap\u00f3s criar o Dockerfile e testar a imagem Docker, podemos fazer o commit das altera\u00e7\u00f5es no Git:

      1. Adicione o Dockerfile e quaisquer outros arquivos modificados ao stage do Git com git add .
      2. Fa\u00e7a o commit das altera\u00e7\u00f5es com git commit -m \"Dockerize application\"
      3. Empurre as altera\u00e7\u00f5es para o reposit\u00f3rio remoto com git push
      "},{"location":"09/#conclusao","title":"Conclus\u00e3o","text":"

      Dockerizar nossa aplica\u00e7\u00e3o FastAPI permite que ela seja executada de forma consistente em qualquer ambiente, facilitando a implanta\u00e7\u00e3o e a distribui\u00e7\u00e3o. Na pr\u00f3xima aula, vamos aprender como podemos levar nossa aplica\u00e7\u00e3o para o pr\u00f3ximo n\u00edvel ao implant\u00e1-la em produ\u00e7\u00e3o usando o Fly.io.

      "},{"location":"10/","title":"[OK] Automatizando os testes com Integra\u00e7\u00e3o Cont\u00ednua","text":""},{"location":"10/#ok-automatizando-os-testes-com-integracao-continua","title":"[OK] Automatizando os testes com Integra\u00e7\u00e3o Cont\u00ednua","text":"

      Objetivos da aula:

      • Compreender o conceito de Integra\u00e7\u00e3o Cont\u00ednua (CI)
      • Familiarizar-se com a ferramenta GitHub Actions
      • Aprender a configurar um pipeline de CI para testar a aplica\u00e7\u00e3o automaticamente
      • Entender a import\u00e2ncia de uma cobertura de testes abrangente
      • Conhecer a plataforma Codecov para monitoramento da cobertura de testes

      Na aula passada, preparamos nossa aplica\u00e7\u00e3o para execu\u00e7\u00e3o em containers Docker. Nesta aula, focaremos em garantir que nossa aplica\u00e7\u00e3o continue funcionando conforme o esperado a cada atualiza\u00e7\u00e3o de c\u00f3digo. Para isso, introduziremos o conceito de Integra\u00e7\u00e3o Cont\u00ednua (CI).

      "},{"location":"10/#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 frequente de c\u00f3digo ao projeto principal. Com cada integra\u00e7\u00e3o - geralmente um commit - \u00e9 disparado um processo automatizado que constr\u00f3i e testa o c\u00f3digo. Isso permite detectar e corrigir problemas rapidamente, contribuindo para a manuten\u00e7\u00e3o da qualidade do software.

      "},{"location":"10/#github-actions","title":"GitHub Actions","text":"

      GitHub Actions \u00e9 um servi\u00e7o fornecido pelo GitHub que permite a automatiza\u00e7\u00e3o de workflows, incluindo a execu\u00e7\u00e3o de testes e implanta\u00e7\u00e3o de software, diretamente em seu reposit\u00f3rio GitHub. Cada tarefa \u00e9 definida como uma \"a\u00e7\u00e3o\", e a\u00e7\u00f5es podem ser combinadas para criar um \"workflow\" que atende a necessidades espec\u00edficas de desenvolvimento.

      "},{"location":"10/#configurando-o-workflow-de-ci","title":"Configurando o workflow de CI","text":"

      Vamos configurar um workflow de CI para nossa aplica\u00e7\u00e3o utilizando o GitHub Actions. Crie um novo arquivo em seu reposit\u00f3rio, sob o diret\u00f3rio .github/workflows/, e copie o seguinte c\u00f3digo:

      .github/workflows/pipeline.yaml
      name: Pipeline\non: [push, pull_request]\njobs:\ntest:\nruns-on: ubuntu-latest\nsteps:\n- name: Copia os arquivos do repo\nuses: actions/checkout@v3\n- name: Instalar o python\nuses: actions/setup-python@v4\nwith:\npython-version: '3.11.1'\n- name: Instalar Poetry\nrun: pip install poetry\n- name: Instalar depend\u00eancias do projeto\nrun: poetry install\n- name: Rodar os testes\nrun: poetry run task test --cov-report=xml\n- name: Subir cobertura para o codecov\nuses: codecov/codecov-action@v3\nwith:\ntoken: ${{ secrets.CODECOV_TOKEN }}\n

      Vamos analisar este arquivo:

      • name: Define o nome do workflow.
      • on: Define os eventos que acionar\u00e3o o workflow (neste caso, push e pull requests).
      • jobs: Define os jobs que ser\u00e3o executados. Nesse caso, temos um job chamado \"test\".
      • runs-on: Define o ambiente em que o job ser\u00e1 executado (neste caso, a \u00faltima vers\u00e3o do Ubuntu).
      • steps: Define os passos do job:
      • actions/checkout@v3: Faz o checkout do reposit\u00f3rio.
      • actions/setup-python@v4: Instala a vers\u00e3o especificada do Python.
      • pip install poetry: Instala a ferramenta Poetry.
      • poetry install: Instala as depend\u00eancias do projeto.
      • poetry run task test --cov-report=xml: Executa os testes e gera um relat\u00f3rio de cobertura em formato xml.
      • codecov/codecov-action@v3: Faz o upload do relat\u00f3rio de cobertura para o Codecov. O token \u00e9 fornecido atrav\u00e9s de uma vari\u00e1vel de ambiente segura definida nas configura\u00e7\u00f5es do reposit\u00f3rio.
      "},{"location":"10/#commit","title":"Commit","text":"

      Ap\u00f3s adicionar e configurar o arquivo do workflow, voc\u00ea deve commitar as mudan\u00e7as em seu reposit\u00f3rio. Siga os passos:

      $ Execu\u00e7\u00e3o no terminal!
      git add .github/workflows/pipeline.yaml\ngit commit -m \"Add CI pipeline\"\ngit push\n
      "},{"location":"10/#conclusao","title":"Conclus\u00e3o","text":"

      A Integra\u00e7\u00e3o Cont\u00ednua \u00e9 uma pr\u00e1tica fundamental no desenvolvimento moderno de software, e o GitHub Actions \u00e9 uma ferramenta poderosa para implementar essa pr\u00e1tica. Ele n\u00e3o apenas ajuda a manter a qualidade do c\u00f3digo ao garantir que todos os testes sejam executados a cada commit, mas tamb\u00e9m permite detectar e corrigir problemas mais cedo no ciclo de desenvolvimento.

      Al\u00e9m disso, monitorar a cobertura de testes com o Codecov nos permite manter um alto padr\u00e3o de qualidade, garantindo que todas as partes do nosso c\u00f3digo sejam testadas.

      Na pr\u00f3xima aula, vamos levar nossa aplica\u00e7\u00e3o ao pr\u00f3ximo n\u00edvel, preparando-a para o deployment em produ\u00e7\u00e3o!

      "},{"location":"11/","title":"[WIP] Fazendo deploy no Fly.io e configurando o PostgreSQL","text":""},{"location":"11/#wip-fazendo-deploy-no-flyio-e-configurando-o-postgresql","title":"[WIP] Fazendo deploy no Fly.io e configurando o PostgreSQL","text":"

      Objetivos da aula:

      • Entender o que \u00e9 o Fly.io e como instalar sua CLI
      • Aprender a fazer o deploy de uma aplica\u00e7\u00e3o Docker no Fly.io
      • Configurar uma inst\u00e2ncia do PostgreSQL no Fly.io
      • Configurar as vari\u00e1veis de ambiente
      • Rodar as migra\u00e7\u00f5es do Alembic
      • Configurar o deploy cont\u00ednuo no Github Actions
      "},{"location":"11/#o-flyio-e-a-instalacao-da-cli","title":"O Fly.io e a instala\u00e7\u00e3o da CLI","text":"

      Na aula anterior, n\u00f3s automatizamos nossos testes e integramos tudo em um pipeline de integra\u00e7\u00e3o e deploy cont\u00ednuos. Agora, vamos aprender a fazer o deploy da nossa aplica\u00e7\u00e3o em um ambiente de produ\u00e7\u00e3o usando o Fly.io.

      O Fly.io \u00e9 uma plataforma de deploy que nos permite lan\u00e7ar nossas aplica\u00e7\u00f5es Docker na nuvem. Ele tamb\u00e9m fornece uma s\u00e9rie de recursos, como balanceamento de carga, cria\u00e7\u00e3o de inst\u00e2ncias de banco de dados e configura\u00e7\u00e3o de vari\u00e1veis de ambiente.

      Para iniciar, precisamos instalar a CLI do Fly.io, chamada flyctl. Voc\u00ea pode baix\u00e1-la no site oficial do Fly.io. Com o flyctl instalado, precisamos fazer login na nossa conta do Fly.io usando o comando:

      $ Execu\u00e7\u00e3o no terminal!
      fly auth login\n

      Este comando ir\u00e1 abrir o navegador para voc\u00ea entrar com suas credenciais do Fly.io.

      "},{"location":"11/#criando-a-aplicacao-no-flyio-e-fazendo-o-deploy","title":"Criando a aplica\u00e7\u00e3o no Fly.io e fazendo o deploy","text":"

      Depois de logado, podemos criar uma nova aplica\u00e7\u00e3o no Fly.io usando o comando:

      $ Execu\u00e7\u00e3o no terminal!
      fly launch\n

      Este comando ir\u00e1 perguntar algumas informa\u00e7\u00f5es sobre sua aplica\u00e7\u00e3o e ent\u00e3o criar\u00e1 uma nova aplica\u00e7\u00e3o no Fly.io. Com nossa aplica\u00e7\u00e3o criada, podemos agora fazer o deploy da nossa imagem Docker usando o comando:

      $ Execu\u00e7\u00e3o no terminal!
      fly deploy --local-only\n

      A op\u00e7\u00e3o --local-only diz para o flyctl construir a imagem Docker localmente e depois fazer o upload dela para o Fly.io.

      "},{"location":"11/#configurando-a-instancia-do-postgresql-no-flyio","title":"Configurando a inst\u00e2ncia do PostgreSQL no Fly.io","text":"

      Agora, vamos criar uma inst\u00e2ncia do PostgreSQL. O Fly.io fornece um servi\u00e7o PostgreSQL que podemos usar para criar uma nova inst\u00e2ncia do PostgreSQL com apenas alguns comandos.

      "},{"location":"11/#configurando-as-variaveis-de-ambiente-e-rodando-as-migracoes-do-alembic","title":"Configurando as vari\u00e1veis de ambiente e rodando as migra\u00e7\u00f5es do Alembic","text":"

      Com a inst\u00e2ncia criada, algumas vari\u00e1veis de ambiente ser\u00e3o automaticamente definidas para n\u00f3s. Para que o Alembic possa executar as migra\u00e7\u00f5es, precisamos configurar a vari\u00e1vel DATABASE_URL no nosso aplicativo para apontar para a inst\u00e2ncia do PostgreSQL do Fly.io.

      $ Execu\u00e7\u00e3o no terminal!
      fly secrets set DATABASE_URL=<value>\n

      Substitua <value> pela string de conex\u00e3o do seu banco de dados PostgreSQL.

      Finalmente, podemos executar nossas migra\u00e7\u00f5es Alembic. Usaremos a CLI do Fly.io para executar o comando dentro de um cont\u00eainer do nosso aplicativo:

      $ Execu\u00e7\u00e3o no terminal!
      fly ssh console --app <your-app-name> 'poetry run alembic upgrade head'\n

      Substitua <your-app-name> pelo nome do seu aplicativo no Fly.io.

      "},{"location":"11/#configurando-o-deploy-continuo-no-github-actions","title":"Configurando o deploy cont\u00ednuo no Github Actions","text":"

      Agora que temos nosso aplicativo funcionando no Fly.io, podemos configurar o Github Actions para fazer o deploy autom\u00e1tico sempre que fizermos um push no nosso reposit\u00f3rio. Para isso, precisaremos adicionar alguns passos ao nosso arquivo de pipeline do Github Actions:

      - name: Build and push Docker image to Fly.io\nrun: |\nflyctl deploy --local-only\nflyctl deploy\n

      Com isso, nossa aplica\u00e7\u00e3o est\u00e1 pronta para uso no Fly.io!

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

      Ao longo desta aula, n\u00f3s mergulhamos no mundo do deploy de aplica\u00e7\u00f5es com o Fly.io, uma plataforma que facilita imensamente a tarefa de colocar nossas aplica\u00e7\u00f5es para funcionar na nuvem. Al\u00e9m disso, tamb\u00e9m tivemos a chance de entender como gerenciar vari\u00e1veis de ambiente de forma segura e eficiente, permitindo a nossa aplica\u00e7\u00e3o se adaptar a diferentes contextos de execu\u00e7\u00e3o.

      Aprendemos como subir nossa imagem Docker no Fly.io e como este processo pode ser simplificado e automatizado. Tamb\u00e9m vimos como \u00e9 poss\u00edvel ter nosso banco de dados rodando no mesmo ambiente da nossa aplica\u00e7\u00e3o, facilitando a manuten\u00e7\u00e3o e a escalabilidade.

      Configuramos e utilizamos o PostgreSQL no Fly.io, o que nos deu uma vis\u00e3o pr\u00e1tica de como gerenciar bancos de dados em um ambiente de produ\u00e7\u00e3o. Ao fazer isso, pudemos integrar ainda mais a nossa aplica\u00e7\u00e3o ao ambiente em que ela est\u00e1 rodando.

      Al\u00e9m disso, exploramos a import\u00e2ncia das migra\u00e7\u00f5es e como elas podem ser gerenciadas usando o Alembic, que nos permitiu atualizar nosso banco de dados de forma controlada e rastre\u00e1vel.

      Finalmente, vimos como podemos automatizar todo o processo de deploy usando o Github Actions. Esta \u00e9 uma pr\u00e1tica extremamente \u00fatil e poderosa, pois permite que a nossa aplica\u00e7\u00e3o esteja sempre atualizada com as \u00faltimas altera\u00e7\u00f5es que fizemos, sem a necessidade de qualquer interven\u00e7\u00e3o manual.

      Com todas essas pe\u00e7as, temos agora uma aplica\u00e7\u00e3o robusta e pronta para escalar, com todos os elementos necess\u00e1rios para ser operada em um ambiente de produ\u00e7\u00e3o real. Estas s\u00e3o ferramentas e pr\u00e1ticas que est\u00e3o no cora\u00e7\u00e3o do desenvolvimento de software moderno, e domin\u00e1-las nos permitir\u00e1 construir aplica\u00e7\u00f5es cada vez mais complexas e eficientes.

      Na pr\u00f3xima aula, faremos uma recapitula\u00e7\u00e3o de tudo o que aprendemos neste curso e discutiremos os pr\u00f3ximos passos. Continue acompanhando para fortalecer ainda mais seus conhecimentos em desenvolvimento de aplica\u00e7\u00f5es com FastAPI, Docker, CI/CD e muito mais. At\u00e9 a pr\u00f3xima aula!

      "},{"location":"12/","title":"[WIP] Despedida","text":""},{"location":"12/#wip-despedida","title":"[WIP] Despedida","text":"

      Objetivos da aula:

      • Fazer uma revis\u00e3o geral dos conceitos e pr\u00e1ticas que abordamos ao longo do curso.
      • Encorajar a explora\u00e7\u00e3o futura e o aprofundamento em \u00e1reas espec\u00edficas.
      • Agradecer a todos que acompanharam esta s\u00e9rie de aulas.
      "},{"location":"12/#introducao","title":"Introdu\u00e7\u00e3o","text":"

      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":"12/#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:

      • FastAPI: Conhecemos e utilizamos o FastAPI, um moderno framework de desenvolvimento web para Python, que nos permite criar APIs de alto desempenho de forma eficiente e com menos c\u00f3digo.

      • Docker: Aprendemos a utilizar o Docker para criar um ambiente isolado e replic\u00e1vel para nossa aplica\u00e7\u00e3o, o que facilita tanto o desenvolvimento quanto o deploy em produ\u00e7\u00e3o.

      • Testes e TDD: Abordamos a import\u00e2ncia dos testes automatizados e da metodologia TDD (Test Driven Development) para garantir a qualidade e a confiabilidade do nosso c\u00f3digo.

      • Banco de dados e migra\u00e7\u00f5es: Trabalhamos com bancos de dados SQL, utilizando o SQLAlchemy para a comunica\u00e7\u00e3o com o banco de dados, e o Alembic para gerenciar as migra\u00e7\u00f5es de banco de dados.

      • Autentica\u00e7\u00e3o e autoriza\u00e7\u00e3o: Implementamos funcionalidades de autentica\u00e7\u00e3o e autoriza\u00e7\u00e3o em nossa aplica\u00e7\u00e3o, utilizando o padr\u00e3o JWT.

      • Integra\u00e7\u00e3o Cont\u00ednua (CI): Utilizamos o Github Actions para criar um pipeline de CI, garantindo que os testes s\u00e3o sempre executados e que o c\u00f3digo mant\u00e9m uma qualidade constante.

      • Deploy em produ\u00e7\u00e3o: Por fim, fizemos o deploy da nossa aplica\u00e7\u00e3o em um ambiente de produ\u00e7\u00e3o real, utilizando o Fly.io, e aprendemos a gerenciar e configurar esse ambiente.

      "},{"location":"12/#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 a pr\u00f3xima!

      "}]} \ No newline at end of file diff --git a/sitemap.xml b/sitemap.xml index 1e45fae7..16435660 100644 --- a/sitemap.xml +++ b/sitemap.xml @@ -2,67 +2,67 @@ https://fastapidozero.dunossauro.com/ - 2023-08-02 + 2023-08-03 daily https://fastapidozero.dunossauro.com/01/ - 2023-08-02 + 2023-08-03 daily https://fastapidozero.dunossauro.com/02/ - 2023-08-02 + 2023-08-03 daily https://fastapidozero.dunossauro.com/03/ - 2023-08-02 + 2023-08-03 daily https://fastapidozero.dunossauro.com/04/ - 2023-08-02 + 2023-08-03 daily https://fastapidozero.dunossauro.com/05/ - 2023-08-02 + 2023-08-03 daily https://fastapidozero.dunossauro.com/06/ - 2023-08-02 + 2023-08-03 daily https://fastapidozero.dunossauro.com/07/ - 2023-08-02 + 2023-08-03 daily https://fastapidozero.dunossauro.com/08/ - 2023-08-02 + 2023-08-03 daily https://fastapidozero.dunossauro.com/09/ - 2023-08-02 + 2023-08-03 daily https://fastapidozero.dunossauro.com/10/ - 2023-08-02 + 2023-08-03 daily https://fastapidozero.dunossauro.com/11/ - 2023-08-02 + 2023-08-03 daily https://fastapidozero.dunossauro.com/12/ - 2023-08-02 + 2023-08-03 daily \ No newline at end of file diff --git a/sitemap.xml.gz b/sitemap.xml.gz index 6c5f78c8..1a5288a8 100644 Binary files a/sitemap.xml.gz and b/sitemap.xml.gz differ