diff --git a/09/index.html b/09/index.html index b766eebe..42df774b 100644 --- a/09/index.html +++ b/09/index.html @@ -1400,7 +1400,7 @@

Rodando as migrações de form - .env # Restante da configuração... -

Adotar essa abordagem evita a exposição das variáveis de ambiente no arquivo de configuração. Esta não foi a abordagem padrão no curso devido à complexidade adicional e à intenção de evitar confusões. Dependendo do ambiente estabelecido pela equipe de DevOps/SRE em um projeto real, essa gestão pode variar entre variáveis de ambiente, arquivos .env ou soluções mais avançadas como Vault.{:target="_blank"}.

+

Adotar essa abordagem evita a exposição das variáveis de ambiente no arquivo de configuração. Esta não foi a abordagem padrão no curso devido à complexidade adicional e à intenção de evitar confusões. Dependendo do ambiente estabelecido pela equipe de DevOps/SRE em um projeto real, essa gestão pode variar entre variáveis de ambiente, arquivos .env ou soluções mais avançadas como Vault.

Se optar por utilizar um arquivo .env com as configurações do PostgreSQL, configure o Pydantic para ignorar variáveis de ambiente que não são necessárias, adicionando extra='ignore' a chamada de SettingsConfigDic:

fast_zero/settings.py
 1
  2
diff --git a/12/index.html b/12/index.html
index bc18093c..d3b0b5f9 100644
--- a/12/index.html
+++ b/12/index.html
@@ -818,13 +818,13 @@ 

Revisão

Outros materiais produzidos por mim sobre FastAPI

Já cobrimos alguns temas não citados neste curso usando FastAPI em Algumas Lives de Python. Você pode assistir para aprender mais também.

Templates e WebSockets

-

Na Live de Python #164 conversamos sobre websockets com Python e usamos FastAPI para exemplificar o comportamento. Durante essa live criamos uma aplicação de chat e usamos os templates com Jinja2 e Brython.

+

Na Live de Python #164 conversamos sobre websockets com Python e usamos FastAPI para exemplificar o comportamento. Durante essa live criamos uma aplicação de chat e usamos os templates com Jinja2 e Brython.

GraphQL (Strawberry)

-

Na Live de Python #185 conversamos sobre GraphQL um padrão alternativo a REST APIs. Todos os exemplos foram aplicados usando Strawberry e FastAPI

+

Na Live de Python #185 conversamos sobre GraphQL um padrão alternativo a REST APIs. Todos os exemplos foram aplicados usando Strawberrye FastAPI

SQLModel

-

Na Live de Python #235 conversamos sobre SQLModel um ORM alternativo ao SQLAlchemy que se integra com o Pydantic. O SQLModel também foi desenvolvido pelo Sebastian (criador do FastAPI). Caminhando ao final dessa aula, podemos ver a implementação do SQLModel em uma aplicação básica com FastAPI.

+

Na Live de Python #235 conversamos sobre SQLModel um ORM alternativo ao SQLAlchemy que se integra com o Pydantic. O SQLModel também foi desenvolvido pelo Sebastian (criador do FastAPI). Caminhando ao final dessa aula, podemos ver a implementação do SQLModel em uma aplicação básica com FastAPI.

Próximos passos

[WIP]

@@ -838,7 +838,7 @@

Conclusão

Última atualização: - September 14, 2023 + November 27, 2023 diff --git a/index.html b/index.html index df0b07d7..49b2b9fa 100644 --- a/index.html +++ b/index.html @@ -1000,7 +1000,7 @@

Aulas

  • Despedida
  • 🦖 Quem vai ministrar essas aulas?

    -

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

    +

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

    Uma fotografia minha, Dunossauro, sentado em um banco com um por do sol ao fundo

    @@ -1068,7 +1068,7 @@

    F.A.Q.

    Última atualização: - October 24, 2023 + November 27, 2023 diff --git a/search/search_index.json b/search/search_index.json index dc9bfa4e..43174448 100644 --- a/search/search_index.json +++ b/search/search_index.json @@ -1 +1 @@ -{"config":{"lang":["pt"],"separator":"[\\s\\-]+","pipeline":["stopWordFilter"]},"docs":[{"location":"","title":"FastAPI do Zero!","text":""},{"location":"#fastapi-do-zero","title":"FastAPI do ZERO","text":"

    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! Problemas j\u00e1 conhecidos

    Construindo um Projeto com Bancos de Dados, Testes e Deploy

    Boas-vindas \u00e0 sua jornada de aprendizado com o framework FastAPI! Neste curso, o foco \u00e9 proporcionar um entendimento pr\u00e1tico das habilidades essenciais para o desenvolvimento eficiente de APIs. Exploraremos temas como integra\u00e7\u00e3o com bancos de dados e implementa\u00e7\u00e3o de testes, oferecendo uma base s\u00f3lida para quem busca trabalhar com essa ferramenta. A abordagem \u00e9 direta e informativa, visando nos equipar com o conhecimento necess\u00e1rio para come\u00e7ar a criar nossos pr\u00f3prios projetos.

    "},{"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 foi desenvolvido para oferecer uma experi\u00eancia pr\u00e1tica no uso do FastAPI, uma das ferramentas mais modernas para constru\u00e7\u00e3o de APIs. Ao longo do curso, o objetivo \u00e9 que voc\u00ea obtenha uma compreens\u00e3o das funcionalidades do FastAPI e de boas pr\u00e1ticas associadas a ele.

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

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

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

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

    Aqui est\u00e1 uma vis\u00e3o geral dos t\u00f3picos que 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 a abordagem deste curso \u00e9 pr\u00e1tica e centrada em um projeto concreto. Atrav\u00e9s de exemplos reais e instru\u00e7\u00f5es passo a passo, voc\u00ea ter\u00e1 a oportunidade de acompanhar o processo de constru\u00e7\u00e3o de uma aplica\u00e7\u00e3o real. Mesmo que os conceitos de desenvolvimento web sejam novos para voc\u00ea, a \u00eanfase na aplica\u00e7\u00e3o pr\u00e1tica e a estrutura detalhada do curso facilitar\u00e3o o entendimento e a aplica\u00e7\u00e3o dessas habilidades 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 h\u00e1 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":"

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

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

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

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

    Perguntas frequentes que me fizeram durante 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
      • Embora a vers\u00e3o 3.7 seja suficiente para executar o FastAPI em si, neste curso usaremos recursos de Python que est\u00e3o presentes apenas a partir da vers\u00e3o 3.11. Portanto, essa \u00e9 a vers\u00e3o m\u00ednima obrigat\u00f3ria para o curso.
    4. O Poetry para gerenciar os pacotes e seu ambiente virtual (caso n\u00e3o conhe\u00e7a o poetry temos uma live de python sobre ele)
    5. Git: Para gerenciar vers\u00f5es
    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)\n  3.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\n\napp = FastAPI()\n\n@app.get('/')\ndef read_root():\n    return {'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":"

    Come\u00e7ando pelo ruff, vamos definir o comprimento de linha para 79 caracteres (conforme sugerido na PEP 8) e em seguida, informaremos que o diret\u00f3rio de ambiente virtual e o de migra\u00e7\u00f5es de banco de dados dever\u00e3o ser ignorados:

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

    Para evitar conflitos de formata\u00e7\u00e3o entre o isort e o blue, definiremos o black como perfil de formata\u00e7\u00e3o a ser seguido, j\u00e1 que o blue \u00e9 um fork dele. Como o black utiliza 88 caracteres por linha, vamos alterar para 79 que \u00e9 o padr\u00e3o que o blue segue e que tamb\u00e9m estamos seguindo:

    pyproject.toml
    [tool.isort]\nprofile = \"black\"\nline_length = 79\nextend_skip = ['migrations']\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\n[tool.poetry.dependencies]\npython = \"3.11.*\"\nfastapi = \"^0.100.0\"\nuvicorn = \"^0.22.0\"\n\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\n[tool.ruff]\nline-length = 79\nexclude = ['.venv', 'migrations']\n\n[tool.isort]\nprofile = \"black\"\nline_length = 79\nextend_skip = ['migrations']\n\n[tool.pytest.ini_options]\npythonpath = \".\"\n\n[tool.blue]\nextend-exclude = '(migrations/)'\n\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\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 @@\n from fastapi import FastAPI\n\n app = 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\n\napp = FastAPI()\n\n\n@app.get('/')\ndef read_root():\n    return {'message': 'Ol\u00e1 Mundo!'}\n
    "},{"location":"01/#introducao-ao-pytest-testando-o-hello-world","title":"Introdu\u00e7\u00e3o ao Pytest: Testando o \"Hello, World!\"","text":"

    Antes de entendermos a din\u00e2mica dos testes, precisamos entender o efeito que eles t\u00eam no nosso c\u00f3digo. 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)\n    self._warn(\"No data was collected.\", slug=\"no-data-collected\")\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      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\n\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\n\ndef test_root_deve_retornar_200_e_ola_mundo():\n    client = TestClient(app)\n\n    response = client.get('/')\n\n    assert response.status_code == 200\n    assert 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\n\ndef test_root_deve_retornar_200_e_ola_mundo():\n    client = TestClient(app)  # Arrange\n\n    response = client.get('/')  # Act\n\n    assert response.status_code == 200  # Assert\n    assert response.json() == {'message': 'Ol\u00e1 Mundo!'}  # Assert\n

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

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

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

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

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

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

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

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

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

    Antes de concluirmos a aula, precisamos 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 uma requisi\u00e7\u00e3o, a processa 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 a processa. 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 com o 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.

    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.

    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.

    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\n\n\nclass UserSchema(BaseModel):\n    username: str\n    email: EmailStr\n    password: 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]

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

    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\n\n\nclass UserSchema(BaseModel):\n    username: str\n    email: EmailStr\n    password: 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\n# C\u00f3digo da nossa rota de ol\u00e1 mundo omitido\n\n@app.post('/users/', status_code=201)\ndef create_user(user: UserSchema):\n    return 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):\n    username: str\n    email: 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\n# c\u00f3digo omitido\n\n@app.post('/users/', status_code=201, response_model=UserPublic)\ndef create_user(user: UserSchema):\n    return 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\n# c\u00f3digo omitido\n\ndatabase = []  # provis\u00f3rio para estudo!\n\n\n@app.post('/users/', status_code=201, response_model=UserPublic)\ndef create_user(user: UserSchema):\n    user_with_id = UserDB(**user.model_dump(), id=len(database) + 1)\n\n    database.append(user_with_id)\n\n    return 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):\n    id: int\n    username: str\n    email: EmailStr\n\n\nclass UserDB(UserSchema):\n    id: 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\n# parte da resposta foi omitida\n\n---------- coverage: platform linux, python 3.11.3-final-0 -----------\nName                    Stmts   Miss  Cover\n-------------------------------------------\nfast_zero/__init__.py       0      0   100%\nfast_zero/app.py           12      3    75%\nfast_zero/schemas.py       11      0   100%\n-------------------------------------------\nTOTAL                      23      3    87%\n\n# parte da resposta foi omitida\n

    Vemos que temos 3 Miss. Possivelmente das linhas que acabamos de escrever.

    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():\n    client = TestClient(app)\n\n    response = client.post(\n        '/users/',\n        json={\n            'username': 'alice',\n            'email': 'alice@example.com',\n            'password': 'secret',\n        },\n    )\n    assert response.status_code == 201\n    assert response.json() == {\n        'username': 'alice',\n        'email': 'alice@example.com',\n        'id': 1,\n    }\n

    Ao executar o teste:

    $ Execu\u00e7\u00e3o no terminal!
    task test\n\n# parte da resposta foi omitida\n\ntests/test_app.py::test_root_deve_retornar_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
    "},{"location":"02/#nao-se-repita-dry","title":"N\u00e3o se repita (DRY)","text":"

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

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

    Se fixtures s\u00e3o uma novidade para voc\u00ea

    Existe uma live de Python onde discutimos especificamente sobre fixtures

    Link direto

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

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

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

    tests/test_app.py
    # ...\n\ndef test_root_deve_retornar_200_e_ola_mundo(client):\n    response = client.get('/')\n    # ...\n\ndef test_create_user(client):\n    response = client.post(\n        '/users/',\n        json={\n            'username': 'alice',\n            'email': 'alice@example.com',\n            'password': 'secret',\n        },\n    )\n    # ...\n

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

    Vemos que estamos no caminho certo. Agora que a rota POST est\u00e1 implementada, 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):\n    users: list[UserPublic]\n

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

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

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

    "},{"location":"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(client):\n    response = client.get('/users/')\n    assert response.status_code == 200\n    assert response.json() == {\n        'users': [\n            {\n                'username': 'alice',\n                'email': 'alice@example.com',\n                'id': 1,\n            }\n        ]\n    }\n

    Com as rotas POST e GET implementadas, agora podemos criar e recuperar usu\u00e1rios. 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# ...\n\n@app.put('/users/{user_id}', response_model=UserPublic)\ndef update_user(user_id: int, user: UserSchema):\n    if user_id > len(database) or user_id < 1:\n        raise HTTPException(status_code=404, detail='User not found')\n\n    user_with_id = UserDB(**user.model_dump(), id=user_id)\n    database[user_id - 1] = user_with_id\n\n    return 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(client):\n    response = client.put(\n        '/users/1',\n        json={\n            'username': 'bob',\n            'email': 'bob@example.com',\n            'password': 'mynewpassword',\n        },\n    )\n    assert response.status_code == 200\n    assert response.json() == {\n        'username': 'bob',\n        'email': 'bob@example.com',\n        'id': 1,\n    }\n

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

    "},{"location":"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):\n    detail: 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# ...\n\n@app.delete('/users/{user_id}', response_model=Message)\ndef delete_user(user_id: int):\n    if user_id > len(database) or user_id < 1:\n        raise HTTPException(status_code=404, detail='User not found')\n\n    del database[user_id - 1]\n\n    return {'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(client):\n    response = client.delete('/users/1')\n\n    assert response.status_code == 200\n    assert response.json() == {'detail': 'User deleted'}\n
    "},{"location":"02/#checando-tudo-antes-do-commit","title":"Checando tudo antes do commit","text":"

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

    $ Execu\u00e7\u00e3o no terminal!
    $ task lint\nAll done! \u2728 \ud83c\udf70 \u2728\n5 files would be left unchanged.\n\n$ task format\nAll done! \u2728 \ud83c\udf70 \u2728\n5 files left unchanged.\nSkipped 1 files\n\n$ 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\n\n---------- coverage: platform linux, python 3.11.4-final-0 -----------\nName                   Stmts   Miss  Cover\n------------------------------------------\nfastzero/__init__.py       0      0   100%\nfastzero/app.py           28      2    93%\nfastzero/schemas.py       15      0   100%\n------------------------------------------\nTOTAL                     43      2    95%\n\n\n============================================ 5 passed in 1.48s =============================================\nWrote HTML report to htmlcov/index.html\n
    "},{"location":"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-os-12-fatores","title":"Configura\u00e7\u00f5es de ambiente e os 12 fatores","text":"

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

    Caso queira saber mais sobre 12 fatores

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

    Link direto

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

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

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

    Agora que entendemos melhor esses conceitos, 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\n\n\nclass Base(DeclarativeBase):\n    pass\n\n\nclass User(Base):\n    __tablename__ = 'users'\n\n    id: Mapped[int] = mapped_column(primary_key=True)\n    username: Mapped[str]\n    password: Mapped[str]\n    email: 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 entre eles.

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

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

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

    "},{"location":"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]

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

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

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

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

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

    tests/conftest.py
    # ...\nimport pytest\nfrom sqlalchemy import create_engine\nfrom sqlalchemy.orm import sessionmaker\n\nfrom fast_zero.models import Base\n\n# ...\n\n\n@pytest.fixture\ndef session():\n    engine = create_engine('sqlite:///:memory:')\n    Session = sessionmaker(bind=engine)\n    Base.metadata.create_all(engine)\n    yield Session()\n    Base.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:'): cria um mecanismo de banco de dados SQLite em mem\u00f3ria usando SQLAlchemy. Este mecanismo ser\u00e1 usado para criar uma sess\u00e3o de banco de dados para nossos testes.

    2. Session = sessionmaker(bind=engine): cria uma f\u00e1brica de sess\u00f5es para criar sess\u00f5es de banco de dados para nossos testes.

    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\n\nfrom fast_zero.models import User\n\n\ndef test_create_user(session):\n    new_user = User(username='alice', password='secret', email='teste@test')\n    session.add(new_user)\n    session.commit()\n\n    user = session.scalar(select(User).where(User.username == 'alice'))\n\n    assert 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
    # ...\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\n\n\nclass Settings(BaseSettings):\n    model_config = SettingsConfigDict(\n        env_file='.env', env_file_encoding='utf-8'\n    )\n\n    DATABASE_URL: str\n

    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  \u251c\u2500\u2500 schemas.py\n\u2502  \u2514\u2500\u2500 settings.py\n\u251c\u2500\u2500 migrations\n\u2502  \u251c\u2500\u2500 env.py\n\u2502  \u251c\u2500\u2500 README\n\u2502  \u251c\u2500\u2500 script.py.mako\n\u2502  \u2514\u2500\u2500 versions\n\u251c\u2500\u2500 poetry.lock\n\u251c\u2500\u2500 pyproject.toml\n\u251c\u2500\u2500 README.md\n\u2514\u2500\u2500 tests\n   \u251c\u2500\u2500 __init__.py\n   \u251c\u2500\u2500 conftest.py\n   \u251c\u2500\u2500 test_app.py\n   \u2514\u2500\u2500 test_db.py\n

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

    "},{"location":"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
    # ...\nfrom alembic import context\nfrom fast_zero.settings import Settings\nfrom fast_zero.models import Base\n\nconfig = context.config\nconfig.set_main_option('sqlalchemy.url', Settings().DATABASE_URL)\n\nif config.config_file_name is not None:\n    fileConfig(config.config_file_name)\n\ntarget_metadata = Base.metadata\n\n# ...\n

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

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

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

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

    "},{"location":"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\n\nRevision ID: e018397cecf4\nRevises:\nCreate Date: 2023-07-13 03:43:03.730534\n\n\"\"\"\nfrom alembic import op\nimport sqlalchemy as sa\n\n\n# revision identifiers, used by Alembic.\nrevision = 'e018397cecf4'\ndown_revision = None\nbranch_labels = None\ndepends_on = None\n\n\ndef upgrade() -> None:\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.create_table('users',\n    sa.Column('id', sa.Integer(), nullable=False),\n    sa.Column('username', sa.String(), nullable=False),\n    sa.Column('password', sa.String(), nullable=False),\n    sa.Column('email', sa.String(), nullable=False),\n    sa.PrimaryKeyConstraint('id')\n    )\n    # ### end Alembic commands ###\n\n\ndef downgrade() -> None:\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.drop_table('users')\n    # ### end Alembic commands ###\n

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

    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.

    Caso n\u00e3o tenha o SQLite instalado na sua m\u00e1quina: Arch
    pacman -S sqlite\n
    Debian/Ubuntu
    sudo apt install sqlite\n
    Mac
    brew install sqlite\n
    • Para usu\u00e1rios Windows, ver o endere\u00e7o SQLite Downloads
    $ Execu\u00e7\u00e3o no terminal!
    sqlite3 database.db\n
    SQLite version 3.42.0 2023-05-16 12:36:15\nEnter \".help\" for usage hints.\nsqlite> .schema\nCREATE TABLE alembic_version (\n    version_num VARCHAR(32) NOT NULL,\n    CONSTRAINT alembic_version_pkc PRIMARY KEY (version_num)\n);\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\n
    INFO  [alembic.runtime.migration] Context impl SQLiteImpl.\nINFO  [alembic.runtime.migration] Will assume non-transactional DDL.\nINFO  [alembic.runtime.migration] Running upgrade  -> e018397cecf4, create users table\n

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

    $ Execu\u00e7\u00e3o no terminal!
    sqlite3 database.db\n
    SQLite version 3.42.0 2023-05-16 12:36:15\nEnter \".help\" for usage hints.\nsqlite> .schema\nCREATE TABLE alembic_version (\n    version_num VARCHAR(32) NOT NULL,\n    CONSTRAINT alembic_version_pkc PRIMARY KEY (version_num)\n);\nCREATE TABLE users (\n    id INTEGER NOT NULL,\n    username VARCHAR NOT NULL,\n    password VARCHAR NOT NULL,\n    email VARCHAR NOT NULL,\n    PRIMARY 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 pessoas, 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\n\nfrom fast_zero.settings import Settings\n\nengine = create_engine(Settings().DATABASE_URL)\n\n\ndef get_session():\n    with Session(engine) as session:\n        yield 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\n\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# ...\n\n\n@app.post('/users/', response_model=UserPublic, status_code=201)\ndef create_user(user: UserSchema, session: Session = Depends(get_session)):\n    db_user = session.scalar(\n        select(User).where(User.username == user.username)\n    )\n\n    if db_user:\n        raise HTTPException(\n            status_code=400, detail='Username already registered'\n        )\n\n    db_user = User(\n        username=user.username, password=user.password, email=user.email\n    )\n    session.add(db_user)\n    session.commit()\n    session.refresh(db_user)\n\n    return db_user\n

    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.

    "},{"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 alterar 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\n\nfrom fast_zero.app import app\nfrom fast_zero.database import get_session\n\n# ...\n\n@pytest.fixture\ndef client(session):\n    def get_session_override():\n        return session\n\n    with TestClient(app) as client:\n        app.dependency_overrides[get_session] = get_session_override\n        yield client\n\n    app.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):\n    response = client.post(\n        '/users',\n        json={\n            'username': 'alice',\n            'email': 'alice@example.com',\n            'password': 'secret',\n        },\n    )\n    assert response.status_code == 201\n    assert 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\n# ...\n\ntests/test_app.py::test_root_deve_retornar_200_e_ola_mundo PASSED\ntests/test_app.py::test_create_user FAILED\n

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

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

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

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

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

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

    Assim, nossa fixture deve ficar dessa forma:

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

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

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

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

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

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

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

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

    • offset permite pular um n\u00famero espec\u00edfico de registros antes de come\u00e7ar a buscar, o que \u00e9 \u00fatil para implementar a navega\u00e7\u00e3o por p\u00e1ginas.
    • limit define o n\u00famero m\u00e1ximo de registros a serem retornados, permitindo que voc\u00ea controle a quantidade de dados enviados em cada resposta.

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

    "},{"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):\n    response = client.get('/users')\n    assert response.status_code == 200\n    assert response.json() == {'users': []}\n

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

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

    Por\u00e9m, \u00e9 claro, queremos tamb\u00e9m testar o caso em que existem usu\u00e1rios no banco. Para isso, 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# ...\n\n@pytest.fixture\ndef user(session):\n    user = User(username='Teste', email='teste@test.com', password='testtest')\n    session.add(user)\n    session.commit()\n    session.refresh(user)\n\n    return user\n

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

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

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

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

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

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

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

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

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

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

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

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

    $ Execu\u00e7\u00e3o no terminal!
    task test\n\n# ...\n\ntests/test_app.py::test_root_deve_retornar_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 de PUT para suportar o banco de dados, como fizemos com os endpoints POST e GET:

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

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

    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 PUT era a \u00fanica que estava utilizando UserDB, e agora que modificamos esta rota, podemos remover UserDB dos nossos e tamb\u00e9m excluir sua defini\u00e7\u00e3o no arquivo schemas.py

    Sobre o arquivo schemas.py

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

    schemas.py
    from pydantic import BaseModel, EmailStr\n\n\nclass UserSchema(BaseModel):\n    username: str\n    email: EmailStr\n    password: str\n\n\nclass UserPublic(BaseModel):\n    id: int\n    username: str\n    email: EmailStr\n\n\nclass UserList(BaseModel):\n    users: list[UserPublic]\n\n\nclass Message(BaseModel):\n    detail: str\n
    "},{"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):\n    response = client.put(\n        '/users/1',\n        json={\n            'username': 'bob',\n            'email': 'bob@example.com',\n            'password': 'mynewpassword',\n        },\n    )\n    assert response.status_code == 200\n    assert 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)):\n    db_user = session.scalar(select(User).where(User.id == user_id))\n\n    if db_user is None:\n        raise HTTPException(status_code=404, detail='User not found')\n\n    session.delete(db_user)\n    session.commit()\n\n    return {'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):\n    response = client.delete('/users/1')\n    assert response.status_code == 200\n    assert 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\n\nfrom jose import jwt\nfrom passlib.context import CryptContext\n\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')\n\n\ndef create_access_token(data: dict):\n    to_encode = data.copy()\n    expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)\n    to_encode.update({'exp': expire})\n    encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)\n    return encoded_jwt\n

    A fun\u00e7\u00e3o create_access_token \u00e9 respons\u00e1vel por criar um novo token JWT que ser\u00e1 usado para autenticar o usu\u00e1rio. Ela recebe um dicion\u00e1rio de dados, adiciona um tempo de expira\u00e7\u00e3o ao token (baseado na constante ACCESS_TOKEN_EXPIRE_MINUTES). Esses dados, em conjunto formam o 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\n\nfrom fast_zero.security import create_access_token, SECRET_KEY\n\n\ndef test_jwt():\n    data = {'test': 'test'}\n    token = create_access_token(data)\n\n    decoded = jwt.decode(token, SECRET_KEY, algorithms=['HS256'])\n\n    assert decoded['test'] == data['test']\n    assert decoded['exp']  # Testa se o valor de exp foi adicionado ao token\n

    Na pr\u00f3xima se\u00e7\u00e3o, 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):\n    return pwd_context.hash(password)\n\n\ndef verify_password(plain_password: str, hashed_password: str):\n    return pwd_context.verify(plain_password, hashed_password)\n

    A fun\u00e7\u00e3o get_password_hash recebe uma senha em texto puro como argumento e retorna uma vers\u00e3o criptografada dessa senha. A fun\u00e7\u00e3o verify_password recebe uma senha em texto puro e uma senha criptografada como argumentos, e verifica se a senha em texto puro, quando criptografada, corresponde \u00e0 senha criptografada. Ambas as fun\u00e7\u00f5es utilizam o objeto pwd_context, que definimos anteriormente usando a biblioteca 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-o-endpoint-de-post-para-encriptar-a-senha","title":"Modificando o endpoint de POST para encriptar a senha","text":"

    Com as fun\u00e7\u00f5es de cria\u00e7\u00e3o de hash de senha e verifica\u00e7\u00e3o de senha em vigor, agora podemos atualizar nossos endpoints para usar essa nova funcionalidade de encripta\u00e7\u00e3o.

    Primeiro, vamos modificar a fun\u00e7\u00e3o create_user para criar um hash da senha antes de armazen\u00e1-la no banco de dados. Para fazer isso precisamos importar a fun\u00e7\u00e3o de gera\u00e7\u00e3o de hash get_password_hash e no momento da cria\u00e7\u00e3o do registro na tabela a senha deve ser passada com o hash gerado:

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

    Desta forma, a senha n\u00e3o ser\u00e1 mais criada em texto plano no objeto User. Fazendo com que caso exista algum problema relacionado a vazamento de dados, as senhas das pessoas nunca sejam expostas.

    "},{"location":"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\n# ...\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/#modificando-o-endpoint-de-atualizacao-de-usuarios","title":"Modificando o endpoint de atualiza\u00e7\u00e3o de usu\u00e1rios","text":"

    \u00c9 igualmente importante modificar a fun\u00e7\u00e3o update_user para tamb\u00e9m criar um hash da senha antes de atualizar User no banco de dados. Caso contr\u00e1rio, a senha em texto puro seria armazenada no banco de dados no momento da atualiza\u00e7\u00e3o.

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

    Assim, a atualiza\u00e7\u00e3o de um User, via m\u00e9todo PUT, tamb\u00e9m criar\u00e1 o hash da senha no momento da atualiza\u00e7\u00e3o. Pois, nesse caso em espec\u00edfico, existe a possibilidade de alterar qualquer coluna da tabela, inclusive o campo password.

    "},{"location":"05/#sobre-os-testes-da-put-usersuser_id","title":"Sobre os testes da PUT /users/{user_id}","text":"

    Assim como no teste da rota de cria\u00e7\u00e3o, os testes tamb\u00e9m passam normalmente por n\u00e3o validarem o campo password.

    $ Execu\u00e7\u00e3o no terminal!
    task test\n\n# ...\n\ntests/test_app.py::test_root_deve_retornar_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\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):\n    access_token: str\n    token_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 (\n    create_access_token,\n    get_password_hash,\n    verify_password,\n)\n\n# ...\n\n@app.post('/token', response_model=Token)\ndef login_for_access_token(\n    form_data: OAuth2PasswordRequestForm = Depends(),\n    session: Session = Depends(get_session),\n):\n    user = session.scalar(select(User).where(User.email == form_data.username))\n\n    if not user:\n        raise HTTPException(\n            status_code=400, detail='Incorrect email or password'\n        )\n\n    if not verify_password(form_data.password, user.password):\n        raise HTTPException(\n            status_code=400, detail='Incorrect email or password'\n        )\n\n    access_token = create_access_token(data={'sub': user.email})\n\n    return {'access_token': access_token, 'token_type': 'bearer'}\n

    Esse endpoint recebe os dados do formul\u00e1rio atrav\u00e9s do form_data (que s\u00e3o injetados automaticamente gra\u00e7as ao Depends()) e tenta recuperar um usu\u00e1rio com o email fornecido. Se o usu\u00e1rio n\u00e3o for encontrado ou a senha n\u00e3o corresponder ao hash armazenado no banco de dados, uma exce\u00e7\u00e3o \u00e9 lan\u00e7ada. Caso contr\u00e1rio, um token de acesso \u00e9 criado usando o create_access_token() que criamos anteriormente e retornado como uma resposta.

    "},{"location":"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):\n    response = client.post(\n        '/token',\n        data={'username': user.email, 'password': user.password},\n    )\n    token = response.json()\n\n    assert response.status_code == 200\n    assert 'access_token' in token\n    assert 'token_type' in token\n

    Nesse teste, n\u00f3s enviamos uma requisi\u00e7\u00e3o POST para o endpoint \"/token\" com um username e uma senha v\u00e1lidos. Ent\u00e3o, n\u00f3s verificamos que a resposta cont\u00e9m um \"access_token\" e um \"token_type\", que s\u00e3o os campos que esperamos de um JWT v\u00e1lido.

    No entanto, h\u00e1 um problema. Agora que a senha est\u00e1 sendo criptografada, nosso teste falhar\u00e1:

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

    Para corrigir isso, precisamos garantir que a senha esteja sendo criptografada na fixture antes de ser salva:

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

    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\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):\n    password = 'testtest'\n    user = User(\n        username='Teste',\n        email='teste@test.com',\n        password=get_password_hash(password),\n    )\n    session.add(user)\n    session.commit()\n    session.refresh(user)\n\n    user.clean_password = 'testtest'\n\n    return user\n

    Monkey patching \u00e9 uma t\u00e9cnica em que modificamos ou estendemos o c\u00f3digo em tempo de execu\u00e7\u00e3o. Neste caso, estamos adicionando um novo atributo clean_password ao objeto user para armazenar a senha em texto puro.

    Agora, podemos alterar o teste para usar clean_password:

    tests/test_app.py
    def test_get_token(client, user):\n    response = client.post(\n        '/token',\n        data={'username': user.email, 'password': user.clean_password},\n    )\n    token = response.json()\n\n    assert response.status_code == 200\n    assert 'access_token' in token\n    assert 'token_type' in token\n

    E agora todos os testes devem passar normalmente:

    $ Execu\u00e7\u00e3o no terminal!
    task test\n\n# ...\n\ntests/test_app.py::test_root_deve_retornar_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):\n    username: str | None = None\n

    Nesse ponto, criaremos uma a fun\u00e7\u00e3o get_current_user que ser\u00e1 respons\u00e1vel por extrair o token JWT do header Authorization da requisi\u00e7\u00e3o, decodificar esse token, extrair as informa\u00e7\u00f5es do usu\u00e1rio e 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\n\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\n\nfrom fast_zero.database import get_session\nfrom fast_zero.models import User\nfrom fast_zero.schemas import TokenData\n\n# ...\n\noauth2_scheme = OAuth2PasswordBearer(tokenUrl=\"token\")\n\nasync def get_current_user(\n    session: Session = Depends(get_session),\n    token: str = Depends(oauth2_scheme),\n):\n    credentials_exception = HTTPException(\n        status_code=status.HTTP_401_UNAUTHORIZED,\n        detail='Could not validate credentials',\n        headers={'WWW-Authenticate': 'Bearer'},\n    )\n\n    try:\n        payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])\n        username: str = payload.get('sub')\n        if not username:\n            raise credentials_exception\n        token_data = TokenData(username=username)\n    except JWTError:\n        raise credentials_exception\n\n    user = session.scalar(\n        select(User).where(User.email == token_data.username)\n    )\n\n    if user is None:\n        raise credentials_exception\n\n    return 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 (\n    create_access_token,\n    get_current_user,\n    get_password_hash,\n    verify_password,\n)\n\n# ...\n\n@app.put('/users/{user_id}', response_model=UserPublic)\ndef update_user(\n    user_id: int,\n    user: UserSchema,\n    session: Session = Depends(get_session),\n    current_user: User = Depends(get_current_user),\n):\n    if current_user.id != user_id:\n        raise HTTPException(status_code=400, detail='Not enough permissions')\n\n    current_user.username = user.username\n    current_user.password = user.password\n    current_user.email = user.email\n    session.commit()\n    session.refresh(current_user)\n\n    return current_user\n

    Com isso, podemos remover a query feita no endpoint para encontrar o User, pois ela j\u00e1 est\u00e1 sendo feita no get_current_user, simplificando ainda mais nosso endpoint.

    Agora, 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(\n    user_id: int,\n    session: Session = Depends(get_session),\n    current_user: User = Depends(get_current_user),\n):\n    if current_user.id != user_id:\n        raise HTTPException(status_code=400, detail='Not enough permissions')\n\n    session.delete(current_user)\n    session.commit()\n\n    return {'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):\n    response = client.post(\n        '/token',\n        data={'username': user.email, 'password': user.clean_password},\n    )\n    return response.json()['access_token']\n

    Agora, podemos atualizar os testes para o endpoint PUT e DELETE para incluir a autentica\u00e7\u00e3o.

    tests/test_app.py
    def test_update_user(client, user, token):\n    response = client.put(\n        f'/users/{user.id}',\n        headers={'Authorization': f'Bearer {token}'},\n        json={\n            'username': 'bob',\n            'email': 'bob@example.com',\n            'password': 'mynewpassword',\n        },\n    )\n    assert response.status_code == 200\n    assert response.json() == {\n        'username': 'bob',\n        'email': 'bob@example.com',\n        'id': 1,\n    }\n\n\ndef test_delete_user(client, user, token):\n    response = client.delete(\n        f'/users/{user.id}',\n        headers={'Authorization': f'Bearer {token}'},\n    )\n    assert response.status_code == 200\n    assert 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\n# ...\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/security.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 rotas 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 oferece uma ferramenta poderosa conhecida como routers, que facilita a organiza\u00e7\u00e3o e agrupamento de diferentes rotas em uma aplica\u00e7\u00e3o. Pense em um router como um \"subaplicativo\" do FastAPI que pode ser integrado em uma aplica\u00e7\u00e3o principal. Isso n\u00e3o s\u00f3 mant\u00e9m o c\u00f3digo organizado e leg\u00edvel, mas tamb\u00e9m se mostra especialmente \u00fatil \u00e0 medida que a aplica\u00e7\u00e3o se expande e novas rotas s\u00e3o adicionadas.

    Esse tipo de organiza\u00e7\u00e3o nos oferece diversos benef\u00edcios:

    1. Organiza\u00e7\u00e3o e Legibilidade: Routers ajudam a manter o c\u00f3digo organizado e leg\u00edvel, o que \u00e9 crucial \u00e0 medida que a aplica\u00e7\u00e3o se expande.
    2. Separa\u00e7\u00e3o de Preocupa\u00e7\u00f5es: Alinhado ao princ\u00edpio de SoC, os routers facilitam o entendimento e teste do c\u00f3digo.
    3. Escalabilidade: A estrutura\u00e7\u00e3o com routers permite adicionar novas rotas e funcionalidades de maneira eficiente conforme o projeto cresce.
    "},{"location":"06/#estruturacao-inicial","title":"Estrutura\u00e7\u00e3o Inicial","text":"

    Vamos iniciar criando uma nova estrutura de diret\u00f3rios chamada routes dentro do seu projeto fast_zero. Aqui, teremos subaplicativos dedicados a fun\u00e7\u00f5es espec\u00edficas, como gerenciamento de usu\u00e1rios e autentica\u00e7\u00e3o.

    \u251c\u2500\u2500 fast_zero\n\u2502  \u251c\u2500\u2500 app.py\n\u2502  \u251c\u2500\u2500 database.py\n\u2502  \u251c\u2500\u2500 models.py\n\u2502  \u251c\u2500\u2500 routes\n\u2502  \u2502  \u251c\u2500\u2500 auth.py\n\u2502  \u2502  \u2514\u2500\u2500 users.py\n

    Esta organiza\u00e7\u00e3o facilita a expans\u00e3o do seu projeto e a manuten\u00e7\u00e3o de uma estrutura clara.

    "},{"location":"06/#implementando-um-router-para-usuarios","title":"Implementando um Router para Usu\u00e1rios","text":"

    No arquivo fast_zero/routes/users.py, vamos importar APIRouter do FastAPI, a ferramenta chave para criar nosso subaplicativo. O par\u00e2metro prefix que passamos ajuda a agrupar todos os endpoints relacionados aos usu\u00e1rios sob um mesmo teto.

    fast_zero/routes/users.py
    from fastapi import APIRouter\n\nrouter = APIRouter(prefix='/users', tags=['users'])\n

    Com essa simples configura\u00e7\u00e3o, estamos prontos para definir rotas espec\u00edficas para usu\u00e1rios neste router, em vez de sobrecarregar o aplicativo principal. Utilizamos @router ao inv\u00e9s de @app para definir estas rotas. O uso da tag 'users' contribui para a organiza\u00e7\u00e3o e documenta\u00e7\u00e3o autom\u00e1tica no swagger.

    Desta forma podemos migrar todos os nossos imports e nossas fun\u00e7\u00f5es de endpoints para o arquivo fast_zero/routes/users.py e os removendo de fast_zero/app.py. Fazendo com que todos esse endpoints estejam no mesmo contexto e isolados da aplica\u00e7\u00e3o principal:

    fast_zero/routes/users.py
    from fastapi import APIRouter, Depends, HTTPException\nfrom sqlalchemy import select\nfrom sqlalchemy.orm import Session\n\nfrom fast_zero.database import get_session\nfrom fast_zero.models import User\nfrom fast_zero.schemas import Message, UserList, UserPublic, UserSchema\nfrom fast_zero.security import (\n    get_current_user,\n    get_password_hash,\n)\n\nrouter = APIRouter(prefix='/users', tags=['users'])\n\n@router.post('/', response_model=UserPublic, status_code=201)\n# ...\n@router.get('/', response_model=UserList)\n# ...\n@router.put('/{user_id}', response_model=UserPublic)\n# ...\n@router.delete('/{user_id}', response_model=Message)\n# ...\n

    Com o prefixo definido no router, os paths dos endpoints se tornam mais simples e diretos. Ao inv\u00e9s de '/users/{user_id}', por exemplo, usamos apenas '/{user_id}'.

    Exemplo do arquivo fast_zero/routes/users.py completo fast_zero/routes/users.py
    from fastapi import APIRouter, Depends, HTTPException\nfrom sqlalchemy import select\nfrom sqlalchemy.orm import Session\n\nfrom fast_zero.database import get_session\nfrom fast_zero.models import User\nfrom fast_zero.schemas import Message, UserList, UserPublic, UserSchema\nfrom fast_zero.security import (\n    get_current_user,\n    get_password_hash,\n)\n\nrouter = APIRouter(prefix='/users', tags=['users'])\n\n\n@router.post('/', response_model=UserPublic, status_code=201)\ndef create_user(user: UserSchema, session: Session = Depends(get_session)):\n    db_user = session.scalar(select(User).where(User.email == user.email))\n    if db_user:\n        raise HTTPException(status_code=400, detail='Email already registered')\n\n    hashed_password = get_password_hash(user.password)\n\n    db_user = User(\n        email=user.email,\n        username=user.username,\n        password=hashed_password,\n    )\n    session.add(db_user)\n    session.commit()\n    session.refresh(db_user)\n    return db_user\n\n\n@router.get('/', response_model=UserList)\ndef read_users(\n    skip: int = 0, limit: int = 100, session: Session = Depends(get_session)\n):\n    users = session.scalars(select(User).offset(skip).limit(limit)).all()\n    return {'users': users}\n\n\n@router.put('/{user_id}', response_model=UserPublic)\ndef update_user(\n    user_id: int,\n    user: UserSchema,\n    session: Session = Depends(get_session),\n    current_user: User = Depends(get_current_user),\n):\n    if current_user.id != user_id:\n        raise HTTPException(status_code=400, detail='Not enough permissions')\n\n    current_user.username = user.username\n    current_user.password = get_password_hash(user.password)\n    current_user.email = user.email\n    session.commit()\n    session.refresh(current_user)\n\n    return current_user\n\n\n@router.delete('/{user_id}', response_model=Message)\ndef delete_user(\n    user_id: int,\n    session: Session = Depends(get_session),\n    current_user: User = Depends(get_current_user),\n):\n    if current_user.id != user_id:\n        raise HTTPException(status_code=400, detail='Not enough permissions')\n\n    session.delete(current_user)\n    session.commit()\n\n    return {'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\n\nfrom fast_zero.database import get_session\nfrom fast_zero.models import User\nfrom fast_zero.schemas import Token\nfrom fast_zero.security import create_access_token, verify_password\n\nrouter = APIRouter(prefix='/auth', tags=['auth'])\n\n\n@router.post('/token', response_model=Token)\ndef login_for_access_token(\n    form_data: OAuth2PasswordRequestForm = Depends(),\n    session: Session = Depends(get_session),\n):\n    user = session.scalar(select(User).where(User.email == form_data.username))\n\n    if not user:\n        raise HTTPException(\n            status_code=400, detail='Incorrect email or password'\n        )\n\n    if not verify_password(form_data.password, user.password):\n        raise HTTPException(\n            status_code=400, detail='Incorrect email or password'\n        )\n\n    access_token = create_access_token(data={'sub': user.email})\n\n    return {'access_token': access_token, 'token_type': 'bearer'}\n

    Neste bloco de c\u00f3digo, n\u00f3s criamos um novo router que lidar\u00e1 exclusivamente com a rota de obten\u00e7\u00e3o de token (/token). O endpoint login_for_access_token \u00e9 definido exatamente da mesma maneira que antes, mas agora como parte deste router de autentica\u00e7\u00e3o.

    "},{"location":"06/#alteracao-da-validacao-de-token","title":"Altera\u00e7\u00e3o da valida\u00e7\u00e3o de token","text":"

    \u00c9 crucial abordar um aspecto relacionado \u00e0 modifica\u00e7\u00e3o do router: o uso do par\u00e2metro prefix. Ao introduzir o prefixo, o endere\u00e7o do endpoint /token, respons\u00e1vel pela valida\u00e7\u00e3o do bearer token JWT, \u00e9 alterado para /auth/token. Esse caminho est\u00e1 explicitamente definido no OAuth2PasswordBearer dentro de security.py, resultando em uma refer\u00eancia ao caminho antigo /token, anterior \u00e0 cria\u00e7\u00e3o do router.

    Esse problema fica evidente ao clicar no bot\u00e3o Authorize no Swagger:

    Percebe-se que o caminho para a autoriza\u00e7\u00e3o est\u00e1 incorreto. Como consequ\u00eancia, ao tentar autenticar atrav\u00e9s do Swagger, nos deparamos com um erro na interface:

    No entanto, o erro n\u00e3o \u00e9 suficientemente descritivo para identificarmos a origem do problema, retornando apenas uma mensagem gen\u00e9rica de Auth Error. Para compreender melhor o que ocorreu, \u00e9 necess\u00e1rio verificar o log produzido pelo uvicorn no terminal:

    Erro mostrado no terminal
    task serve\n# ...\nINFO:     127.0.0.1:40132 - \"POST /token HTTP/1.1\" 404 Not Found\n

    A solu\u00e7\u00e3o para este problema \u00e9 relativamente simples. Precisamos ajustar o par\u00e2metro tokenUrl na OAuth2PasswordBearer para refletir as mudan\u00e7as feitas no router, direcionando para /auth/token. Faremos isso no arquivo security.py:

    security.py
    oauth2_scheme = OAuth2PasswordBearer(tokenUrl='auth/token')\n

    Ap\u00f3s essa altera\u00e7\u00e3o, ao utilizar o Swagger, a autoriza\u00e7\u00e3o ser\u00e1 direcionada corretamente para o endpoint apropriado.

    "},{"location":"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\n\nfrom fast_zero.routes import auth, users\n\napp = FastAPI()\n\napp.include_router(users.router)\napp.include_router(auth.router)\n\n\n@app.get('/')\ndef read_root():\n    return {'message': 'Ol\u00e1 Mundo!'}\n

    Como voc\u00ea pode ver, nosso arquivo app.py \u00e9 muito mais simples agora. Ele agora delega as rotas para os respectivos routers, mantendo o foco em iniciar nossa aplica\u00e7\u00e3o FastAPI.

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

    Para acompanhar a nova estrutura routers, podemos desacoplar os testes do m\u00f3dulo test/test_app.py e criar arquivos de teste espec\u00edficos para cada um dos dom\u00ednios:

    • /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):\n    response = client.post(\n        '/auth/token',\n        data={'username': user.email, 'password': user.clean_password},\n    )\n    token = response.json()\n\n    assert response.status_code == 200\n    assert 'access_token' in token\n    assert 'token_type' in token\n

    \u00c9 importante notar que com a cria\u00e7\u00e3o do router usando prefix='/auth' devemos alterar o endpoint onde o request \u00e9 feito de '/token' para '/auth/token'. Fazendo com que a requisi\u00e7\u00e3o seja encaminhada para o lugar certo.

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

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

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

    Para a constru\u00e7\u00e3o desse arquivo, nenhum teste foi modificado. Eles foram somente movidos para o dom\u00ednio espec\u00edfico do router. Importante, por\u00e9m, notar que alguns destes testes usam a fixture token para checar a autoriza\u00e7\u00e3o, como o endpoint do token foi alterado, devemos alterar a fixture de token para que esses testes continuem passando.

    "},{"location":"06/#ajustando-a-fixture-de-token","title":"Ajustando a fixture de token","text":"

    A altera\u00e7\u00e3o da fixture de token \u00e9 igual que fizemos em /tests/test_auth.py, precisamos somente corrigir o novo endere\u00e7o do router no arquivo /tests/conftest.py:

    /tests/conftest.py
    @pytest.fixture\ndef token(client, user):\n    response = client.post(\n        '/auth/token',\n        data={'username': user.email, 'password': user.clean_password},\n    )\n    return response.json()['access_token']\n

    Fazendo assim com que os testes que dependem dessa fixture passem a funcionar.

    "},{"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\n# ...\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\n\nSession = Annotated[Session, Depends(get_session)]\nCurrentUser = Annotated[User, Depends(get_current_user)]\n

    Desse modo, conseguimos refinar a defini\u00e7\u00e3o dos endpoints para que se tornem mais concisos, sem alterar seu funcionamento:

    fast_zero/routes/users.py
    @router.post('/', response_model=UserPublic, status_code=201)\ndef create_user(user: UserSchema, session: Session):\n# ...\n\n@router.get('/', response_model=UserList)\ndef read_users(session: Session, skip: int = 0, limit: int = 100):\n# ...\n\n@router.put('/{user_id}', response_model=UserPublic)\ndef update_user(\n    user_id: int,\n    user: UserSchema,\n    session: Session,\n    current_user: CurrentUser\n):\n# ...\n\n@router.delete('/{user_id}', response_model=Message)\ndef delete_user(user_id: int, session: Session, current_user: CurrentUser):\n# ...\n

    Da mesma forma, podemos otimizar o roteador de autentica\u00e7\u00e3o:

    fast_zero/routers/auth.py
    from typing import Annotated\n\n# ...\n\nOAuth2Form = Annotated[OAuth2PasswordRequestForm, Depends()]\nSession = Annotated[Session, Depends(get_session)]\n\n@router.post('/token', response_model=Token)\ndef login_for_access_token(form_data: OAuth2Form, session: Session):\n#...\n

    Atrav\u00e9s do uso de tipos Annotated, conseguimos reutilizar os mesmos consistentemente, reduzindo a repeti\u00e7\u00e3o de c\u00f3digo e aumentando a efici\u00eancia do nosso trabalho.

    "},{"location":"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\n\n\nclass Settings(BaseSettings):\n    model_config = SettingsConfigDict(\n        env_file='.env', env_file_encoding='utf-8'\n    )\n\n    DATABASE_URL: str\n    SECRET_KEY: str\n    ALGORITHM: str\n    ACCESS_TOKEN_EXPIRE_MINUTES: int\n

    Agora, precisamos adicionar estes valores ao nosso arquivo .env.

    .env
    DATABASE_URL=\"sqlite:///database.db\"\nSECRET_KEY=\"your-secret-key\"\nALGORITHM=\"HS256\"\nACCESS_TOKEN_EXPIRE_MINUTES=30\n

    Com isso, podemos alterar o nosso c\u00f3digo em fast_zero/security.py para ler as constantes a partir da classe Settings.

    "},{"location":"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\n\nsettings = Settings()\n

    Com isso, todos os lugares onde as constantes eram usadas devem ser substitu\u00eddos por settings.CONSTANTE. Por exemplo, na fun\u00e7\u00e3o create_access_token, vamos alterar para usar as constantes da classe Settings:

    fast_zero/security.py
    def create_access_token(data: dict):\n    to_encode = data.copy()\n    expire = datetime.utcnow() + timedelta(\n        minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES\n    )\n    to_encode.update({'exp': expire})\n    encoded_jwt = jwt.encode(\n        to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM\n    )\n    return encoded_jwt\n

    Desta forma, eliminamos todas as constantes do c\u00f3digo-fonte e passamos a usar as configura\u00e7\u00f5es a partir da classe Settings. Isso torna nosso c\u00f3digo mais seguro, pois as constantes sens\u00edveis, como a chave secreta, est\u00e3o agora seguras em nosso arquivo .env, e nosso c\u00f3digo fica mais modular, pois podemos facilmente alterar estas constantes simplesmente mudando os valores no arquivo .env. Al\u00e9m disso, essa abordagem facilita o gerenciamento de diferentes ambientes (como desenvolvimento, teste e produ\u00e7\u00e3o) pois cada ambiente pode ter seu pr\u00f3prio arquivo .env com suas configura\u00e7\u00f5es espec\u00edficas.

    "},{"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\n# ...\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":"Tornando o sistema de autentica\u00e7\u00e3o robusto","text":""},{"location":"07/#tornando-o-sistema-de-autenticacao-robusto","title":"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):\n    response = client.put(\n        f'/users/{user.id}',\n        headers={'Authorization': f'Bearer {token}'},\n        json={\n            'username': 'bob',\n            'email': 'bob@example.com',\n            'password': 'mynewpassword',\n        },\n    )\n    assert response.status_code == 400\n    assert response.json() == {'detail': 'Not enough permissions'}\n
    Este teste vai simular um usu\u00e1rio tentando alterar as informa\u00e7\u00f5es de outro usu\u00e1rio. Se nosso sistema estiver funcionando corretamente, ele dever\u00e1 rejeitar essa tentativa e retornar um erro."},{"location":"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\n# ...\n\nclass UserFactory(factory.Factory):\n    class Meta:\n        model = User\n\n    id = factory.Sequence(lambda n: n)\n    username = factory.LazyAttribute(lambda obj: f'test{obj.id}')\n    email = factory.LazyAttribute(lambda obj: f'{obj.username}@test.com')\n    password = factory.LazyAttribute(lambda obj: f'{obj.username}@example.com')\n

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

    • 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):\n    password = 'testtest'\n    user = UserFactory(password=get_password_hash(password))\n\n    session.add(user)\n    session.commit()\n    session.refresh(user)\n\n    user.clean_password = 'testtest'\n\n    return user\n\n\n@pytest.fixture\ndef other_user(session):\n    password = 'testtest'\n    user = UserFactory(password=get_password_hash(password))\n\n    session.add(user)\n    session.commit()\n    session.refresh(user)\n\n    user.clean_password = 'testtest'\n\n    return user\n

    A cria\u00e7\u00e3o de outra fixture chamada other_user \u00e9 crucial para simular o cen\u00e1rio de um usu\u00e1rio tentando acessar ou modificar as informa\u00e7\u00f5es de outro usu\u00e1rio no sistema. Ao criar duas fixtures diferentes, user e other_user, podemos efetivamente simular dois usu\u00e1rios diferentes em nossos testes. Isso nos permite avaliar como nosso sistema reage quando um usu\u00e1rio tenta realizar uma a\u00e7\u00e3o n\u00e3o autorizada, como alterar as informa\u00e7\u00f5es de outro usu\u00e1rio.

    Um aspecto interessante no uso das f\u00e1bricas \u00e9 que, sempre que forem chamadas, elas retornar\u00e3o um novo User, pois estamos fixando apenas a senha. Dessa forma, cada chamada a essa f\u00e1brica de usu\u00e1rios retornar\u00e1 um User diferente, com base nos atributos \"lazy\" que usamos.

    Com essa nova configura\u00e7\u00e3o, podemos finalmente testar o cen\u00e1rio de um usu\u00e1rio tentando alterar as informa\u00e7\u00f5es de outro usu\u00e1rio. E como voc\u00ea pode ver, nossos testes passaram com sucesso, o que indica que nosso sistema est\u00e1 lidando corretamente com essa situa\u00e7\u00e3o.

    tests/test_users.py
    def test_update_user_with_wrong_user(client, other_user, token):\n    response = client.put(\n        f'/users/{other_user.id}',\n        headers={'Authorization': f'Bearer {token}'},\n        json={\n            'username': 'bob',\n            'email': 'bob@example.com',\n            'password': 'mynewpassword',\n        },\n    )\n    assert response.status_code == 400\n    assert 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\n# ...\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):\n    response = client.delete(\n        f'/users/{other_user.id}',\n        headers={'Authorization': f'Bearer {token}'},\n    )\n    assert response.status_code == 400\n    assert response.json() == {'detail': 'Not enough permissions'}\n

    Como voc\u00ea pode ver, esse teste tenta deletar o user de um id diferente usando o token do user. Se nosso sistema estiver funcionando corretamente, ele dever\u00e1 rejeitar essa tentativa e retornar um status 400 com uma mensagem de erro indicando que o usu\u00e1rio n\u00e3o tem permiss\u00f5es suficientes para realizar essa a\u00e7\u00e3o.

    Vamos executar esse teste agora e ver o que acontece:

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

    \u00d3timo, nosso teste passou! Isso significa que nosso sistema est\u00e1 corretamente impedindo um usu\u00e1rio de deletar a conta de outro usu\u00e1rio.

    Agora que terminamos de testar a autoriza\u00e7\u00e3o, 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 \"congelar\" o tempo em um ponto espec\u00edfico ou avan\u00e7\u00e1-lo conforme necess\u00e1rio durante os testes. Isso \u00e9 especialmente \u00fatil para testar funcionalidades sens\u00edveis ao tempo, como a expira\u00e7\u00e3o de tokens, sem ter que esperar em tempo real.

    Primeiro, 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.

    Ao elaborarmos o teste, usaremos a funcionalidade de congelamento de tempo do freezegun. O objetivo \u00e9 simular a cria\u00e7\u00e3o de um token \u00e0s 12:00 e, em seguida, verificar sua expira\u00e7\u00e3o \u00e0s 12:31. Neste cen\u00e1rio, estamos utilizando o conceito de \"viajar no tempo\" para al\u00e9m do per\u00edodo de validade do token, garantindo que a tentativa subsequente de utiliz\u00e1-lo resultar\u00e1 em um erro de autentica\u00e7\u00e3o.

    tests/test_auth.py
    from freezegun import freeze_time\n\n# ...\n\ndef test_token_expired_after_time(client, user):\n    with freeze_time('2023-07-14 12:00:00'):\n        response = client.post(\n            '/auth/token',\n            data={'username': user.email, 'password': user.clean_password},\n        )\n        assert response.status_code == 200\n        token = response.json()['access_token']\n\n    with freeze_time('2023-07-14 12:31:00'):\n        response = client.put(\n            f'/users/{user.id}',\n            headers={'Authorization': f'Bearer {token}'},\n            json={\n                'username': 'wrongwrong',\n                'email': 'wrong@wrong.com',\n                'password': 'wrong',\n            },\n        )\n        assert response.status_code == 401\n        assert response.json() == {'detail': 'Could not validate credentials'}\n

    Lembre-se de que configuramos nosso token para expirar ap\u00f3s 30 minutos. Portanto, n\u00f3s avan\u00e7amos o tempo em 31 minutos para garantir que o token tenha expirado.

    Agora, vamos executar nosso teste e ver o que acontece:

    $ Execu\u00e7\u00e3o no terminal!
    task test\n\n# ...\n\ntests/test_users.py::test_token_expired_after_time 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):\n    response = client.post(\n        '/auth/token',\n        data={'username': 'no_user@no_domain.com', 'password': 'testtest'},\n    )\n    assert response.status_code == 400\n    assert 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):\n    response = client.post(\n        '/auth/token', data={'username': user.email, 'password': 'wrong_password'}\n    )\n    assert response.status_code == 400\n    assert response.json() == {'detail': 'Incorrect email or password'}\n

    Com esses testes, garantimos que nossas exce\u00e7\u00f5es est\u00e3o sendo lan\u00e7adas corretamente. Essa \u00e9 uma parte importante da constru\u00e7\u00e3o de um sistema de autentica\u00e7\u00e3o robusto, pois nos permite ter confian\u00e7a de que estamos tratando corretamente os casos de erro.

    "},{"location":"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 (\n    create_access_token,\n    get_current_user,\n    verify_password,\n)\n\n# ...\n\n@router.post('/refresh_token', response_model=Token)\ndef refresh_access_token(\n    user: User = Depends(get_current_user),\n):\n    new_access_token = create_access_token(data={'sub': user.email})\n\n    return {'access_token': new_access_token, 'token_type': 'bearer'}\n

    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):\n    response = client.post(\n        '/auth/refresh_token',\n        headers={'Authorization': f'Bearer {token}'},\n    )\n\n    data = response.json()\n\n    assert response.status_code == 200\n    assert 'access_token' in data\n    assert 'token_type' in data\n    assert data['token_type'] == 'bearer'\n

    Ainda \u00e9 importante garantir que nosso sistema trate corretamente os tokens expirados. Para isso, 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_expired_dont_refresh(client, user):\n    with freeze_time('2023-07-14 12:00:00'):\n        response = client.post(\n            '/auth/token',\n            data={'username': user.email, 'password': user.clean_password},\n        )\n        assert response.status_code == 200\n        token = response.json()['access_token']\n\n    with freeze_time('2023-07-14 12:31:00'):\n        response = client.post(\n            '/auth/refresh_token',\n            headers={'Authorization': f'Bearer {token}'},\n        )\n        assert response.status_code == 401\n        assert response.json() == {'detail': 'Could not validate credentials'}\n

    Agora, se executarmos nossos testes, todos eles devem passar, incluindo os novos testes que acabamos de adicionar.

    $ Execu\u00e7\u00e3o no terminal!
    task test\n\n# ...\n\ntests/test_app.py::test_root_deve_retornar_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_expired_after_time PASSED\ntests/test_db.py::test_create_user PASSED\ntests/test_users.py::test_create_user PASSED\ntests/test_users.py::test_read_users PASSED\ntests/test_users.py::test_read_users_with_users PASSED\ntests/test_users.py::test_update_user PASSED\ntests/test_users.py::test_update_user_with_wrong_user PASSED\ntests/test_users.py::test_delete_user PASSED\ntests/test_users.py::test_delete_user_wrong_user PASSED\ntests/test_users.py::test_token_expired_dont_refresh PASSED\n

    Com esses testes, podemos ter certeza de que cobrimos alguns casos importantes relacionados \u00e0 autentica\u00e7\u00e3o de usu\u00e1rios em nossa API.

    "},{"location":"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":"Criando Rotas CRUD para Gerenciamento de Tarefas em FastAPI","text":""},{"location":"08/#criando-rotas-crud-para-gerenciamento-de-tarefas-em-fastapi","title":"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\n\nrouter = APIRouter(prefix='/todos', tags=['todos'])\n

    Neste c\u00f3digo, criamos uma nova inst\u00e2ncia da classe APIRouter do FastAPI. Esta classe \u00e9 usada para definir as rotas de nossa aplica\u00e7\u00e3o. A inst\u00e2ncia router funcionar\u00e1 como um mini aplicativo FastAPI, que poder\u00e1 ter suas pr\u00f3prias rotas, modelos de resposta, etc.

    A op\u00e7\u00e3o prefix no construtor do APIRouter \u00e9 usada para definir um prefixo comum para todas as rotas definidas neste roteador. Isso significa que todas as rotas que definirmos neste roteador come\u00e7ar\u00e3o com /todos. Usamos um prefixo aqui porque queremos agrupar todas as rotas relacionadas a tarefas em um lugar. Isso torna nossa aplica\u00e7\u00e3o mais organizada e f\u00e1cil de entender.

    A op\u00e7\u00e3o tags \u00e9 usada para agrupar as rotas em se\u00e7\u00f5es no documento interativo de API gerado pelo FastAPI (como Swagger UI e ReDoc). Todas as rotas que definirmos neste roteador aparecer\u00e3o na se\u00e7\u00e3o \"todos\" da documenta\u00e7\u00e3o da API.

    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\n\nfrom fast_zero.routes import auth, todos, users\n\napp = FastAPI()\n\napp.include_router(users.router)\napp.include_router(auth.router)\napp.include_router(todos.router)\n\n\n@app.get('/')\ndef read_root():\n    return {'message': 'Ol\u00e1 Mundo!'}\n

    Neste c\u00f3digo, chamamos o m\u00e9todo include_router do FastAPI para cada roteador que definimos. Este m\u00e9todo adiciona todas as rotas do roteador \u00e0 nossa aplica\u00e7\u00e3o. Com isso, nossa aplica\u00e7\u00e3o agora ter\u00e1 todas as rotas definidas nos roteadores users, auth e todos.

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

    fast_zero/models.py
    from enum import Enum\n\nfrom sqlalchemy import ForeignKey\nfrom sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship\n\n\nclass TodoState(str, Enum):\n    draft = 'draft'\n    todo = 'todo'\n    doing = 'doing'\n    done = 'done'\n    trash = 'trash'\n\n\nclass Base(DeclarativeBase):\n    pass\n\n\nclass User(Base):\n    __tablename__ = 'users'\n\n    id: Mapped[int] = mapped_column(primary_key=True)\n    username: Mapped[str]\n    password: Mapped[str]\n    email: Mapped[str]\n\n    todos: Mapped[list['Todo']] = relationship(\n        back_populates='user', cascade='all, delete-orphan'\n    )\n\n\nclass Todo(Base):\n    __tablename__ = 'todos'\n\n    id: Mapped[int] = mapped_column(primary_key=True)\n    title: Mapped[str]\n    description: Mapped[str]\n    state: Mapped[TodoState]\n    user_id: Mapped[int] = mapped_column(ForeignKey('users.id'))\n\n    user: 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):\n    todo = Todo(\n        title='Test Todo',\n        description='Test Desc',\n        state='draft',\n        user_id=user.id,\n    )\n\n    session.add(todo)\n    session.commit()\n    session.refresh(todo)\n\n    user = session.scalar(select(User).where(User.id == user.id))\n\n    assert todo in user.todos\n

    Com isso, voc\u00ea pode executar os testes:

    $ Execu\u00e7\u00e3o no terminal!
    task test tests/test_db.py\n# ...\ntests/test_db.py::test_create_user_without_todos PASSED\ntests/test_db.py::test_create_todo PASSED\n

    Isso mostra que os testes foram bem-sucedidos. Mesmo sem testes mais extensivos, agora 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\n#...\n\nclass TodoSchema(BaseModel):\n    title: str\n    description: str\n    state: TodoState\n\nclass TodoPublic(BaseModel):\n    id: int\n    title: str\n    description: str\n    state: TodoState\n\nclass TodoList(BaseModel):\n    todos: list[TodoPublic]\n

    TodoSchema ser\u00e1 usado para validar os dados de entrada quando uma nova tarefa \u00e9 criada e TodoPublic ser\u00e1 usado para validar os dados de sa\u00edda quando uma tarefa \u00e9 retornada em um endpoint.

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

    fast_zero/routes/todos.py
    from typing import Annotated\n\nfrom fastapi import APIRouter, Depends\nfrom sqlalchemy.orm import Session\n\nfrom fast_zero.database import get_session\nfrom fast_zero.models import Todo, User\nfrom fast_zero.schemas import TodoPublic, TodoSchema\nfrom fast_zero.security import get_current_user\n\nrouter = APIRouter()\n\nCurrentUser = Annotated[User, Depends(get_current_user)]\n\nrouter = APIRouter(prefix='/todos', tags=['todos'])\n\n\n@router.post('/', response_model=TodoPublic)\ndef create_todo(\n    todo: TodoSchema,\n    user: CurrentUser,\n    session: Session = Depends(get_session),\n):\n    db_todo: Todo = Todo(\n        title=todo.title,\n        description=todo.description,\n        state=todo.state,\n        user_id=user.id,\n    )\n    session.add(db_todo)\n    session.commit()\n    session.refresh(db_todo)\n\n    return db_todo\n

    Neste endpoint, fazemos uso da depend\u00eancia get_current_user que garante que somente usu\u00e1rios autenticados possam criar tarefas, protegendo assim nossa aplica\u00e7\u00e3o.

    "},{"location":"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):\n    response = client.post(\n        '/todos/',\n        headers={'Authorization': f'Bearer {token}'},\n        json={\n            'title': 'Test todo',\n            'description': 'Test todo description',\n            'state': 'draft',\n        },\n    )\n    assert response.json() == {\n        'id': 1,\n        'title': 'Test todo',\n        'description': 'Test todo description',\n        'state': 'draft',\n    }\n

    No teste, fazemos uma requisi\u00e7\u00e3o POST para o endpoint '/todos' passando um token de autentica\u00e7\u00e3o v\u00e1lido e um JSON com os dados da tarefa a ser criada. Em seguida, verificamos se a resposta cont\u00e9m os dados corretos da tarefa criada.

    Para executar este teste, voc\u00ea deve usar o comando abaixo no terminal:

    $ Execu\u00e7\u00e3o no terminal!
    task test tests/test_todos.py\n# ...\ntests/test_todos.py::test_create_todo PASSED\n

    Com essa implementa\u00e7\u00e3o, os testes devem passar. Por\u00e9m, apesar do sucesso dos testes, nosso c\u00f3digo ainda n\u00e3o est\u00e1 completamente pronto. Ainda \u00e9 necess\u00e1rio criar uma migra\u00e7\u00e3o para a tabela de tarefas no banco de dados.

    "},{"location":"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\n# ...\n\nGenerating /<caminho>/fast_zero/migrations/versions/de865434f506_create_todos_table.py\n

    Este comando gera um arquivo de migra\u00e7\u00e3o, que se parece com o c\u00f3digo abaixo:

    migrations/versions/de865434f506_create_todos_table.py
    def upgrade() -> None:\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.create_table('todos',\n    sa.Column('id', sa.Integer(), nullable=False),\n    sa.Column('title', sa.String(), nullable=False),\n    sa.Column('description', sa.String(), nullable=False),\n    sa.Column('state', sa.Enum('draft', 'todo', 'doing', 'done', 'trash', name='todostate'), nullable=False),\n    sa.Column('user_id', sa.Integer(), nullable=False),\n    sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),\n    sa.PrimaryKeyConstraint('id')\n    )\n    # ### end Alembic commands ###\n\n\ndef downgrade() -> None:\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.drop_table('todos')\n    # ### end Alembic commands ###\n

    Depois que a migra\u00e7\u00e3o for criada, precisamos aplic\u00e1-la ao nosso banco de dados. Execute o comando alembic upgrade head para aplicar a migra\u00e7\u00e3o.

    $ Execu\u00e7\u00e3o no terminal!
    alembic upgrade head\nINFO  [alembic.runtime.migration] Context impl SQLiteImpl.\nINFO  [alembic.runtime.migration] Will assume non-transactional DDL.\nINFO  [alembic.runtime.migration] Running upgrade e018397cecf4 -> de865434f506, create todos table\n

    Agora que a migra\u00e7\u00e3o foi aplicada, nosso banco de dados deve ter uma nova tabela de tarefas. Para verificar, voc\u00ea pode abrir o banco de dados com o comando sqlite3 database.db e depois executar o comando .schema para ver o esquema do banco de dados.

    $ Execu\u00e7\u00e3o no terminal!
    sqlite3 database.db\n# ...\nsqlite> .schema\n# ...\nCREATE TABLE todos (\n    id INTEGER NOT NULL,\n    title VARCHAR NOT NULL,\n    description VARCHAR NOT NULL,\n    state VARCHAR(5) NOT NULL,\n    user_id INTEGER NOT NULL,\n    PRIMARY KEY (id),\n    FOREIGN KEY(user_id) REFERENCES users (id)\n);\n

    Finalmente, agora que temos a tabela de tarefas em nosso banco de dados, podemos testar nosso endpoint de cria\u00e7\u00e3o de tarefas no Swagger. Para fazer isso, execute nosso servidor FastAPI e abra o Swagger no seu navegador.

    "},{"location":"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 tarefas \u00e9 fazer um filtro de busca. Em alguns momentos queremos buscar uma tarefa por t\u00edtulo, em outro momento por descri\u00e7\u00e3o, \u00e0s vezes s\u00f3 pelo estado. Por exemplo, somente tarefas conclu\u00eddas.

    Para fazer isso, podemos contar com um recurso do FastAPI chamado Query. A Query permite que definamos par\u00e2metros espec\u00edficos na URL, que podem ser utilizados para filtrar os resultados retornados pelo endpoint. Isso \u00e9 feito atrav\u00e9s da inclus\u00e3o de par\u00e2metros como query strings na URL, 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=TodoList)\ndef list_todos(\n    session: Session,\n    user: CurrentUser,\n    title: str = Query(None),\n    description: str = Query(None),\n    state: str = Query(None),\n    offset: int = Query(None),\n    limit: int = Query(None),\n):\n    query = select(Todo).where(Todo.user_id == user.id)\n\n    if title:\n        query = query.filter(Todo.title.contains(title))\n\n    if description:\n        query = query.filter(Todo.description.contains(description))\n\n    if state:\n        query = query.filter(Todo.state == state)\n\n    todos = session.scalars(query.offset(offset).limit(limit)).all()\n\n    return {'todos': todos}\n

    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\n\nfrom fast_zero.models import Todo, TodoState, User\n\n# ...\n\nclass TodoFactory(factory.Factory):\n    class Meta:\n        model = Todo\n\n    title = factory.Faker('text')\n    description = factory.Faker('text')\n    state = factory.fuzzy.FuzzyChoice(TodoState)\n    user_id = 1\n

    A fixture acima pode ser usada em diversos testes, reduzindo a duplica\u00e7\u00e3o de c\u00f3digo e melhorando a manuten\u00e7\u00e3o. Por exemplo, em um teste que precisa criar v\u00e1rios objetos Todo, voc\u00ea pode simplesmente usar a TodoFactory para criar esses objetos com uma \u00fanica linha de c\u00f3digo. A factory j\u00e1 cont\u00e9m a l\u00f3gica necess\u00e1ria para criar um objeto v\u00e1lido, e voc\u00ea pode facilmente sobrescrever qualquer um dos atributos, se necess\u00e1rio, para o caso de teste espec\u00edfico.

    A utiliza\u00e7\u00e3o de f\u00e1bricas tamb\u00e9m promove uma melhor separa\u00e7\u00e3o entre a l\u00f3gica de cria\u00e7\u00e3o do objeto e a l\u00f3gica do teste, tornando os testes mais leg\u00edveis e f\u00e1ceis de seguir. Com a TodoFactory, somos capazes de simular e testar diversos cen\u00e1rios de busca e filtragem, garantindo que nosso endpoint de listagem funcione corretamente em todas as situa\u00e7\u00f5es poss\u00edveis, aumentando assim a robustez e confiabilidade de nosso sistema.

    "},{"location":"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):\n    session.bulk_save_objects(TodoFactory.create_batch(5, user_id=user.id))\n    session.commit()\n\n    response = client.get(\n        '/todos/',\n        headers={'Authorization': f'Bearer {token}'},\n    )\n\n    assert len(response.json()['todos']) == 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):\n    session.bulk_save_objects(TodoFactory.create_batch(5, user_id=user.id))\n    session.commit()\n\n    response = client.get(\n        '/todos/?offset=1&limit=2',\n        headers={'Authorization': f'Bearer {token}'},\n    )\n\n    assert len(response.json()['todos']) == 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):\n    session.bulk_save_objects(\n        TodoFactory.create_batch(5, user_id=user.id, title='Test todo 1')\n    )\n    session.commit()\n\n    response = client.get(\n        '/todos/?title=Test todo 1',\n        headers={'Authorization': f'Bearer {token}'},\n    )\n\n    assert len(response.json()['todos']) == 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):\n    session.bulk_save_objects(\n        TodoFactory.create_batch(5, user_id=user.id, description='description')\n    )\n    session.commit()\n\n    response = client.get(\n        '/todos/?description=desc',\n        headers={'Authorization': f'Bearer {token}'},\n    )\n\n    assert len(response.json()['todos']) == 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):\n    session.bulk_save_objects(\n        TodoFactory.create_batch(5, user_id=user.id, state=TodoState.draft)\n    )\n    session.commit()\n\n    response = client.get(\n        '/todos/?state=draft',\n        headers={'Authorization': f'Bearer {token}'},\n    )\n\n    assert len(response.json()['todos']) == 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):\n    session.bulk_save_objects(\n        TodoFactory.create_batch(\n            5,\n            user_id=user.id,\n            title='Test todo combined',\n            description='combined description',\n            state=TodoState.done,\n        )\n    )\n\n    session.bulk_save_objects(\n        TodoFactory.create_batch(\n            3,\n            user_id=user.id,\n            title='Other title',\n            description='other description',\n            state=TodoState.todo,\n        )\n    )\n    session.commit()\n\n    response = client.get(\n        '/todos/?title=Test todo combined&description=combined&state=done',\n        headers={'Authorization': f'Bearer {token}'},\n    )\n\n    assert len(response.json()['todos']) == 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/#endpoint-de-alteracao","title":"Endpoint de Altera\u00e7\u00e3o","text":"

    Para fazer a altera\u00e7\u00e3o de uma tarefa, precisamos de um modelo onde tudo seja opcional, j\u00e1 que poder\u00edamos querer atualizar apenas um ou alguns campos da tarefa. Vamos criar o esquema TodoUpdate, no qual todos os campos s\u00e3o opcionais:

    fast_zero/schemas.py
    class TodoUpdate(BaseModel):\n    title: str | None = None\n    description: str | None = None\n    completed: str | None = None\n

    Para podermos alterar somente os valores que recebemos no modelo, temos que fazer um dump somente dos valores que recebemos e os atualizar no objeto que pegamos da base de dados:

    fast_zero/routes/todos.py
    @router.patch('/{todo_id}', response_model=TodoPublic)\ndef patch_todo(\n    todo_id: int, session: Session, user: CurrentUser, todo: TodoUpdate\n):\n    db_todo = session.scalar(\n        select(Todo).where(Todo.user_id == user.id, Todo.id == todo_id)\n    )\n\n    if not db_todo:\n        raise HTTPException(status_code=404, detail='Task not found.')\n\n    for key, value in todo.model_dump(exclude_unset=True).items():\n        setattr(db_todo, key, value)\n\n    session.add(db_todo)\n    session.commit()\n    session.refresh(db_todo)\n\n    return db_todo\n

    A linha for key, value in todo.model_dump(exclude_unset=True).items(): est\u00e1 iterando atrav\u00e9s de todos os campos definidos na inst\u00e2ncia todo do modelo de atualiza\u00e7\u00e3o. A fun\u00e7\u00e3o model_dump \u00e9 um m\u00e9todo que vem do modelo BaseModel do Pydantic e permite exportar o modelo para um dicion\u00e1rio.

    O par\u00e2metro exclude_unset=True \u00e9 importante aqui, pois significa que apenas os campos que foram explicitamente definidos (ou seja, aqueles que foram inclu\u00eddos na solicita\u00e7\u00e3o PATCH) ser\u00e3o inclu\u00eddos no dicion\u00e1rio resultante. Isso permite que voc\u00ea atualize apenas os campos que foram fornecidos na solicita\u00e7\u00e3o, deixando os outros inalterados.

    Depois de obter a chave e o valor de cada campo definido, a linha setattr(db_todo, key, value) \u00e9 usada para atualizar o objeto db_todo que representa a tarefa no banco de dados. A fun\u00e7\u00e3o setattr \u00e9 uma fun\u00e7\u00e3o embutida do Python que permite definir o valor de um atributo em um objeto. Neste caso, ele est\u00e1 definindo o atributo com o nome igual \u00e0 chave (ou seja, o nome do campo) no objeto db_todo com o valor correspondente.

    Dessa forma, garantimos que somente os campos enviados ao schema sejam atualizados no objeto.

    "},{"location":"08/#testes-para-o-endpoint-de-alteracao","title":"Testes para o Endpoint de Altera\u00e7\u00e3o","text":"

    Os testes aqui incluem o caso de atualiza\u00e7\u00e3o bem-sucedida e o caso de erro quando a tarefa n\u00e3o \u00e9 encontrada:

    fast_zero/tests/test_todos.py
    def test_patch_todo_error(client, token):\n    response = client.patch(\n        '/todos/10',\n        json={},\n        headers={'Authorization': f'Bearer {token}'},\n    )\n    assert response.status_code == 404\n    assert response.json() == {'detail': 'Task not found.'}\n\ndef test_patch_todo(session, client, user, token):\n    todo = TodoFactory(user_id=user.id)\n\n    session.add(todo)\n    session.commit()\n\n    response = client.patch(\n        f'/todos/{todo.id}',\n        json={'title': 'teste!'},\n        headers={'Authorization': f'Bearer {token}'},\n    )\n    assert response.status_code == 200\n    assert response.json()['title'] == 'teste!'\n

    Agora precisamos executar os testes para ver se est\u00e1 tudo correto:

    $ Execu\u00e7\u00e3o no terminal!
    task test tests/test_todos.py\n\n# ...\n\ntests/test_todos.py::test_create_todo PASSED\ntests/test_todos.py::test_list_todos PASSED\ntests/test_todos.py::test_list_todos_pagination PASSED\ntests/test_todos.py::test_list_todos_filter_title PASSED\ntests/test_todos.py::test_list_todos_filter_description PASSED\ntests/test_todos.py::test_list_todos_filter_state PASSED\ntests/test_todos.py::test_list_todos_filter_combined PASSED\ntests/test_todos.py::test_patch_todo_error PASSED\ntests/test_todos.py::test_patch_todo PASSED\n

    Com tudo funcionando, podemos partir para o nosso endpoint de DELETE.

    "},{"location":"08/#endpoint-de-delecao","title":"Endpoint de Dele\u00e7\u00e3o","text":"

    A rota para deletar uma tarefa \u00e9 simples e direta. Caso o todo exista, vamos deletar ele com a sesion caso n\u00e3o, retornamos 404:

    fast_zero/routes/todos.py
    @router.delete('/{todo_id}', response_model=Message)\ndef delete_todo(todo_id: int, session: Session, user: CurrentUser):\n    todo = session.scalar(\n        select(Todo).where(Todo.user_id == user.id, Todo.id == todo_id)\n    )\n\n    if not todo:\n        raise HTTPException(status_code=404, detail='Task not found.')\n\n    session.delete(todo)\n    session.commit()\n\n    return {'detail': 'Task has been deleted successfully.'}\n
    "},{"location":"08/#testes-para-o-endpoint-de-delecao","title":"Testes para o Endpoint de Dele\u00e7\u00e3o","text":"

    Esses testes verificam tanto a remo\u00e7\u00e3o bem-sucedida quanto o caso de erro quando a tarefa n\u00e3o \u00e9 encontrada:

    fast_zero/tests/test_todos.py
    def test_delete_todo(session, client, user, token):\n    todo = TodoFactory(id=1, user_id=user.id)\n\n    session.add(todo)\n    session.commit()\n\n    response = client.delete(\n        f'/todos/{todo.id}', headers={'Authorization': f'Bearer {token}'}\n    )\n\n    assert response.status_code == 200\n    assert response.json() == {'detail': 'Task has been deleted successfully.'}\n\ndef test_delete_todo_error(client, token):\n    response = client.delete(\n        f'/todos/{10}', headers={'Authorization': f'Bearer {token}'}\n    )\n\n    assert response.status_code == 404\n    assert response.json() == {'detail': 'Task not found.'}\n

    Por fim, precisamos executar os testes para ver se est\u00e1 tudo correto:

    $ Execu\u00e7\u00e3o no terminal!
    task test tests/test_todos.py\n\n# ...\n\ntests/test_todos.py::test_create_todo PASSED\ntests/test_todos.py::test_list_todos PASSED\ntests/test_todos.py::test_list_todos_pagination PASSED\ntests/test_todos.py::test_list_todos_filter_title PASSED\ntests/test_todos.py::test_list_todos_filter_description PASSED\ntests/test_todos.py::test_list_todos_filter_state PASSED\ntests/test_todos.py::test_list_todos_filter_combined PASSED\ntests/test_todos.py::test_delete_todo PASSED\ntests/test_todos.py::test_delete_todo_error PASSED\ntests/test_todos.py::test_patch_todo_error PASSED\ntests/test_todos.py::test_patch_todo PASSED\n
    "},{"location":"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":"

    Nesta aula exploramos os aspectos essenciais para construir uma API completa e funcional para gerenciar tarefas, integrando-se ao sistema de autentica\u00e7\u00e3o que j\u00e1 t\u00ednhamos desenvolvido.

    Iniciamos criando a estrutura de banco de dados para as tarefas, incluindo tabelas e migra\u00e7\u00f5es, e em seguida definimos os schemas necess\u00e1rios. A partir da\u00ed, trabalhamos na cria\u00e7\u00e3o dos endpoints para as opera\u00e7\u00f5es CRUD: cria\u00e7\u00e3o, leitura (listagem com filtragem), atualiza\u00e7\u00e3o (edi\u00e7\u00e3o) e exclus\u00e3o (dele\u00e7\u00e3o).

    Em cada est\u00e1gio, focamos na qualidade e na robustez, utilizando testes rigorosos para assegurar que os endpoints se comportassem conforme esperado. Exploramos tamb\u00e9m t\u00e9cnicas espec\u00edficas como atualiza\u00e7\u00e3o parcial e filtragem avan\u00e7ada, tornando a API flex\u00edvel e poderosa.

    O resultado foi um sistema integrado de gerenciamento de tarefas, ou um \"todo list\", ligado aos usu\u00e1rios e \u00e0 autentica\u00e7\u00e3o que j\u00e1 hav\u00edamos implementado. Esta aula refor\u00e7ou a import\u00e2ncia de um design cuidadoso e uma implementa\u00e7\u00e3o criteriosa, ilustrando como a FastAPI pode ser usada para criar APIs eficientes e profissionais.

    Agora que a nossa aplica\u00e7\u00e3o est\u00e1 crescendo e ganhando mais funcionalidades, na pr\u00f3xima aula, vamos mergulhar no mundo da dockeriza\u00e7\u00e3o. Iremos aprender a colocar a nossa aplica\u00e7\u00e3o dentro de um container Docker, facilitando o deploy e o escalonamento. Este \u00e9 um passo vital no desenvolvimento moderno de aplica\u00e7\u00f5es e estou ansioso para gui\u00e1-lo atrav\u00e9s dele. At\u00e9 l\u00e1!

    "},{"location":"09/","title":"Dockerizando a nossa aplica\u00e7\u00e3o e introduzindo o PostgreSQL","text":""},{"location":"09/#dockerizando-a-nossa-aplicacao-e-introduzindo-o-postgresql","title":"Dockerizando a nossa aplica\u00e7\u00e3o e introduzindo o PostgreSQL","text":"

    Objetivos da aula:

    • Compreender os conceitos b\u00e1sicos do Docker
    • Entender como criar uma imagem Docker para a nossa aplica\u00e7\u00e3o FastAPI
    • Aprender a rodar a aplica\u00e7\u00e3o utilizando Docker
    • Introduzir o conceito de Docker Compose para gerenciamento de m\u00faltiplos cont\u00eaineres
    • Aprender o que \u00e9 um Dockerfile e sua estrutura
    • Entender os benef\u00edcios e motivos da mudan\u00e7a de SQLite para PostgreSQL
    Caso prefira ver a aula em v\u00eddeo

    Aula Slides C\u00f3digo

    Depois de implementar nosso gerenciador de tarefas na aula anterior, temos uma primeira vers\u00e3o est\u00e1vel da nossa aplica\u00e7\u00e3o. Nesta aula, al\u00e9m de aprendermos a \"dockerizar\" nossa aplica\u00e7\u00e3o FastAPI, tamb\u00e9m abordaremos a migra\u00e7\u00e3o do banco de dados SQLite para o PostgreSQL.

    "},{"location":"09/#o-docker-e-a-nossa-aplicacao","title":"O Docker e a nossa aplica\u00e7\u00e3o","text":"

    Docker \u00e9 uma plataforma aberta que permite automatizar o processo de implanta\u00e7\u00e3o, escalonamento e opera\u00e7\u00e3o de aplica\u00e7\u00f5es dentro de cont\u00eaineres. Ele serve para \"empacotar\" uma aplica\u00e7\u00e3o e suas depend\u00eancias em um cont\u00eainer virtual que pode ser executado em qualquer sistema operacional que suporte Docker. Isso facilita a implanta\u00e7\u00e3o, o desenvolvimento e o compartilhamento de aplica\u00e7\u00f5es, al\u00e9m de proporcionar um ambiente isolado e consistente.

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

    Para criar um container Docker, escrevemos uma lista de passos de como construir o ambiente para execu\u00e7\u00e3o da nossa aplica\u00e7\u00e3o em um arquivo chamado Dockerfile. Ele define o ambiente de execu\u00e7\u00e3o, os comandos necess\u00e1rios para preparar o ambiente e o comando a ser executado quando um cont\u00eainer \u00e9 iniciado a partir da imagem.

    Uma das coisas interessantes sobre Docker \u00e9 que existe um Hub de containers prontos onde a comunidade hospeda imagens \"prontas\", que podemos usar como ponto de partida. Por exemplo, a comunidade de python mant\u00e9m um grupo de imagens com o ambiente python pronto para uso. Podemos partir dessa imagem com o python j\u00e1 instalado adicionar os passos para que nossa aplica\u00e7\u00e3o seja executada.

    Aqui est\u00e1 um exemplo de Dockerfile para executar nossa aplica\u00e7\u00e3o:

    FROM python:3.11-slim\nENV POETRY_VIRTUALENVS_CREATE=false\n\nWORKDIR app/\nCOPY . .\n\nRUN pip install poetry\n\nRUN poetry config installer.max-workers 10\nRUN poetry install --no-interaction --no-ansi\n\nEXPOSE 800\nCMD [ \"poetry\", \"run\", \"uvicorn\", \"--host\", \"0.0.0.0\", \"fast_zero.app:app\" ]\n

    Aqui est\u00e1 o que cada linha faz:

    1. FROM python:3.11-slim: Define a imagem base para nosso cont\u00eainer. Estamos usando a vers\u00e3o slim da imagem do Python 3.11, que tem tudo que precisamos para rodar nossa aplica\u00e7\u00e3o.
    2. ENV POETRY_VIRTUALENVS_CREATE=false: Define uma vari\u00e1vel de ambiente que diz ao Poetry para n\u00e3o criar um ambiente virtual. (O container j\u00e1 \u00e9 um ambiente isolado)
    3. RUN pip install poetry: Instala o Poetry, nosso gerenciador de pacotes.
    4. WORKDIR app/: Define o diret\u00f3rio em que executaremos os comandos a seguir.
    5. COPY . .: Copia todos os arquivos do diret\u00f3rio atual para o cont\u00eainer.
    6. RUN poetry config installer.max-workers 10: Configura o Poetry para usar at\u00e9 10 workers ao instalar pacotes.
    7. RUN poetry install --no-interaction --no-ansi: Instala as depend\u00eancias do nosso projeto sem intera\u00e7\u00e3o e sem cores no output.
    8. EXPOSE 8000: Informa ao Docker que o cont\u00eainer vai escutar na porta 8000.
    9. CMD [ \"poetry\", \"run\", \"uvicorn\", \"--host\", \"0.0.0.0\", \"fast_zero.app:app\" ]: Define o comando que ser\u00e1 executado quando o cont\u00eainer for iniciado.

    Vamos entender melhor esse \u00faltimo comando:

    • 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. Especificamente, \"0.0.0.0\" \u00e9 um endere\u00e7o IP que permite que o servidor aceite conex\u00f5es de qualquer endere\u00e7o de rede dispon\u00edvel, tornando-o acess\u00edvel externamente.
    • 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/#executando-o-container","title":"Executando o container","text":"

    Para executar o cont\u00eainer, usamos o comando docker run. Especificamos o nome do cont\u00eainer com a flag --name, indicamos a imagem que queremos executar e a tag que queremos usar <nome_da_imagem>:<tag>. A flag -p serve para mapear a porta do host para a porta do cont\u00eainer <porta_do_host>:<porta_do_cont\u00eainer>. Portanto, teremos o seguinte comando:

    $ Execu\u00e7\u00e3o no terminal!
    docker run --name fastzeroapp -p 8000:8000 fast_zero:latest\n

    Este comando iniciar\u00e1 nossa aplica\u00e7\u00e3o dentro de um cont\u00eainer Docker, que estar\u00e1 escutando na porta 8000. Para testar se tudo est\u00e1 funcionando corretamente, voc\u00ea pode acessar http://localhost:8000 em um navegador ou usar um comando como:

    $ Execu\u00e7\u00e3o no terminal!
    curl http://localhost:8000\n
    Caso voc\u00ea fique preso no terminal

    Caso voc\u00ea tenha a aplica\u00e7\u00e3o travada no terminal e n\u00e3o consiga sair, voc\u00ea pode teclar Ctrl+C para parar a execu\u00e7\u00e3o do container.

    "},{"location":"09/#gerenciando-containers-docker","title":"Gerenciando Containers docker","text":"

    Quando voc\u00ea trabalha com Docker, \u00e9 importante saber como gerenciar os cont\u00eaineres. Aqui est\u00e3o algumas opera\u00e7\u00f5es b\u00e1sicas para gerenci\u00e1-los:

    1. Rodar um cont\u00eainer em background: Se voc\u00ea deseja executar o cont\u00eainer em segundo plano para que n\u00e3o ocupe o terminal, pode usar a op\u00e7\u00e3o -d:

      $ Execu\u00e7\u00e3o no terminal!
      docker run -d --name fastzeroapp -p 8000:8000 fast_zero:latest\n
    2. Parar um cont\u00eainer: Quando voc\u00ea \"para\" um cont\u00eainer, est\u00e1 essencialmente interrompendo a execu\u00e7\u00e3o do processo principal do cont\u00eainer. Isso significa que o cont\u00eainer n\u00e3o est\u00e1 mais ativo, mas ainda existe no sistema, junto com seus dados associados e configura\u00e7\u00e3o. Isso permite que voc\u00ea reinicie o cont\u00eainer posteriormente, se desejar.

      $ Execu\u00e7\u00e3o no terminal!
      docker stop fastzeroapp\n
    3. Remover um cont\u00eainer: Ao \"remover\" um cont\u00eainer, voc\u00ea est\u00e1 excluindo o cont\u00eainer do sistema. Isso significa que todos os dados associados ao cont\u00eainer s\u00e3o apagados. Uma vez que um cont\u00eainer \u00e9 removido, voc\u00ea n\u00e3o pode reinici\u00e1-lo; no entanto, voc\u00ea pode sempre criar um novo cont\u00eainer a partir da mesma imagem.

      $ Execu\u00e7\u00e3o no terminal!
      docker rm fastzeroapp\n

    Ambos os comandos (stop e rm) usam o nome do cont\u00eainer que definimos anteriormente com a flag --name. \u00c9 uma boa pr\u00e1tica manter a gest\u00e3o dos seus cont\u00eaineres, principalmente durante o desenvolvimento, para evitar um uso excessivo de recursos ou conflitos de nomes e portas.

    "},{"location":"09/#introduzindo-o-postgresql","title":"Introduzindo o postgreSQL","text":"

    O PostgreSQL \u00e9 um Sistema de Gerenciamento de Banco de Dados Objeto-Relacional (ORDBMS) poderoso e de c\u00f3digo aberto. Ele \u00e9 amplamente utilizado em produ\u00e7\u00e3o em muitos projetos devido \u00e0 sua robustez, escalabilidade e conjunto de recursos extensos.

    Mudar para um banco de dados como PostgreSQL tem v\u00e1rios benef\u00edcios:

    • Escalabilidade: SQLite n\u00e3o \u00e9 ideal para aplica\u00e7\u00f5es em larga escala ou com grande volume de dados. PostgreSQL foi projetado para lidar com uma grande quantidade de dados e requisi\u00e7\u00f5es.
    • Concorr\u00eancia: Diferentemente do SQLite, que tem limita\u00e7\u00f5es para grava\u00e7\u00f5es simult\u00e2neas, o PostgreSQL suporta m\u00faltiplas opera\u00e7\u00f5es simult\u00e2neas.
    • Funcionalidades avan\u00e7adas: PostgreSQL vem com v\u00e1rias extens\u00f5es e funcionalidades que o SQLite pode n\u00e3o oferecer.

    Al\u00e9m disso, SQLite tem algumas limita\u00e7\u00f5es que podem torn\u00e1-lo inadequado para produ\u00e7\u00e3o em alguns casos. Por exemplo, ele n\u00e3o suporta alta concorr\u00eancia e pode ter problemas de performance com grandes volumes de dados.

    Nota

    Embora para o escopo da nossa aplica\u00e7\u00e3o e os objetivos de aprendizado o SQLite pudesse ser suficiente, \u00e9 sempre bom nos prepararmos para cen\u00e1rios de produ\u00e7\u00e3o real. A ado\u00e7\u00e3o de PostgreSQL nos d\u00e1 uma pr\u00e9via das pr\u00e1ticas do mundo real e garante que nossa aplica\u00e7\u00e3o possa escalar sem grandes modifica\u00e7\u00f5es de infraestrutura.

    "},{"location":"09/#como-executar-o-postgres","title":"Como executar o postgres?","text":"

    Embora o PostgreSQL seja poderoso, sua instala\u00e7\u00e3o direta em uma m\u00e1quina real pode ser desafiadora e pode resultar em configura\u00e7\u00f5es diferentes entre os ambientes de desenvolvimento. Felizmente, podemos utilizar o Docker para resolver esse problema. No Docker Hub, est\u00e3o dispon\u00edveis imagens pr\u00e9-constru\u00eddas do PostgreSQL, permitindo-nos executar o PostgreSQL com um \u00fanico comando. Confira a imagem oficial do PostgreSQL.

    Para executar um cont\u00eainer do PostgreSQL, use o seguinte comando:

    $ Execu\u00e7\u00e3o no terminal!
    docker run -d \\\n    --name app_database \\\n    -e POSTGRES_USER=app_user \\\n    -e POSTGRES_DB=app_db \\\n    -e POSTGRES_PASSWORD=app_password \\\n    -p 5432:5432 \\\n    postgres\n
    "},{"location":"09/#explicando-as-flags-e-configuracoes","title":"Explicando as Flags e Configura\u00e7\u00f5es","text":"
    • Flag -e:

    Esta flag \u00e9 usada para definir vari\u00e1veis de ambiente no cont\u00eainer. No contexto do PostgreSQL, essas vari\u00e1veis s\u00e3o essenciais. Elas configuram o nome de usu\u00e1rio, nome do banco de dados, e senha durante a primeira execu\u00e7\u00e3o do cont\u00eainer. Sem elas, o PostgreSQL pode n\u00e3o iniciar da forma esperada. \u00c9 uma forma pr\u00e1tica de configurar o PostgreSQL sem interagir manualmente ou criar arquivos de configura\u00e7\u00e3o.

    • Porta 5432:

    O PostgreSQL, por padr\u00e3o, escuta por conex\u00f5es na porta 5432. Mapeando esta porta do cont\u00eainer para a mesma porta no host (usando -p), fazemos com que o PostgreSQL seja acess\u00edvel nesta porta na m\u00e1quina anfitri\u00e3, permitindo que outras aplica\u00e7\u00f5es se conectem a ele.

    Sobre as vari\u00e1veis

    Os valores acima (app_user, app_db, e app_password) s\u00e3o padr\u00f5es gen\u00e9ricos para facilitar a inicializa\u00e7\u00e3o do PostgreSQL em um ambiente de desenvolvimento. No entanto, \u00e9 altamente recomend\u00e1vel que voc\u00ea altere esses valores, especialmente app_password, para garantir a seguran\u00e7a do seu banco de dados.

    "},{"location":"09/#volumes-e-persistencia-de-dados","title":"Volumes e Persist\u00eancia de Dados","text":"

    Para garantir a persist\u00eancia dos dados entre execu\u00e7\u00f5es do cont\u00eainer, utilizamos volumes. Um volume mapeia um diret\u00f3rio do sistema host para um diret\u00f3rio no cont\u00eainer. Isso \u00e9 crucial para bancos de dados, pois sem um volume, ao remover o cont\u00eainer, todos os dados armazenados dentro dele se perderiam.

    No PostgreSQL, o diret\u00f3rio padr\u00e3o para armazenamento de dados \u00e9 /var/lib/postgresql/data. Mapeamos esse diret\u00f3rio para um volume (neste caso \"pgdata\") em nossa m\u00e1quina host para garantir a persist\u00eancia dos dados:

    $ Execu\u00e7\u00e3o no terminal!
    docker run -d \\\n    --name app_database \\\n    -e POSTGRES_USER=app_user \\\n    -e POSTGRES_DB=app_db \\\n    -e POSTGRES_PASSWORD=app_password \\\n    -v pgdata:/var/lib/postgresql/data \\\n    -p 5432:5432 \\\n    postgres\n

    O par\u00e2metro do volume \u00e9 passado ao cont\u00eainer usando o par\u00e2metro -v Dessa forma, os dados do banco continuar\u00e3o existindo, mesmo que o cont\u00eainer seja reiniciado ou removido.

    "},{"location":"09/#adicionando-o-suporte-ao-postgresql-na-nossa-aplicacao","title":"Adicionando o suporte ao PostgreSQL na nossa aplica\u00e7\u00e3o","text":"

    Para que o SQLAlchemy suporte o PostgreSQL, precisamos instalar uma depend\u00eancia chamada psycopg2-binary. Este \u00e9 o adaptador PostgreSQL para Python e \u00e9 crucial para fazer a comunica\u00e7\u00e3o.

    Para instalar essa depend\u00eancia, utilize o seguinte comando:

    $ Execu\u00e7\u00e3o no terminal!
    poetry add psycopg2-binary\n

    Uma das vantagens do SQLAlchemy enquanto ORM \u00e9 a flexibilidade. Com apenas algumas altera\u00e7\u00f5es m\u00ednimas, como a atualiza\u00e7\u00e3o da string de conex\u00e3o, podemos facilmente transicionar para um banco de dados diferente. Assim, ap\u00f3s ajustar o arquivo .env com a string de conex\u00e3o do PostgreSQL, a aplica\u00e7\u00e3o dever\u00e1 operar normalmente, mas desta vez utilizando o PostgreSQL.

    Para ajustar a conex\u00e3o com o PostgreSQL, modifique seu arquivo .env para incluir a seguinte string de conex\u00e3o:

    .env
    DATABASE_URL=\"postgresql://app_user:app_password@localhost:5432/app_db\"\n

    Caso tenha alterado as vari\u00e1veis de ambiente do cont\u00eainer

    Se voc\u00ea alterou app_user, app_password ou app_db ao inicializar o cont\u00eainer PostgreSQL, garanta que esses valores sejam refletidos na string de conex\u00e3o acima. A palavra localhost indica que o banco de dados PostgreSQL est\u00e1 sendo executado na mesma m\u00e1quina que sua aplica\u00e7\u00e3o. Se o banco de dados estiver em uma m\u00e1quina diferente, substitua localhost pelo endere\u00e7o IP correspondente e, se necess\u00e1rio, ajuste a porta 5432.

    "},{"location":"09/#executando-as-migracoes","title":"Executando as migra\u00e7\u00f5es","text":"

    Migra\u00e7\u00f5es s\u00e3o como vers\u00f5es para seu banco de dados, permitindo que voc\u00ea atualize sua estrutura de forma ordenada e controlada. Sempre que mudamos de banco de dados, ou at\u00e9 mesmo quando alteramos sua estrutura, as migra\u00e7\u00f5es precisam ser executadas para garantir que a base de dados esteja em sincronia com nosso c\u00f3digo.

    No contexto de cont\u00eaineres, rodar as migra\u00e7\u00f5es se torna ainda mais simples. Quando mudamos de banco de dados, como \u00e9 o caso de termos sa\u00eddo de um SQLite (por exemplo) para um PostgreSQL, as migra\u00e7\u00f5es s\u00e3o essenciais. O motivo \u00e9 simples: o novo banco de dados n\u00e3o ter\u00e1 a estrutura e os dados do antigo, a menos que migremos. As migra\u00e7\u00f5es ir\u00e3o garantir que o novo banco de dados tenha a mesma estrutura e rela\u00e7\u00f5es que o anterior.

    Antes de executar o proximo comando

    Assegure-se de que ambos os cont\u00eaineres, tanto da aplica\u00e7\u00e3o quanto do banco de dados, estejam ativos. O cont\u00eainer do banco de dados deve estar rodando para que a aplica\u00e7\u00e3o possa se conectar a ele.

    Assegure-se de que o cont\u00eainer da aplica\u00e7\u00e3o esteja ativo. Estamos usando a flag --network=host para que o cont\u00eainer use a rede do host. Isso pode ser essencial para evitar problemas de conex\u00e3o, j\u00e1 que n\u00e3o podemos prever como est\u00e1 configurada a rede do computador onde este comando ser\u00e1 executado.

    execu\u00e7\u00e3o no terminal
    docker run -d --network=host --name fastzeroapp -p 8000:8000 fast_zero:latest\n

    Para aplicar migra\u00e7\u00f5es em um ambiente com cont\u00eaineres, frequentemente temos comandos espec\u00edficos associados ao servi\u00e7o. Vejamos como executar migra\u00e7\u00f5es usando o Docker:

    $ Execu\u00e7\u00e3o no terminal!
    docker exec -it fastzeroapp poetry run alembic upgrade head\n

    O comando docker exec \u00e9 usado para invocar um comando espec\u00edfico dentro de um cont\u00eainer em execu\u00e7\u00e3o. A op\u00e7\u00e3o -it \u00e9 uma combina\u00e7\u00e3o de -i (interativo) e -t (pseudo-TTY), que juntas garantem um terminal interativo, permitindo a comunica\u00e7\u00e3o direta com o cont\u00eainer.

    Ap\u00f3s executar as migra\u00e7\u00f5es, voc\u00ea pode verificar a cria\u00e7\u00e3o das tabelas utilizando um sistema de gerenciamento de banco de dados. A seguir, apresentamos um exemplo com o Beekeeper Studio:

    Lembre-se: Embora as tabelas estejam agora criadas e estruturadas, o banco de dados ainda n\u00e3o cont\u00e9m os dados anteriormente presentes no SQLite ou em qualquer outro banco que voc\u00ea estivesse utilizando antes.

    "},{"location":"09/#simplificando-nosso-fluxo-com-docker-compose","title":"Simplificando nosso fluxo com docker-compose","text":"

    Docker Compose \u00e9 uma ferramenta que permite definir e gerenciar aplicativos multi-cont\u00eainer com Docker. \u00c9 como se voc\u00ea tivesse um maestro conduzindo uma orquestra: o maestro (ou Docker Compose) garante que todos os m\u00fasicos (ou cont\u00eaineres) toquem em harmonia. Definimos nossa aplica\u00e7\u00e3o e servi\u00e7os relacionados, como o PostgreSQL, em um arquivo docker-compose.yml e os gerenciamos juntos atrav\u00e9s de comandos simplificados.

    Ao adotar o Docker Compose, facilitamos o desenvolvimento e a execu\u00e7\u00e3o da nossa aplica\u00e7\u00e3o com seus servi\u00e7os dependentes utilizando um \u00fanico comando.

    "},{"location":"09/#criacao-do-docker-composeyml","title":"Cria\u00e7\u00e3o do docker-compose.yml","text":"docker-compose.yaml
    version: '3'\n\nservices:\n  fastzero_database:\n    image: postgres\n    volumes:\n      - pgdata:/var/lib/postgresql/data\n    environment:\n      POSTGRES_USER: app_user\n      POSTGRES_DB: app_db\n      POSTGRES_PASSWORD: app_password\n    ports:\n      - \"5432:5432\"\n\n  fastzero_app:\n    image: fastzero_app\n    build:\n      context: .\n      dockerfile: Dockerfile\n    ports:\n      - \"8000:8000\"\n    depends_on:\n      - fastzero_database\n    environment:\n      DATABASE_URL: postgresql://app_user:app_password@fastzero_database:5432/app_db\n\nvolumes:\n  pgdata:\n

    Explica\u00e7\u00e3o linha a linha:

    1. version: '3': Especifica a vers\u00e3o do formato do arquivo Compose. O n\u00famero '3' \u00e9 uma das vers\u00f5es mais recentes e amplamente usadas.

    2. services:: Define os servi\u00e7os (cont\u00eaineres) que ser\u00e3o gerenciados.

    3. fastzero_database:: Define nosso servi\u00e7o de banco de dados PostgreSQL.

    4. image: postgres: Usa a imagem oficial do PostgreSQL.

    5. volumes:: Mapeia volumes para persist\u00eancia de dados.

    6. pgdata:/var/lib/postgresql/data: Cria ou usa um volume chamado \"pgdata\" e o mapeia para o diret\u00f3rio /var/lib/postgresql/data no cont\u00eainer.

    7. environment:: Define vari\u00e1veis de ambiente para o servi\u00e7o.

    8. fastzero_app:: Define o servi\u00e7o para nossa aplica\u00e7\u00e3o.

    9. image: fastzero_app: Usa a imagem Docker da nossa aplica\u00e7\u00e3o.

    10. build: : Instru\u00e7\u00f5es para construir a imagem se n\u00e3o estiver dispon\u00edvel, nosso Dockerfile.

    11. ports:: Mapeia portas do cont\u00eainer para o host.

    12. \"8000:8000\": Mapeia a porta 8000 do cont\u00eainer para a porta 8000 do host.

    13. depends_on:: Especifica que fastzero_app depende de fastzero_database. Isto garante que o banco de dados seja iniciado antes da aplica\u00e7\u00e3o.

    14. DATABASE_URL: ...: \u00c9 uma vari\u00e1vel de ambiente que nossa aplica\u00e7\u00e3o usar\u00e1 para se conectar ao banco de dados. Aqui, ele se conecta ao servi\u00e7o fastzero_database que definimos anteriormente.

    15. volumes: (n\u00edvel superior): Define volumes que podem ser usados pelos servi\u00e7os.

    16. pgdata:: Define um volume chamado \"pgdata\". Este volume \u00e9 usado para persistir os dados do PostgreSQL entre as execu\u00e7\u00f5es do cont\u00eainer.

    Sobre o docker-compose

    Para usar o Docker Compose, voc\u00ea precisa t\u00ea-lo instalado em seu sistema. Ele n\u00e3o est\u00e1 inclu\u00eddo na instala\u00e7\u00e3o padr\u00e3o do Docker, ent\u00e3o lembre-se de instal\u00e1-lo separadamente!

    O guia oficial de instala\u00e7\u00e3o pode ser encontrado aqui

    Com este arquivo docker-compose.yml, voc\u00ea pode iniciar ambos os servi\u00e7os (aplica\u00e7\u00e3o e banco de dados) simultaneamente usando:

    docker-compose up\n

    Para parar os servi\u00e7os e manter os dados seguros nos volumes definidos, use:

    docker-compose down\n

    Esses comandos simplificam o fluxo de trabalho e garantem que os servi\u00e7os iniciem corretamente e se comuniquem conforme o esperado.

    Execu\u00e7\u00e3o em modo desanexado

    Voc\u00ea pode iniciar os servi\u00e7os em segundo plano com a flag -d usando docker-compose up -d. Isso permite que os cont\u00eaineres rodem em segundo plano, liberando o terminal para outras tarefas.

    "},{"location":"09/#rodando-as-migracoes-de-forma-automatica","title":"Rodando as migra\u00e7\u00f5es de forma autom\u00e1tica","text":"

    Automatizar as migra\u00e7\u00f5es do banco de dados \u00e9 uma pr\u00e1tica recomendada para garantir que sua aplica\u00e7\u00e3o esteja sempre sincronizada com o estado mais atual do seu esquema de banco de dados. \u00c9 como preparar todos os ingredientes antes de come\u00e7ar a cozinhar: voc\u00ea garante que tudo o que \u00e9 necess\u00e1rio est\u00e1 pronto para ser usado.

    Para automatizar as migra\u00e7\u00f5es em nossos cont\u00eaineres Docker, utilizamos um entrypoint. O entrypoint define o comando que ser\u00e1 executado quando o cont\u00eainer iniciar. Em outras palavras, \u00e9 o primeiro ponto de entrada de execu\u00e7\u00e3o do cont\u00eainer.

    Por que usar o Entrypoint?

    No Docker, o entrypoint permite que voc\u00ea configure um ambiente de cont\u00eainer que ser\u00e1 executado como um execut\u00e1vel. \u00c9 \u00fatil para preparar o ambiente, como realizar migra\u00e7\u00f5es de banco de dados, antes de iniciar a aplica\u00e7\u00e3o propriamente dita. Isso significa que qualquer comando definido no CMD do Dockerfile n\u00e3o ser\u00e1 executado automaticamente se um entrypoint estiver definido. Em vez disso, precisamos incluir explicitamente esse comando no script de entrypoint.

    Implementando o Entrypoint

    Criamos um script chamado entrypoint.sh que ir\u00e1 preparar nosso ambiente antes de a aplica\u00e7\u00e3o iniciar:

    entrypoin.sh
    #!/bin/sh\n\n# Executa as migra\u00e7\u00f5es do banco de dados\npoetry run alembic upgrade head\n\n# Inicia a aplica\u00e7\u00e3o\npoetry run uvicorn --host 0.0.0.0 --port 8000 fast_zero.app:app\n

    Explica\u00e7\u00e3o Detalhada do Script:

    • #!/bin/sh: Indica ao sistema operacional que o script deve ser executado no shell Unix.
    • poetry run alembic upgrade head: Roda as migra\u00e7\u00f5es do banco de dados at\u00e9 a \u00faltima vers\u00e3o.
    • poetry run uvicorn --host 0.0.0.0 --port 8000 fast_zero.app:app: Inicia a aplica\u00e7\u00e3o. Este \u00e9 o comando que normalmente estaria no CMD do Dockerfile, mas agora est\u00e1 inclu\u00eddo no entrypoint para garantir que as migra\u00e7\u00f5es sejam executadas antes do servidor iniciar.

    Como Funciona na Pr\u00e1tica?

    Quando o cont\u00eainer \u00e9 iniciado, o Docker executa o script de entrypoint, que por sua vez executa as migra\u00e7\u00f5es e s\u00f3 ent\u00e3o inicia a aplica\u00e7\u00e3o. Isso garante que o banco de dados esteja atualizado com as \u00faltimas migra\u00e7\u00f5es antes de qualquer intera\u00e7\u00e3o com a aplica\u00e7\u00e3o.

    Visualizando o Processo:

    Voc\u00ea pode pensar no entrypoint.sh como o ato de aquecer e verificar todos os instrumentos antes de uma apresenta\u00e7\u00e3o musical. Antes de a m\u00fasica come\u00e7ar, cada instrumento \u00e9 afinado e testado. Da mesma forma, nosso script assegura que o banco de dados est\u00e1 em harmonia com a aplica\u00e7\u00e3o antes de ela come\u00e7ar a receber requisi\u00e7\u00f5es.

    Adicionando o Entrypoint ao Docker Compose:

    Inclu\u00edmos o entrypoint no nosso servi\u00e7o no arquivo docker-compose.yml, garantindo que esteja apontando para o script correto:

    docker-compose.yaml
      fastzero_app:\n    image: fastzero_app\n    entrypoint: ./entrypoint.sh\n    build:\n      context: .\n      dockerfile: Dockerfile\n

    Reconstruindo e Executando com Novas Configura\u00e7\u00f5es:

    Para aplicar as altera\u00e7\u00f5es, reconstru\u00edmos e executamos os servi\u00e7os com a op\u00e7\u00e3o --build:

    $ Execu\u00e7\u00e3o no terminal!
    docker-compose up --build\n

    Observando o Comportamento Esperado:

    Quando o cont\u00eainer \u00e9 iniciado, voc\u00ea deve ver as migra\u00e7\u00f5es sendo aplicadas, seguidas pela inicializa\u00e7\u00e3o da aplica\u00e7\u00e3o:

    $ Exemplo do resultado no terminal!
    fastzero_app-1  | INFO  [alembic.runtime.migration] Context impl PostgresqlImpl.\nfastzero_app-1  | INFO  [alembic.runtime.migration] Will assume transactional DDL.\nfastzero_app-1  | INFO:     Started server process [10]\nfastzero_app-1  | INFO:     Waiting for application startup.\nfastzero_app-1  | INFO:     Application startup complete.\nfastzero_app-1  | INFO:     Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)\n

    Este processo garante que as migra\u00e7\u00f5es do banco de dados s\u00e3o realizadas automaticamente, mantendo a base de dados alinhada com a aplica\u00e7\u00e3o e pronta para a\u00e7\u00e3o assim que o servidor Uvicorn entra em cena.

    Nota de revis\u00e3o sobre vari\u00e1veis de ambiente

    Utilizar vari\u00e1veis de ambiente definidas em um arquivo .env \u00e9 uma pr\u00e1tica recomendada para cen\u00e1rios de produ\u00e7\u00e3o devido \u00e0 seguran\u00e7a que oferece. No entanto, para manter a simplicidade e o foco nas funcionalidades do FastAPI neste curso, optamos por explicitar essas vari\u00e1veis no docker-compose.yml. Isso \u00e9 particularmente relevante, pois o Docker Compose \u00e9 utilizado apenas para o ambiente de desenvolvimento; no deploy para fly.io, o qual \u00e9 o nosso foco, o compose n\u00e3o ser\u00e1 utilizado em produ\u00e7\u00e3o.

    Ainda assim, \u00e9 valioso mencionar como essa configura\u00e7\u00e3o mais segura seria realizada, especialmente para aqueles que planejam utilizar o Docker Compose em produ\u00e7\u00e3o.

    Em ambientes de produ\u00e7\u00e3o com Docker Compose, \u00e9 uma boa pr\u00e1tica gerenciar vari\u00e1veis de ambiente sens\u00edveis, como credenciais, por meio de um arquivo .env. Isso previne a exposi\u00e7\u00e3o dessas informa\u00e7\u00f5es diretamente no arquivo docker-compose.yml, contribuindo para a seguran\u00e7a do projeto.

    As vari\u00e1veis de ambiente podem ser definidas em nosso arquivo .env localizado na raiz do projeto:

    .env
    POSTGRES_USER=app_user\nPOSTGRES_DB=app_db\nPOSTGRES_PASSWORD=app_password\nDATABASE_URL=postgresql://app_user:app_password@fastzero_database:5432/app_db\n

    Para aplicar essas vari\u00e1veis, referencie o arquivo .env no docker-compose.yml:

    docker-compose.yaml
    services:\n  fastzero_database:\n    image: postgres\n    env_file:\n      - .env\n    # Restante da configura\u00e7\u00e3o...\n\n  fastzero_app:\n    build: .\n    env_file:\n      - .env\n    # Restante da configura\u00e7\u00e3o...\n

    Adotar essa abordagem evita a exposi\u00e7\u00e3o das vari\u00e1veis de ambiente no arquivo de configura\u00e7\u00e3o. Esta n\u00e3o foi a abordagem padr\u00e3o no curso devido \u00e0 complexidade adicional e \u00e0 inten\u00e7\u00e3o de evitar confus\u00f5es. Dependendo do ambiente estabelecido pela equipe de DevOps/SRE em um projeto real, essa gest\u00e3o pode variar entre vari\u00e1veis de ambiente, arquivos .env ou solu\u00e7\u00f5es mais avan\u00e7adas como Vault.{:target=\"_blank\"}.

    Se optar por utilizar um arquivo .env com as configura\u00e7\u00f5es do PostgreSQL, configure o Pydantic para ignorar vari\u00e1veis de ambiente que n\u00e3o s\u00e3o necess\u00e1rias, adicionando extra='ignore' a chamada de SettingsConfigDic:

    fast_zero/settings.py
    from pydantic_settings import BaseSettings, SettingsConfigDict\n\n\nclass Settings(BaseSettings):\n    model_config = SettingsConfigDict(\n        env_file='.env', env_file_encoding='utf-8', extra='ignore'\n    )\n\n    DATABASE_URL: str\n    SECRET_KEY: str\n    ALGORITHM: str\n    ACCESS_TOKEN_EXPIRE_MINUTES: int\n

    Com essa configura\u00e7\u00e3o, o Pydantic ir\u00e1 ignorar quaisquer vari\u00e1veis no .env que n\u00e3o sejam explicitamente declaradas na classe Settings, evitando assim conflitos e erros inesperados.

    Agradecimentos especiais a @vcwild e @williangl pelas revis\u00f5es valiosas nesta aula que me fizeram criar essa nota.

    "},{"location":"09/#testes-com-docker","title":"Testes com Docker","text":"

    Agora que temos o docker-compose configurado, realizar testes tornou-se uma tarefa simplificada. Podemos executar toda a su\u00edte de testes com um \u00fanico comando, sem a necessidade de ajustes adicionais ou configura\u00e7\u00f5es complexas. Isso \u00e9 poss\u00edvel devido \u00e0 maneira como o docker-compose gerencia os servi\u00e7os e suas depend\u00eancias.

    Para executar os testes, utilizamos o comando:

    $ Execu\u00e7\u00e3o no terminal!
    docker-compose run --entrypoint=\"poetry run task test\" fastzero_app\n

    Vamos entender melhor o que cada parte do comando faz:

    • docker-compose run: Este comando executa um servi\u00e7o \u00fanico definido no seu arquivo docker-compose.yml. Ao contr\u00e1rio do docker-compose up, que inicia todos os servi\u00e7os, o run permite que voc\u00ea inicie um servi\u00e7o espec\u00edfico.

    • --entrypoint: A flag --entrypoint substitui o ponto de entrada padr\u00e3o do container. O ponto de entrada padr\u00e3o \u00e9 especificado no Dockerfile ou no docker-compose.yml e normalmente \u00e9 o comando que inicia sua aplica\u00e7\u00e3o. Ao substituir o entrypoint, voc\u00ea pode executar comandos diferentes, como neste caso, onde queremos rodar nossos testes.

    • \"poetry run task test\": Este \u00e9 o comando que substituir\u00e1 o entrypoint padr\u00e3o. Ele diz ao Docker para iniciar o container e executar a nossa su\u00edte de testes com o Poetry, uma ferramenta para gerenciamento de depend\u00eancias e pacotes em Python.

    • fastzero_app: Este \u00e9 o nome do servi\u00e7o que definimos no docker-compose.yml. \u00c9 o servi\u00e7o que cont\u00e9m a nossa aplica\u00e7\u00e3o FastAPI e onde nossos testes ser\u00e3o executados.

    Ao utilizar esse comando, o Docker Compose cuidar\u00e1 de iniciar os servi\u00e7os dos quais fastzero_app depende, neste caso, o servi\u00e7o fastzero_database do PostgreSQL. Isso \u00e9 importante porque nossos testes podem depender de um banco de dados ativo para funcionar corretamente. O Compose garante que a ordem de inicializa\u00e7\u00e3o dos servi\u00e7os seja respeitada e que o servi\u00e7o do banco de dados esteja pronto antes de iniciar os testes.

    Se executarmos o comando podemos ver que ele inicia o banco de dados, inicia o container da aplica\u00e7\u00e3o e na sequ\u00eancia executa o comando que passamos no --entreypoint que \u00e9 exatamente como executar os testes:

    $ Execu\u00e7\u00e3o no terminal!
    docker-compose run --entrypoint=\"poetry run task test\" fastzero_app\n\n# Resulado esperado\n[+] Building 0.0s (0/0)                                           docker:default\n[+] Creating 2/2\n \u2714 Network default                Created                      0.1s \n \u2714 Container fastzero_database-1  Created                      0.1s \n[+] Running 1/1\n \u2714 Container fastzero_database-1  Started                      0.3s \n[+] Building 0.0s (0/0)                                           docker:default\nAll done! \u2728 \ud83c\udf70 \u2728\n18 files would be left unchanged.\n================ test session starts ================\nplatform linux - Python 3.11.6, pytest-7.4.3, pluggy-1.3.0 - /app/.venv/bin/python\ncachedir: .pytest_cache\nrootdir: /app\nconfigfile: pyproject.toml\nplugins: cov-4.1.0, anyio-4.1.0, Faker-20.1.0\ncollected 27 items\n\ntests/test_app.py::test_root_deve_retornar_200_e_ola_mundo PASSED\n...\n

    \u00c9 importante notar que, embora o docker-compose run inicie as depend\u00eancias necess\u00e1rias para a execu\u00e7\u00e3o do servi\u00e7o especificado, ele n\u00e3o finaliza essas depend\u00eancias ap\u00f3s a conclus\u00e3o do comando. Isso significa que ap\u00f3s a execu\u00e7\u00e3o dos testes, o servi\u00e7o do banco de dados continuar\u00e1 ativo. Voc\u00ea precisar\u00e1 finaliz\u00e1-lo manualmente com docker-compose down para encerrar todos os servi\u00e7os e limpar o ambiente:

    $ Execu\u00e7\u00e3o no terminal!
    docker-compose down\n[+] Running 2/2\n \u2714 Container 09-fastzero_database-1  Removed    0.4s \n \u2714 Network 09_default                Removed\n

    Assim tendo o ambiente limpo novamente.

    "},{"location":"09/#executando-os-testes-no-postgresql","title":"Executando os testes no PostgreSQL","text":"

    Embora nosso docker-compose esteja configurado para levantar o banco de dados PostgreSQL ao executar os testes, \u00e9 importante ressaltar que o container do PostgreSQL n\u00e3o est\u00e1 sendo utilizado durante a execu\u00e7\u00e3o dos testes. Isso acontece porque a fixture respons\u00e1vel por criar a sess\u00e3o do banco de dados est\u00e1 com as instru\u00e7\u00f5es \"hardcoded\" para o SQLite, como no c\u00f3digo abaixo:

    tests/conftest.py
    @pytest.fixture\ndef session():\n    engine = create_engine(\n        'sqlite:///:memory:',\n        connect_args={'check_same_thread': False},\n        poolclass=StaticPool,\n    )\n    Base.metadata.create_all(engine)\n\n    Session = sessionmaker(bind=engine)\n\n    yield Session()\n\n    Base.metadata.drop_all(engine)\n

    Por conta disso, os testes t\u00eam sido executados no SQLite, mesmo com a presen\u00e7a do PostgreSQL no ambiente do Docker.

    No entanto, \u00e9 importante que os testes sejam executados no mesmo ambiente que o que rodar\u00e1 em produ\u00e7\u00e3o, para que n\u00e3o encontremos problemas relacionados a incompatibilidade de opera\u00e7\u00f5es no banco de dados. A altera\u00e7\u00e3o \u00e9 relativamente simples, temos que tornar a nossa fixture o mais pr\u00f3ximo poss\u00edvel do cliente da sess\u00e3o de produ\u00e7\u00e3o. Para fazer isso, precisamos alterar somente a chamada create_engine para carregar a var\u00e1vel de ambiente do banco de dados de testes. Desta forma:

    tests/conftest.py
    import pytest\nfrom fastapi.testclient import TestClient\nfrom sqlalchemy import create_engine\nfrom sqlalchemy.orm import sessionmaker\n\nfrom fast_zero.app import app\nfrom fast_zero.database import get_session\nfrom fast_zero.models import Base\nfrom fast_zero.settings import Settings\nfrom fast_zero.security import get_password_hash\nfrom tests.factories import UserFactory\n\n\n@pytest.fixture\ndef session():\n    database = \n    engine = create_engine(Settings().DATABASE_URL)\n    Session = sessionmaker(autocommit=False, autoflush=False, bind=engine)\n    Base.metadata.create_all(engine)\n    with Session() as session:\n        yield session\n        session.rollback()\n\n    Base.metadata.drop_all(engine)\n

    Com essa modifica\u00e7\u00e3o, agora estamos apontando para o banco de dados PostgreSQL, conforme definido nas configura\u00e7\u00f5es da nossa aplica\u00e7\u00e3o (Settings().DATABASE_URL). A transi\u00e7\u00e3o do SQLite para o PostgreSQL \u00e9 facilitada pela abstra\u00e7\u00e3o fornecida pelo SQLAlchemy, que nos permite mudar de um banco para outro sem problemas. \u00c9 importante notar que essa flexibilidade se deve ao fato de n\u00e3o termos utilizado recursos espec\u00edficos do PostgreSQL que n\u00e3o s\u00e3o suportados pelo SQLite. Caso contr\u00e1rio, a mudan\u00e7a poderia n\u00e3o ser t\u00e3o direta.

    Agora, com a nova configura\u00e7\u00e3o, os testes utilizar\u00e3o o PostgreSQL, proporcionando um ambiente de testes mais fiel ao ambiente de produ\u00e7\u00e3o e, consequentemente, aumentando a confiabilidade dos testes executados:

    $ Execu\u00e7\u00e3o no terminal!
    docker-compose run --entrypoint=\"poetry run task test\" fastzero_app\n\n# resultado esperado\ndocker-compose run --entrypoint=\"poetry run task test\" fastzero_app\n[+] Building 0.0s (0/0)                                          docker:default\n[+] Creating 1/0\n \u2714 Container 09-fastzero_database-1  Running                     0.0s \n[+] Building 0.0s (0/0)                                          docker:default\nAll done! \u2728 \ud83c\udf70 \u2728\n18 files would be left unchanged.\n======================= test session starts =======================\nplatform linux - Python 3.11.6, pytest-7.4.3, pluggy-1.3.0 - /app/.venv/bin/python\ncachedir: .pytest_cache\nrootdir: /app\nconfigfile: pyproject.toml\nplugins: cov-4.1.0, anyio-4.1.0, Faker-20.1.0\ncollected 27 items\n\ntests/test_app.py::test_root_deve_retornar_200_e_ola_mundo PASSED\n

    Dessa forma temos um ambiente mais coeso e podemos reproduzir nossas configura\u00e7\u00f5es de forma bastante simples em qualquer ambiente.

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

    Ap\u00f3s criar nosso arquivo Dockerfile e docker-compose.yaml, executar os testes e construir nosso ambiente, podemos fazer o commit das altera\u00e7\u00f5es no Git:

    1. Adicionando todos os arquivos modificados nessa aula com git add .
    2. Fa\u00e7a o commit das altera\u00e7\u00f5es com git commit -m \"Dockerizando nossa aplica\u00e7\u00e3o e alterando os testes para serem executados no PostgreSQL\"
    3. Envie 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, junto com o PostgreSQL, nos permite garantir consist\u00eancia em diferentes ambientes. A combina\u00e7\u00e3o de Docker e Docker Compose simplifica o processo de desenvolvimento e implanta\u00e7\u00e3o. Na pr\u00f3xima aula, vamos aprender como levar nossa aplica\u00e7\u00e3o para o pr\u00f3ximo n\u00edvel executando os testes de forma remota com a integra\u00e7\u00e3o cont\u00ednua do GitHub Actions.

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

    T\u00f3pico de manuten\u00e7\u00e3o: https://github.com/dunossauro/fastapi-do-zero/issues/34

    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]\n\njobs:\n  test:\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: Copia os arquivos do repo\n        uses: actions/checkout@v3\n\n      - name: Instalar o python\n        uses: actions/setup-python@v4\n        with:\n          python-version: '3.11.1'\n\n      - name: Instalar Poetry\n        run: pip install poetry\n\n      - name: Instalar depend\u00eancias do projeto\n        run: poetry install\n\n      - name: Rodar os testes\n        run: poetry run task test --cov-report=xml\n\n      - name: Subir cobertura para o codecov\n        uses: codecov/codecov-action@v3\n        with:\n          token: ${{ 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":"

    Antes de avan\u00e7armos, \u00e9 importante mencionar uma especificidade do Fly.io: para criar uma inst\u00e2ncia do PostgreSQL, a plataforma requer que um cart\u00e3o de cr\u00e9dito seja fornecido. Esta \u00e9 uma medida de seguran\u00e7a adotada para evitar o uso indevido de seus servi\u00e7os, como a execu\u00e7\u00e3o de ferramentas de minera\u00e7\u00e3o. Apesar dessa exig\u00eancia, o servi\u00e7o de PostgreSQL \u00e9 oferecido de forma gratuita. Mais detalhes podem ser encontrados neste artigo.

    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\n  run: |\n    flyctl deploy --local-only\n    flyctl 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":"Despedida do curso","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.
    • Apresentar outros materiais criados com FastAPI.
    "},{"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/#outros-materiais-produzidos-por-mim-sobre-fastapi","title":"Outros materiais produzidos por mim sobre FastAPI","text":"

    J\u00e1 cobrimos alguns temas n\u00e3o citados neste curso usando FastAPI em Algumas Lives de Python. Voc\u00ea pode assistir para aprender mais tamb\u00e9m.

    "},{"location":"12/#templates-e-websockets","title":"Templates e WebSockets","text":"

    Na Live de Python #164 conversamos sobre websockets com Python e usamos FastAPI para exemplificar o comportamento. Durante essa live criamos uma aplica\u00e7\u00e3o de chat e usamos os templates com Jinja2 e Brython.

    "},{"location":"12/#graphql-strawberry","title":"GraphQL (Strawberry)","text":"

    Na Live de Python #185 conversamos sobre GraphQL um padr\u00e3o alternativo a REST APIs. Todos os exemplos foram aplicados usando Strawberry e FastAPI

    "},{"location":"12/#sqlmodel","title":"SQLModel","text":"

    Na Live de Python #235 conversamos sobre SQLModel um ORM alternativo ao SQLAlchemy que se integra com o Pydantic. O SQLModel tamb\u00e9m foi desenvolvido pelo Sebastian (criador do FastAPI). Caminhando ao final dessa aula, podemos ver a implementa\u00e7\u00e3o do SQLModel em uma aplica\u00e7\u00e3o b\u00e1sica com FastAPI.

    "},{"location":"12/#proximos-passos","title":"Pr\u00f3ximos passos","text":"

    [WIP]

    "},{"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":"#fastapi-do-zero","title":"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! Problemas j\u00e1 conhecidos

    Construindo um Projeto com Bancos de Dados, Testes e Deploy

    Boas-vindas \u00e0 sua jornada de aprendizado com o framework FastAPI! Neste curso, o foco \u00e9 proporcionar um entendimento pr\u00e1tico das habilidades essenciais para o desenvolvimento eficiente de APIs. Exploraremos temas como integra\u00e7\u00e3o com bancos de dados e implementa\u00e7\u00e3o de testes, oferecendo uma base s\u00f3lida para quem busca trabalhar com essa ferramenta. A abordagem \u00e9 direta e informativa, visando nos equipar com o conhecimento necess\u00e1rio para come\u00e7ar a criar nossos pr\u00f3prios projetos.

    "},{"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 foi desenvolvido para oferecer uma experi\u00eancia pr\u00e1tica no uso do FastAPI, uma das ferramentas mais modernas para constru\u00e7\u00e3o de APIs. Ao longo do curso, o objetivo \u00e9 que voc\u00ea obtenha uma compreens\u00e3o das funcionalidades do FastAPI e de boas pr\u00e1ticas associadas a ele.

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

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

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

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

    Aqui est\u00e1 uma vis\u00e3o geral dos t\u00f3picos que 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 a abordagem deste curso \u00e9 pr\u00e1tica e centrada em um projeto concreto. Atrav\u00e9s de exemplos reais e instru\u00e7\u00f5es passo a passo, voc\u00ea ter\u00e1 a oportunidade de acompanhar o processo de constru\u00e7\u00e3o de uma aplica\u00e7\u00e3o real. Mesmo que os conceitos de desenvolvimento web sejam novos para voc\u00ea, a \u00eanfase na aplica\u00e7\u00e3o pr\u00e1tica e a estrutura detalhada do curso facilitar\u00e3o o entendimento e a aplica\u00e7\u00e3o dessas habilidades 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 h\u00e1 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":"

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

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

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

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

    Perguntas frequentes que me fizeram durante 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
      • Embora a vers\u00e3o 3.7 seja suficiente para executar o FastAPI em si, neste curso usaremos recursos de Python que est\u00e3o presentes apenas a partir da vers\u00e3o 3.11. Portanto, essa \u00e9 a vers\u00e3o m\u00ednima obrigat\u00f3ria para o curso.
    4. O Poetry para gerenciar os pacotes e seu ambiente virtual (caso n\u00e3o conhe\u00e7a o poetry temos uma live de python sobre ele)
    5. Git: Para gerenciar vers\u00f5es
    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)\n  3.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\n\napp = FastAPI()\n\n@app.get('/')\ndef read_root():\n    return {'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":"

    Come\u00e7ando pelo ruff, vamos definir o comprimento de linha para 79 caracteres (conforme sugerido na PEP 8) e em seguida, informaremos que o diret\u00f3rio de ambiente virtual e o de migra\u00e7\u00f5es de banco de dados dever\u00e3o ser ignorados:

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

    Para evitar conflitos de formata\u00e7\u00e3o entre o isort e o blue, definiremos o black como perfil de formata\u00e7\u00e3o a ser seguido, j\u00e1 que o blue \u00e9 um fork dele. Como o black utiliza 88 caracteres por linha, vamos alterar para 79 que \u00e9 o padr\u00e3o que o blue segue e que tamb\u00e9m estamos seguindo:

    pyproject.toml
    [tool.isort]\nprofile = \"black\"\nline_length = 79\nextend_skip = ['migrations']\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\n[tool.poetry.dependencies]\npython = \"3.11.*\"\nfastapi = \"^0.100.0\"\nuvicorn = \"^0.22.0\"\n\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\n[tool.ruff]\nline-length = 79\nexclude = ['.venv', 'migrations']\n\n[tool.isort]\nprofile = \"black\"\nline_length = 79\nextend_skip = ['migrations']\n\n[tool.pytest.ini_options]\npythonpath = \".\"\n\n[tool.blue]\nextend-exclude = '(migrations/)'\n\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\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 @@\n from fastapi import FastAPI\n\n app = 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\n\napp = FastAPI()\n\n\n@app.get('/')\ndef read_root():\n    return {'message': 'Ol\u00e1 Mundo!'}\n
    "},{"location":"01/#introducao-ao-pytest-testando-o-hello-world","title":"Introdu\u00e7\u00e3o ao Pytest: Testando o \"Hello, World!\"","text":"

    Antes de entendermos a din\u00e2mica dos testes, precisamos entender o efeito que eles t\u00eam no nosso c\u00f3digo. 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)\n    self._warn(\"No data was collected.\", slug=\"no-data-collected\")\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      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\n\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\n\ndef test_root_deve_retornar_200_e_ola_mundo():\n    client = TestClient(app)\n\n    response = client.get('/')\n\n    assert response.status_code == 200\n    assert 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\n\ndef test_root_deve_retornar_200_e_ola_mundo():\n    client = TestClient(app)  # Arrange\n\n    response = client.get('/')  # Act\n\n    assert response.status_code == 200  # Assert\n    assert response.json() == {'message': 'Ol\u00e1 Mundo!'}  # Assert\n

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

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

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

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

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

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

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

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

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

    Antes de concluirmos a aula, precisamos 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 uma requisi\u00e7\u00e3o, a processa 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 a processa. 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 com o 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.

    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.

    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.

    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\n\n\nclass UserSchema(BaseModel):\n    username: str\n    email: EmailStr\n    password: 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]

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

    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\n\n\nclass UserSchema(BaseModel):\n    username: str\n    email: EmailStr\n    password: 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\n# C\u00f3digo da nossa rota de ol\u00e1 mundo omitido\n\n@app.post('/users/', status_code=201)\ndef create_user(user: UserSchema):\n    return 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):\n    username: str\n    email: 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\n# c\u00f3digo omitido\n\n@app.post('/users/', status_code=201, response_model=UserPublic)\ndef create_user(user: UserSchema):\n    return 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\n# c\u00f3digo omitido\n\ndatabase = []  # provis\u00f3rio para estudo!\n\n\n@app.post('/users/', status_code=201, response_model=UserPublic)\ndef create_user(user: UserSchema):\n    user_with_id = UserDB(**user.model_dump(), id=len(database) + 1)\n\n    database.append(user_with_id)\n\n    return 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):\n    id: int\n    username: str\n    email: EmailStr\n\n\nclass UserDB(UserSchema):\n    id: 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\n# parte da resposta foi omitida\n\n---------- coverage: platform linux, python 3.11.3-final-0 -----------\nName                    Stmts   Miss  Cover\n-------------------------------------------\nfast_zero/__init__.py       0      0   100%\nfast_zero/app.py           12      3    75%\nfast_zero/schemas.py       11      0   100%\n-------------------------------------------\nTOTAL                      23      3    87%\n\n# parte da resposta foi omitida\n

    Vemos que temos 3 Miss. Possivelmente das linhas que acabamos de escrever.

    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():\n    client = TestClient(app)\n\n    response = client.post(\n        '/users/',\n        json={\n            'username': 'alice',\n            'email': 'alice@example.com',\n            'password': 'secret',\n        },\n    )\n    assert response.status_code == 201\n    assert response.json() == {\n        'username': 'alice',\n        'email': 'alice@example.com',\n        'id': 1,\n    }\n

    Ao executar o teste:

    $ Execu\u00e7\u00e3o no terminal!
    task test\n\n# parte da resposta foi omitida\n\ntests/test_app.py::test_root_deve_retornar_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
    "},{"location":"02/#nao-se-repita-dry","title":"N\u00e3o se repita (DRY)","text":"

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

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

    Se fixtures s\u00e3o uma novidade para voc\u00ea

    Existe uma live de Python onde discutimos especificamente sobre fixtures

    Link direto

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

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

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

    tests/test_app.py
    # ...\n\ndef test_root_deve_retornar_200_e_ola_mundo(client):\n    response = client.get('/')\n    # ...\n\ndef test_create_user(client):\n    response = client.post(\n        '/users/',\n        json={\n            'username': 'alice',\n            'email': 'alice@example.com',\n            'password': 'secret',\n        },\n    )\n    # ...\n

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

    Vemos que estamos no caminho certo. Agora que a rota POST est\u00e1 implementada, 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):\n    users: list[UserPublic]\n

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

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

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

    "},{"location":"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(client):\n    response = client.get('/users/')\n    assert response.status_code == 200\n    assert response.json() == {\n        'users': [\n            {\n                'username': 'alice',\n                'email': 'alice@example.com',\n                'id': 1,\n            }\n        ]\n    }\n

    Com as rotas POST e GET implementadas, agora podemos criar e recuperar usu\u00e1rios. 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# ...\n\n@app.put('/users/{user_id}', response_model=UserPublic)\ndef update_user(user_id: int, user: UserSchema):\n    if user_id > len(database) or user_id < 1:\n        raise HTTPException(status_code=404, detail='User not found')\n\n    user_with_id = UserDB(**user.model_dump(), id=user_id)\n    database[user_id - 1] = user_with_id\n\n    return 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(client):\n    response = client.put(\n        '/users/1',\n        json={\n            'username': 'bob',\n            'email': 'bob@example.com',\n            'password': 'mynewpassword',\n        },\n    )\n    assert response.status_code == 200\n    assert response.json() == {\n        'username': 'bob',\n        'email': 'bob@example.com',\n        'id': 1,\n    }\n

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

    "},{"location":"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):\n    detail: 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# ...\n\n@app.delete('/users/{user_id}', response_model=Message)\ndef delete_user(user_id: int):\n    if user_id > len(database) or user_id < 1:\n        raise HTTPException(status_code=404, detail='User not found')\n\n    del database[user_id - 1]\n\n    return {'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(client):\n    response = client.delete('/users/1')\n\n    assert response.status_code == 200\n    assert response.json() == {'detail': 'User deleted'}\n
    "},{"location":"02/#checando-tudo-antes-do-commit","title":"Checando tudo antes do commit","text":"

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

    $ Execu\u00e7\u00e3o no terminal!
    $ task lint\nAll done! \u2728 \ud83c\udf70 \u2728\n5 files would be left unchanged.\n\n$ task format\nAll done! \u2728 \ud83c\udf70 \u2728\n5 files left unchanged.\nSkipped 1 files\n\n$ 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\n\n---------- coverage: platform linux, python 3.11.4-final-0 -----------\nName                   Stmts   Miss  Cover\n------------------------------------------\nfastzero/__init__.py       0      0   100%\nfastzero/app.py           28      2    93%\nfastzero/schemas.py       15      0   100%\n------------------------------------------\nTOTAL                     43      2    95%\n\n\n============================================ 5 passed in 1.48s =============================================\nWrote HTML report to htmlcov/index.html\n
    "},{"location":"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-os-12-fatores","title":"Configura\u00e7\u00f5es de ambiente e os 12 fatores","text":"

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

    Caso queira saber mais sobre 12 fatores

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

    Link direto

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

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

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

    Agora que entendemos melhor esses conceitos, 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\n\n\nclass Base(DeclarativeBase):\n    pass\n\n\nclass User(Base):\n    __tablename__ = 'users'\n\n    id: Mapped[int] = mapped_column(primary_key=True)\n    username: Mapped[str]\n    password: Mapped[str]\n    email: 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 entre eles.

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

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

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

    "},{"location":"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]

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

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

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

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

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

    tests/conftest.py
    # ...\nimport pytest\nfrom sqlalchemy import create_engine\nfrom sqlalchemy.orm import sessionmaker\n\nfrom fast_zero.models import Base\n\n# ...\n\n\n@pytest.fixture\ndef session():\n    engine = create_engine('sqlite:///:memory:')\n    Session = sessionmaker(bind=engine)\n    Base.metadata.create_all(engine)\n    yield Session()\n    Base.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:'): cria um mecanismo de banco de dados SQLite em mem\u00f3ria usando SQLAlchemy. Este mecanismo ser\u00e1 usado para criar uma sess\u00e3o de banco de dados para nossos testes.

    2. Session = sessionmaker(bind=engine): cria uma f\u00e1brica de sess\u00f5es para criar sess\u00f5es de banco de dados para nossos testes.

    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\n\nfrom fast_zero.models import User\n\n\ndef test_create_user(session):\n    new_user = User(username='alice', password='secret', email='teste@test')\n    session.add(new_user)\n    session.commit()\n\n    user = session.scalar(select(User).where(User.username == 'alice'))\n\n    assert 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
    # ...\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\n\n\nclass Settings(BaseSettings):\n    model_config = SettingsConfigDict(\n        env_file='.env', env_file_encoding='utf-8'\n    )\n\n    DATABASE_URL: str\n

    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  \u251c\u2500\u2500 schemas.py\n\u2502  \u2514\u2500\u2500 settings.py\n\u251c\u2500\u2500 migrations\n\u2502  \u251c\u2500\u2500 env.py\n\u2502  \u251c\u2500\u2500 README\n\u2502  \u251c\u2500\u2500 script.py.mako\n\u2502  \u2514\u2500\u2500 versions\n\u251c\u2500\u2500 poetry.lock\n\u251c\u2500\u2500 pyproject.toml\n\u251c\u2500\u2500 README.md\n\u2514\u2500\u2500 tests\n   \u251c\u2500\u2500 __init__.py\n   \u251c\u2500\u2500 conftest.py\n   \u251c\u2500\u2500 test_app.py\n   \u2514\u2500\u2500 test_db.py\n

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

    "},{"location":"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
    # ...\nfrom alembic import context\nfrom fast_zero.settings import Settings\nfrom fast_zero.models import Base\n\nconfig = context.config\nconfig.set_main_option('sqlalchemy.url', Settings().DATABASE_URL)\n\nif config.config_file_name is not None:\n    fileConfig(config.config_file_name)\n\ntarget_metadata = Base.metadata\n\n# ...\n

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

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

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

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

    "},{"location":"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\n\nRevision ID: e018397cecf4\nRevises:\nCreate Date: 2023-07-13 03:43:03.730534\n\n\"\"\"\nfrom alembic import op\nimport sqlalchemy as sa\n\n\n# revision identifiers, used by Alembic.\nrevision = 'e018397cecf4'\ndown_revision = None\nbranch_labels = None\ndepends_on = None\n\n\ndef upgrade() -> None:\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.create_table('users',\n    sa.Column('id', sa.Integer(), nullable=False),\n    sa.Column('username', sa.String(), nullable=False),\n    sa.Column('password', sa.String(), nullable=False),\n    sa.Column('email', sa.String(), nullable=False),\n    sa.PrimaryKeyConstraint('id')\n    )\n    # ### end Alembic commands ###\n\n\ndef downgrade() -> None:\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.drop_table('users')\n    # ### end Alembic commands ###\n

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

    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.

    Caso n\u00e3o tenha o SQLite instalado na sua m\u00e1quina: Arch
    pacman -S sqlite\n
    Debian/Ubuntu
    sudo apt install sqlite\n
    Mac
    brew install sqlite\n
    • Para usu\u00e1rios Windows, ver o endere\u00e7o SQLite Downloads
    $ Execu\u00e7\u00e3o no terminal!
    sqlite3 database.db\n
    SQLite version 3.42.0 2023-05-16 12:36:15\nEnter \".help\" for usage hints.\nsqlite> .schema\nCREATE TABLE alembic_version (\n    version_num VARCHAR(32) NOT NULL,\n    CONSTRAINT alembic_version_pkc PRIMARY KEY (version_num)\n);\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\n
    INFO  [alembic.runtime.migration] Context impl SQLiteImpl.\nINFO  [alembic.runtime.migration] Will assume non-transactional DDL.\nINFO  [alembic.runtime.migration] Running upgrade  -> e018397cecf4, create users table\n

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

    $ Execu\u00e7\u00e3o no terminal!
    sqlite3 database.db\n
    SQLite version 3.42.0 2023-05-16 12:36:15\nEnter \".help\" for usage hints.\nsqlite> .schema\nCREATE TABLE alembic_version (\n    version_num VARCHAR(32) NOT NULL,\n    CONSTRAINT alembic_version_pkc PRIMARY KEY (version_num)\n);\nCREATE TABLE users (\n    id INTEGER NOT NULL,\n    username VARCHAR NOT NULL,\n    password VARCHAR NOT NULL,\n    email VARCHAR NOT NULL,\n    PRIMARY 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 pessoas, 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\n\nfrom fast_zero.settings import Settings\n\nengine = create_engine(Settings().DATABASE_URL)\n\n\ndef get_session():\n    with Session(engine) as session:\n        yield 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\n\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# ...\n\n\n@app.post('/users/', response_model=UserPublic, status_code=201)\ndef create_user(user: UserSchema, session: Session = Depends(get_session)):\n    db_user = session.scalar(\n        select(User).where(User.username == user.username)\n    )\n\n    if db_user:\n        raise HTTPException(\n            status_code=400, detail='Username already registered'\n        )\n\n    db_user = User(\n        username=user.username, password=user.password, email=user.email\n    )\n    session.add(db_user)\n    session.commit()\n    session.refresh(db_user)\n\n    return db_user\n

    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.

    "},{"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 alterar 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\n\nfrom fast_zero.app import app\nfrom fast_zero.database import get_session\n\n# ...\n\n@pytest.fixture\ndef client(session):\n    def get_session_override():\n        return session\n\n    with TestClient(app) as client:\n        app.dependency_overrides[get_session] = get_session_override\n        yield client\n\n    app.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):\n    response = client.post(\n        '/users',\n        json={\n            'username': 'alice',\n            'email': 'alice@example.com',\n            'password': 'secret',\n        },\n    )\n    assert response.status_code == 201\n    assert 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\n# ...\n\ntests/test_app.py::test_root_deve_retornar_200_e_ola_mundo PASSED\ntests/test_app.py::test_create_user FAILED\n

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

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

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

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

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

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

    Assim, nossa fixture deve ficar dessa forma:

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

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

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

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

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

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

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

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

    • offset permite pular um n\u00famero espec\u00edfico de registros antes de come\u00e7ar a buscar, o que \u00e9 \u00fatil para implementar a navega\u00e7\u00e3o por p\u00e1ginas.
    • limit define o n\u00famero m\u00e1ximo de registros a serem retornados, permitindo que voc\u00ea controle a quantidade de dados enviados em cada resposta.

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

    "},{"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):\n    response = client.get('/users')\n    assert response.status_code == 200\n    assert response.json() == {'users': []}\n

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

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

    Por\u00e9m, \u00e9 claro, queremos tamb\u00e9m testar o caso em que existem usu\u00e1rios no banco. Para isso, 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# ...\n\n@pytest.fixture\ndef user(session):\n    user = User(username='Teste', email='teste@test.com', password='testtest')\n    session.add(user)\n    session.commit()\n    session.refresh(user)\n\n    return user\n

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

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

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

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

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

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

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

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

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

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

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

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

    $ Execu\u00e7\u00e3o no terminal!
    task test\n\n# ...\n\ntests/test_app.py::test_root_deve_retornar_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 de PUT para suportar o banco de dados, como fizemos com os endpoints POST e GET:

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

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

    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 PUT era a \u00fanica que estava utilizando UserDB, e agora que modificamos esta rota, podemos remover UserDB dos nossos e tamb\u00e9m excluir sua defini\u00e7\u00e3o no arquivo schemas.py

    Sobre o arquivo schemas.py

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

    schemas.py
    from pydantic import BaseModel, EmailStr\n\n\nclass UserSchema(BaseModel):\n    username: str\n    email: EmailStr\n    password: str\n\n\nclass UserPublic(BaseModel):\n    id: int\n    username: str\n    email: EmailStr\n\n\nclass UserList(BaseModel):\n    users: list[UserPublic]\n\n\nclass Message(BaseModel):\n    detail: str\n
    "},{"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):\n    response = client.put(\n        '/users/1',\n        json={\n            'username': 'bob',\n            'email': 'bob@example.com',\n            'password': 'mynewpassword',\n        },\n    )\n    assert response.status_code == 200\n    assert 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)):\n    db_user = session.scalar(select(User).where(User.id == user_id))\n\n    if db_user is None:\n        raise HTTPException(status_code=404, detail='User not found')\n\n    session.delete(db_user)\n    session.commit()\n\n    return {'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):\n    response = client.delete('/users/1')\n    assert response.status_code == 200\n    assert 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\n\nfrom jose import jwt\nfrom passlib.context import CryptContext\n\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')\n\n\ndef create_access_token(data: dict):\n    to_encode = data.copy()\n    expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)\n    to_encode.update({'exp': expire})\n    encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)\n    return encoded_jwt\n

    A fun\u00e7\u00e3o create_access_token \u00e9 respons\u00e1vel por criar um novo token JWT que ser\u00e1 usado para autenticar o usu\u00e1rio. Ela recebe um dicion\u00e1rio de dados, adiciona um tempo de expira\u00e7\u00e3o ao token (baseado na constante ACCESS_TOKEN_EXPIRE_MINUTES). Esses dados, em conjunto formam o 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\n\nfrom fast_zero.security import create_access_token, SECRET_KEY\n\n\ndef test_jwt():\n    data = {'test': 'test'}\n    token = create_access_token(data)\n\n    decoded = jwt.decode(token, SECRET_KEY, algorithms=['HS256'])\n\n    assert decoded['test'] == data['test']\n    assert decoded['exp']  # Testa se o valor de exp foi adicionado ao token\n

    Na pr\u00f3xima se\u00e7\u00e3o, 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):\n    return pwd_context.hash(password)\n\n\ndef verify_password(plain_password: str, hashed_password: str):\n    return pwd_context.verify(plain_password, hashed_password)\n

    A fun\u00e7\u00e3o get_password_hash recebe uma senha em texto puro como argumento e retorna uma vers\u00e3o criptografada dessa senha. A fun\u00e7\u00e3o verify_password recebe uma senha em texto puro e uma senha criptografada como argumentos, e verifica se a senha em texto puro, quando criptografada, corresponde \u00e0 senha criptografada. Ambas as fun\u00e7\u00f5es utilizam o objeto pwd_context, que definimos anteriormente usando a biblioteca 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-o-endpoint-de-post-para-encriptar-a-senha","title":"Modificando o endpoint de POST para encriptar a senha","text":"

    Com as fun\u00e7\u00f5es de cria\u00e7\u00e3o de hash de senha e verifica\u00e7\u00e3o de senha em vigor, agora podemos atualizar nossos endpoints para usar essa nova funcionalidade de encripta\u00e7\u00e3o.

    Primeiro, vamos modificar a fun\u00e7\u00e3o create_user para criar um hash da senha antes de armazen\u00e1-la no banco de dados. Para fazer isso precisamos importar a fun\u00e7\u00e3o de gera\u00e7\u00e3o de hash get_password_hash e no momento da cria\u00e7\u00e3o do registro na tabela a senha deve ser passada com o hash gerado:

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

    Desta forma, a senha n\u00e3o ser\u00e1 mais criada em texto plano no objeto User. Fazendo com que caso exista algum problema relacionado a vazamento de dados, as senhas das pessoas nunca sejam expostas.

    "},{"location":"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\n# ...\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/#modificando-o-endpoint-de-atualizacao-de-usuarios","title":"Modificando o endpoint de atualiza\u00e7\u00e3o de usu\u00e1rios","text":"

    \u00c9 igualmente importante modificar a fun\u00e7\u00e3o update_user para tamb\u00e9m criar um hash da senha antes de atualizar User no banco de dados. Caso contr\u00e1rio, a senha em texto puro seria armazenada no banco de dados no momento da atualiza\u00e7\u00e3o.

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

    Assim, a atualiza\u00e7\u00e3o de um User, via m\u00e9todo PUT, tamb\u00e9m criar\u00e1 o hash da senha no momento da atualiza\u00e7\u00e3o. Pois, nesse caso em espec\u00edfico, existe a possibilidade de alterar qualquer coluna da tabela, inclusive o campo password.

    "},{"location":"05/#sobre-os-testes-da-put-usersuser_id","title":"Sobre os testes da PUT /users/{user_id}","text":"

    Assim como no teste da rota de cria\u00e7\u00e3o, os testes tamb\u00e9m passam normalmente por n\u00e3o validarem o campo password.

    $ Execu\u00e7\u00e3o no terminal!
    task test\n\n# ...\n\ntests/test_app.py::test_root_deve_retornar_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\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):\n    access_token: str\n    token_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 (\n    create_access_token,\n    get_password_hash,\n    verify_password,\n)\n\n# ...\n\n@app.post('/token', response_model=Token)\ndef login_for_access_token(\n    form_data: OAuth2PasswordRequestForm = Depends(),\n    session: Session = Depends(get_session),\n):\n    user = session.scalar(select(User).where(User.email == form_data.username))\n\n    if not user:\n        raise HTTPException(\n            status_code=400, detail='Incorrect email or password'\n        )\n\n    if not verify_password(form_data.password, user.password):\n        raise HTTPException(\n            status_code=400, detail='Incorrect email or password'\n        )\n\n    access_token = create_access_token(data={'sub': user.email})\n\n    return {'access_token': access_token, 'token_type': 'bearer'}\n

    Esse endpoint recebe os dados do formul\u00e1rio atrav\u00e9s do form_data (que s\u00e3o injetados automaticamente gra\u00e7as ao Depends()) e tenta recuperar um usu\u00e1rio com o email fornecido. Se o usu\u00e1rio n\u00e3o for encontrado ou a senha n\u00e3o corresponder ao hash armazenado no banco de dados, uma exce\u00e7\u00e3o \u00e9 lan\u00e7ada. Caso contr\u00e1rio, um token de acesso \u00e9 criado usando o create_access_token() que criamos anteriormente e retornado como uma resposta.

    "},{"location":"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):\n    response = client.post(\n        '/token',\n        data={'username': user.email, 'password': user.password},\n    )\n    token = response.json()\n\n    assert response.status_code == 200\n    assert 'access_token' in token\n    assert 'token_type' in token\n

    Nesse teste, n\u00f3s enviamos uma requisi\u00e7\u00e3o POST para o endpoint \"/token\" com um username e uma senha v\u00e1lidos. Ent\u00e3o, n\u00f3s verificamos que a resposta cont\u00e9m um \"access_token\" e um \"token_type\", que s\u00e3o os campos que esperamos de um JWT v\u00e1lido.

    No entanto, h\u00e1 um problema. Agora que a senha est\u00e1 sendo criptografada, nosso teste falhar\u00e1:

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

    Para corrigir isso, precisamos garantir que a senha esteja sendo criptografada na fixture antes de ser salva:

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

    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\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):\n    password = 'testtest'\n    user = User(\n        username='Teste',\n        email='teste@test.com',\n        password=get_password_hash(password),\n    )\n    session.add(user)\n    session.commit()\n    session.refresh(user)\n\n    user.clean_password = 'testtest'\n\n    return user\n

    Monkey patching \u00e9 uma t\u00e9cnica em que modificamos ou estendemos o c\u00f3digo em tempo de execu\u00e7\u00e3o. Neste caso, estamos adicionando um novo atributo clean_password ao objeto user para armazenar a senha em texto puro.

    Agora, podemos alterar o teste para usar clean_password:

    tests/test_app.py
    def test_get_token(client, user):\n    response = client.post(\n        '/token',\n        data={'username': user.email, 'password': user.clean_password},\n    )\n    token = response.json()\n\n    assert response.status_code == 200\n    assert 'access_token' in token\n    assert 'token_type' in token\n

    E agora todos os testes devem passar normalmente:

    $ Execu\u00e7\u00e3o no terminal!
    task test\n\n# ...\n\ntests/test_app.py::test_root_deve_retornar_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):\n    username: str | None = None\n

    Nesse ponto, criaremos uma a fun\u00e7\u00e3o get_current_user que ser\u00e1 respons\u00e1vel por extrair o token JWT do header Authorization da requisi\u00e7\u00e3o, decodificar esse token, extrair as informa\u00e7\u00f5es do usu\u00e1rio e 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\n\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\n\nfrom fast_zero.database import get_session\nfrom fast_zero.models import User\nfrom fast_zero.schemas import TokenData\n\n# ...\n\noauth2_scheme = OAuth2PasswordBearer(tokenUrl=\"token\")\n\nasync def get_current_user(\n    session: Session = Depends(get_session),\n    token: str = Depends(oauth2_scheme),\n):\n    credentials_exception = HTTPException(\n        status_code=status.HTTP_401_UNAUTHORIZED,\n        detail='Could not validate credentials',\n        headers={'WWW-Authenticate': 'Bearer'},\n    )\n\n    try:\n        payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])\n        username: str = payload.get('sub')\n        if not username:\n            raise credentials_exception\n        token_data = TokenData(username=username)\n    except JWTError:\n        raise credentials_exception\n\n    user = session.scalar(\n        select(User).where(User.email == token_data.username)\n    )\n\n    if user is None:\n        raise credentials_exception\n\n    return 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 (\n    create_access_token,\n    get_current_user,\n    get_password_hash,\n    verify_password,\n)\n\n# ...\n\n@app.put('/users/{user_id}', response_model=UserPublic)\ndef update_user(\n    user_id: int,\n    user: UserSchema,\n    session: Session = Depends(get_session),\n    current_user: User = Depends(get_current_user),\n):\n    if current_user.id != user_id:\n        raise HTTPException(status_code=400, detail='Not enough permissions')\n\n    current_user.username = user.username\n    current_user.password = user.password\n    current_user.email = user.email\n    session.commit()\n    session.refresh(current_user)\n\n    return current_user\n

    Com isso, podemos remover a query feita no endpoint para encontrar o User, pois ela j\u00e1 est\u00e1 sendo feita no get_current_user, simplificando ainda mais nosso endpoint.

    Agora, 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(\n    user_id: int,\n    session: Session = Depends(get_session),\n    current_user: User = Depends(get_current_user),\n):\n    if current_user.id != user_id:\n        raise HTTPException(status_code=400, detail='Not enough permissions')\n\n    session.delete(current_user)\n    session.commit()\n\n    return {'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):\n    response = client.post(\n        '/token',\n        data={'username': user.email, 'password': user.clean_password},\n    )\n    return response.json()['access_token']\n

    Agora, podemos atualizar os testes para o endpoint PUT e DELETE para incluir a autentica\u00e7\u00e3o.

    tests/test_app.py
    def test_update_user(client, user, token):\n    response = client.put(\n        f'/users/{user.id}',\n        headers={'Authorization': f'Bearer {token}'},\n        json={\n            'username': 'bob',\n            'email': 'bob@example.com',\n            'password': 'mynewpassword',\n        },\n    )\n    assert response.status_code == 200\n    assert response.json() == {\n        'username': 'bob',\n        'email': 'bob@example.com',\n        'id': 1,\n    }\n\n\ndef test_delete_user(client, user, token):\n    response = client.delete(\n        f'/users/{user.id}',\n        headers={'Authorization': f'Bearer {token}'},\n    )\n    assert response.status_code == 200\n    assert 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\n# ...\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/security.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 rotas 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 oferece uma ferramenta poderosa conhecida como routers, que facilita a organiza\u00e7\u00e3o e agrupamento de diferentes rotas em uma aplica\u00e7\u00e3o. Pense em um router como um \"subaplicativo\" do FastAPI que pode ser integrado em uma aplica\u00e7\u00e3o principal. Isso n\u00e3o s\u00f3 mant\u00e9m o c\u00f3digo organizado e leg\u00edvel, mas tamb\u00e9m se mostra especialmente \u00fatil \u00e0 medida que a aplica\u00e7\u00e3o se expande e novas rotas s\u00e3o adicionadas.

    Esse tipo de organiza\u00e7\u00e3o nos oferece diversos benef\u00edcios:

    1. Organiza\u00e7\u00e3o e Legibilidade: Routers ajudam a manter o c\u00f3digo organizado e leg\u00edvel, o que \u00e9 crucial \u00e0 medida que a aplica\u00e7\u00e3o se expande.
    2. Separa\u00e7\u00e3o de Preocupa\u00e7\u00f5es: Alinhado ao princ\u00edpio de SoC, os routers facilitam o entendimento e teste do c\u00f3digo.
    3. Escalabilidade: A estrutura\u00e7\u00e3o com routers permite adicionar novas rotas e funcionalidades de maneira eficiente conforme o projeto cresce.
    "},{"location":"06/#estruturacao-inicial","title":"Estrutura\u00e7\u00e3o Inicial","text":"

    Vamos iniciar criando uma nova estrutura de diret\u00f3rios chamada routes dentro do seu projeto fast_zero. Aqui, teremos subaplicativos dedicados a fun\u00e7\u00f5es espec\u00edficas, como gerenciamento de usu\u00e1rios e autentica\u00e7\u00e3o.

    \u251c\u2500\u2500 fast_zero\n\u2502  \u251c\u2500\u2500 app.py\n\u2502  \u251c\u2500\u2500 database.py\n\u2502  \u251c\u2500\u2500 models.py\n\u2502  \u251c\u2500\u2500 routes\n\u2502  \u2502  \u251c\u2500\u2500 auth.py\n\u2502  \u2502  \u2514\u2500\u2500 users.py\n

    Esta organiza\u00e7\u00e3o facilita a expans\u00e3o do seu projeto e a manuten\u00e7\u00e3o de uma estrutura clara.

    "},{"location":"06/#implementando-um-router-para-usuarios","title":"Implementando um Router para Usu\u00e1rios","text":"

    No arquivo fast_zero/routes/users.py, vamos importar APIRouter do FastAPI, a ferramenta chave para criar nosso subaplicativo. O par\u00e2metro prefix que passamos ajuda a agrupar todos os endpoints relacionados aos usu\u00e1rios sob um mesmo teto.

    fast_zero/routes/users.py
    from fastapi import APIRouter\n\nrouter = APIRouter(prefix='/users', tags=['users'])\n

    Com essa simples configura\u00e7\u00e3o, estamos prontos para definir rotas espec\u00edficas para usu\u00e1rios neste router, em vez de sobrecarregar o aplicativo principal. Utilizamos @router ao inv\u00e9s de @app para definir estas rotas. O uso da tag 'users' contribui para a organiza\u00e7\u00e3o e documenta\u00e7\u00e3o autom\u00e1tica no swagger.

    Desta forma podemos migrar todos os nossos imports e nossas fun\u00e7\u00f5es de endpoints para o arquivo fast_zero/routes/users.py e os removendo de fast_zero/app.py. Fazendo com que todos esse endpoints estejam no mesmo contexto e isolados da aplica\u00e7\u00e3o principal:

    fast_zero/routes/users.py
    from fastapi import APIRouter, Depends, HTTPException\nfrom sqlalchemy import select\nfrom sqlalchemy.orm import Session\n\nfrom fast_zero.database import get_session\nfrom fast_zero.models import User\nfrom fast_zero.schemas import Message, UserList, UserPublic, UserSchema\nfrom fast_zero.security import (\n    get_current_user,\n    get_password_hash,\n)\n\nrouter = APIRouter(prefix='/users', tags=['users'])\n\n@router.post('/', response_model=UserPublic, status_code=201)\n# ...\n@router.get('/', response_model=UserList)\n# ...\n@router.put('/{user_id}', response_model=UserPublic)\n# ...\n@router.delete('/{user_id}', response_model=Message)\n# ...\n

    Com o prefixo definido no router, os paths dos endpoints se tornam mais simples e diretos. Ao inv\u00e9s de '/users/{user_id}', por exemplo, usamos apenas '/{user_id}'.

    Exemplo do arquivo fast_zero/routes/users.py completo fast_zero/routes/users.py
    from fastapi import APIRouter, Depends, HTTPException\nfrom sqlalchemy import select\nfrom sqlalchemy.orm import Session\n\nfrom fast_zero.database import get_session\nfrom fast_zero.models import User\nfrom fast_zero.schemas import Message, UserList, UserPublic, UserSchema\nfrom fast_zero.security import (\n    get_current_user,\n    get_password_hash,\n)\n\nrouter = APIRouter(prefix='/users', tags=['users'])\n\n\n@router.post('/', response_model=UserPublic, status_code=201)\ndef create_user(user: UserSchema, session: Session = Depends(get_session)):\n    db_user = session.scalar(select(User).where(User.email == user.email))\n    if db_user:\n        raise HTTPException(status_code=400, detail='Email already registered')\n\n    hashed_password = get_password_hash(user.password)\n\n    db_user = User(\n        email=user.email,\n        username=user.username,\n        password=hashed_password,\n    )\n    session.add(db_user)\n    session.commit()\n    session.refresh(db_user)\n    return db_user\n\n\n@router.get('/', response_model=UserList)\ndef read_users(\n    skip: int = 0, limit: int = 100, session: Session = Depends(get_session)\n):\n    users = session.scalars(select(User).offset(skip).limit(limit)).all()\n    return {'users': users}\n\n\n@router.put('/{user_id}', response_model=UserPublic)\ndef update_user(\n    user_id: int,\n    user: UserSchema,\n    session: Session = Depends(get_session),\n    current_user: User = Depends(get_current_user),\n):\n    if current_user.id != user_id:\n        raise HTTPException(status_code=400, detail='Not enough permissions')\n\n    current_user.username = user.username\n    current_user.password = get_password_hash(user.password)\n    current_user.email = user.email\n    session.commit()\n    session.refresh(current_user)\n\n    return current_user\n\n\n@router.delete('/{user_id}', response_model=Message)\ndef delete_user(\n    user_id: int,\n    session: Session = Depends(get_session),\n    current_user: User = Depends(get_current_user),\n):\n    if current_user.id != user_id:\n        raise HTTPException(status_code=400, detail='Not enough permissions')\n\n    session.delete(current_user)\n    session.commit()\n\n    return {'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\n\nfrom fast_zero.database import get_session\nfrom fast_zero.models import User\nfrom fast_zero.schemas import Token\nfrom fast_zero.security import create_access_token, verify_password\n\nrouter = APIRouter(prefix='/auth', tags=['auth'])\n\n\n@router.post('/token', response_model=Token)\ndef login_for_access_token(\n    form_data: OAuth2PasswordRequestForm = Depends(),\n    session: Session = Depends(get_session),\n):\n    user = session.scalar(select(User).where(User.email == form_data.username))\n\n    if not user:\n        raise HTTPException(\n            status_code=400, detail='Incorrect email or password'\n        )\n\n    if not verify_password(form_data.password, user.password):\n        raise HTTPException(\n            status_code=400, detail='Incorrect email or password'\n        )\n\n    access_token = create_access_token(data={'sub': user.email})\n\n    return {'access_token': access_token, 'token_type': 'bearer'}\n

    Neste bloco de c\u00f3digo, n\u00f3s criamos um novo router que lidar\u00e1 exclusivamente com a rota de obten\u00e7\u00e3o de token (/token). O endpoint login_for_access_token \u00e9 definido exatamente da mesma maneira que antes, mas agora como parte deste router de autentica\u00e7\u00e3o.

    "},{"location":"06/#alteracao-da-validacao-de-token","title":"Altera\u00e7\u00e3o da valida\u00e7\u00e3o de token","text":"

    \u00c9 crucial abordar um aspecto relacionado \u00e0 modifica\u00e7\u00e3o do router: o uso do par\u00e2metro prefix. Ao introduzir o prefixo, o endere\u00e7o do endpoint /token, respons\u00e1vel pela valida\u00e7\u00e3o do bearer token JWT, \u00e9 alterado para /auth/token. Esse caminho est\u00e1 explicitamente definido no OAuth2PasswordBearer dentro de security.py, resultando em uma refer\u00eancia ao caminho antigo /token, anterior \u00e0 cria\u00e7\u00e3o do router.

    Esse problema fica evidente ao clicar no bot\u00e3o Authorize no Swagger:

    Percebe-se que o caminho para a autoriza\u00e7\u00e3o est\u00e1 incorreto. Como consequ\u00eancia, ao tentar autenticar atrav\u00e9s do Swagger, nos deparamos com um erro na interface:

    No entanto, o erro n\u00e3o \u00e9 suficientemente descritivo para identificarmos a origem do problema, retornando apenas uma mensagem gen\u00e9rica de Auth Error. Para compreender melhor o que ocorreu, \u00e9 necess\u00e1rio verificar o log produzido pelo uvicorn no terminal:

    Erro mostrado no terminal
    task serve\n# ...\nINFO:     127.0.0.1:40132 - \"POST /token HTTP/1.1\" 404 Not Found\n

    A solu\u00e7\u00e3o para este problema \u00e9 relativamente simples. Precisamos ajustar o par\u00e2metro tokenUrl na OAuth2PasswordBearer para refletir as mudan\u00e7as feitas no router, direcionando para /auth/token. Faremos isso no arquivo security.py:

    security.py
    oauth2_scheme = OAuth2PasswordBearer(tokenUrl='auth/token')\n

    Ap\u00f3s essa altera\u00e7\u00e3o, ao utilizar o Swagger, a autoriza\u00e7\u00e3o ser\u00e1 direcionada corretamente para o endpoint apropriado.

    "},{"location":"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\n\nfrom fast_zero.routes import auth, users\n\napp = FastAPI()\n\napp.include_router(users.router)\napp.include_router(auth.router)\n\n\n@app.get('/')\ndef read_root():\n    return {'message': 'Ol\u00e1 Mundo!'}\n

    Como voc\u00ea pode ver, nosso arquivo app.py \u00e9 muito mais simples agora. Ele agora delega as rotas para os respectivos routers, mantendo o foco em iniciar nossa aplica\u00e7\u00e3o FastAPI.

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

    Para acompanhar a nova estrutura routers, podemos desacoplar os testes do m\u00f3dulo test/test_app.py e criar arquivos de teste espec\u00edficos para cada um dos dom\u00ednios:

    • /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):\n    response = client.post(\n        '/auth/token',\n        data={'username': user.email, 'password': user.clean_password},\n    )\n    token = response.json()\n\n    assert response.status_code == 200\n    assert 'access_token' in token\n    assert 'token_type' in token\n

    \u00c9 importante notar que com a cria\u00e7\u00e3o do router usando prefix='/auth' devemos alterar o endpoint onde o request \u00e9 feito de '/token' para '/auth/token'. Fazendo com que a requisi\u00e7\u00e3o seja encaminhada para o lugar certo.

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

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

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

    Para a constru\u00e7\u00e3o desse arquivo, nenhum teste foi modificado. Eles foram somente movidos para o dom\u00ednio espec\u00edfico do router. Importante, por\u00e9m, notar que alguns destes testes usam a fixture token para checar a autoriza\u00e7\u00e3o, como o endpoint do token foi alterado, devemos alterar a fixture de token para que esses testes continuem passando.

    "},{"location":"06/#ajustando-a-fixture-de-token","title":"Ajustando a fixture de token","text":"

    A altera\u00e7\u00e3o da fixture de token \u00e9 igual que fizemos em /tests/test_auth.py, precisamos somente corrigir o novo endere\u00e7o do router no arquivo /tests/conftest.py:

    /tests/conftest.py
    @pytest.fixture\ndef token(client, user):\n    response = client.post(\n        '/auth/token',\n        data={'username': user.email, 'password': user.clean_password},\n    )\n    return response.json()['access_token']\n

    Fazendo assim com que os testes que dependem dessa fixture passem a funcionar.

    "},{"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\n# ...\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\n\nSession = Annotated[Session, Depends(get_session)]\nCurrentUser = Annotated[User, Depends(get_current_user)]\n

    Desse modo, conseguimos refinar a defini\u00e7\u00e3o dos endpoints para que se tornem mais concisos, sem alterar seu funcionamento:

    fast_zero/routes/users.py
    @router.post('/', response_model=UserPublic, status_code=201)\ndef create_user(user: UserSchema, session: Session):\n# ...\n\n@router.get('/', response_model=UserList)\ndef read_users(session: Session, skip: int = 0, limit: int = 100):\n# ...\n\n@router.put('/{user_id}', response_model=UserPublic)\ndef update_user(\n    user_id: int,\n    user: UserSchema,\n    session: Session,\n    current_user: CurrentUser\n):\n# ...\n\n@router.delete('/{user_id}', response_model=Message)\ndef delete_user(user_id: int, session: Session, current_user: CurrentUser):\n# ...\n

    Da mesma forma, podemos otimizar o roteador de autentica\u00e7\u00e3o:

    fast_zero/routers/auth.py
    from typing import Annotated\n\n# ...\n\nOAuth2Form = Annotated[OAuth2PasswordRequestForm, Depends()]\nSession = Annotated[Session, Depends(get_session)]\n\n@router.post('/token', response_model=Token)\ndef login_for_access_token(form_data: OAuth2Form, session: Session):\n#...\n

    Atrav\u00e9s do uso de tipos Annotated, conseguimos reutilizar os mesmos consistentemente, reduzindo a repeti\u00e7\u00e3o de c\u00f3digo e aumentando a efici\u00eancia do nosso trabalho.

    "},{"location":"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\n\n\nclass Settings(BaseSettings):\n    model_config = SettingsConfigDict(\n        env_file='.env', env_file_encoding='utf-8'\n    )\n\n    DATABASE_URL: str\n    SECRET_KEY: str\n    ALGORITHM: str\n    ACCESS_TOKEN_EXPIRE_MINUTES: int\n

    Agora, precisamos adicionar estes valores ao nosso arquivo .env.

    .env
    DATABASE_URL=\"sqlite:///database.db\"\nSECRET_KEY=\"your-secret-key\"\nALGORITHM=\"HS256\"\nACCESS_TOKEN_EXPIRE_MINUTES=30\n

    Com isso, podemos alterar o nosso c\u00f3digo em fast_zero/security.py para ler as constantes a partir da classe Settings.

    "},{"location":"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\n\nsettings = Settings()\n

    Com isso, todos os lugares onde as constantes eram usadas devem ser substitu\u00eddos por settings.CONSTANTE. Por exemplo, na fun\u00e7\u00e3o create_access_token, vamos alterar para usar as constantes da classe Settings:

    fast_zero/security.py
    def create_access_token(data: dict):\n    to_encode = data.copy()\n    expire = datetime.utcnow() + timedelta(\n        minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES\n    )\n    to_encode.update({'exp': expire})\n    encoded_jwt = jwt.encode(\n        to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM\n    )\n    return encoded_jwt\n

    Desta forma, eliminamos todas as constantes do c\u00f3digo-fonte e passamos a usar as configura\u00e7\u00f5es a partir da classe Settings. Isso torna nosso c\u00f3digo mais seguro, pois as constantes sens\u00edveis, como a chave secreta, est\u00e3o agora seguras em nosso arquivo .env, e nosso c\u00f3digo fica mais modular, pois podemos facilmente alterar estas constantes simplesmente mudando os valores no arquivo .env. Al\u00e9m disso, essa abordagem facilita o gerenciamento de diferentes ambientes (como desenvolvimento, teste e produ\u00e7\u00e3o) pois cada ambiente pode ter seu pr\u00f3prio arquivo .env com suas configura\u00e7\u00f5es espec\u00edficas.

    "},{"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\n# ...\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":"Tornando o sistema de autentica\u00e7\u00e3o robusto","text":""},{"location":"07/#tornando-o-sistema-de-autenticacao-robusto","title":"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):\n    response = client.put(\n        f'/users/{user.id}',\n        headers={'Authorization': f'Bearer {token}'},\n        json={\n            'username': 'bob',\n            'email': 'bob@example.com',\n            'password': 'mynewpassword',\n        },\n    )\n    assert response.status_code == 400\n    assert response.json() == {'detail': 'Not enough permissions'}\n
    Este teste vai simular um usu\u00e1rio tentando alterar as informa\u00e7\u00f5es de outro usu\u00e1rio. Se nosso sistema estiver funcionando corretamente, ele dever\u00e1 rejeitar essa tentativa e retornar um erro."},{"location":"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\n# ...\n\nclass UserFactory(factory.Factory):\n    class Meta:\n        model = User\n\n    id = factory.Sequence(lambda n: n)\n    username = factory.LazyAttribute(lambda obj: f'test{obj.id}')\n    email = factory.LazyAttribute(lambda obj: f'{obj.username}@test.com')\n    password = factory.LazyAttribute(lambda obj: f'{obj.username}@example.com')\n

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

    • 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):\n    password = 'testtest'\n    user = UserFactory(password=get_password_hash(password))\n\n    session.add(user)\n    session.commit()\n    session.refresh(user)\n\n    user.clean_password = 'testtest'\n\n    return user\n\n\n@pytest.fixture\ndef other_user(session):\n    password = 'testtest'\n    user = UserFactory(password=get_password_hash(password))\n\n    session.add(user)\n    session.commit()\n    session.refresh(user)\n\n    user.clean_password = 'testtest'\n\n    return user\n

    A cria\u00e7\u00e3o de outra fixture chamada other_user \u00e9 crucial para simular o cen\u00e1rio de um usu\u00e1rio tentando acessar ou modificar as informa\u00e7\u00f5es de outro usu\u00e1rio no sistema. Ao criar duas fixtures diferentes, user e other_user, podemos efetivamente simular dois usu\u00e1rios diferentes em nossos testes. Isso nos permite avaliar como nosso sistema reage quando um usu\u00e1rio tenta realizar uma a\u00e7\u00e3o n\u00e3o autorizada, como alterar as informa\u00e7\u00f5es de outro usu\u00e1rio.

    Um aspecto interessante no uso das f\u00e1bricas \u00e9 que, sempre que forem chamadas, elas retornar\u00e3o um novo User, pois estamos fixando apenas a senha. Dessa forma, cada chamada a essa f\u00e1brica de usu\u00e1rios retornar\u00e1 um User diferente, com base nos atributos \"lazy\" que usamos.

    Com essa nova configura\u00e7\u00e3o, podemos finalmente testar o cen\u00e1rio de um usu\u00e1rio tentando alterar as informa\u00e7\u00f5es de outro usu\u00e1rio. E como voc\u00ea pode ver, nossos testes passaram com sucesso, o que indica que nosso sistema est\u00e1 lidando corretamente com essa situa\u00e7\u00e3o.

    tests/test_users.py
    def test_update_user_with_wrong_user(client, other_user, token):\n    response = client.put(\n        f'/users/{other_user.id}',\n        headers={'Authorization': f'Bearer {token}'},\n        json={\n            'username': 'bob',\n            'email': 'bob@example.com',\n            'password': 'mynewpassword',\n        },\n    )\n    assert response.status_code == 400\n    assert 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\n# ...\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):\n    response = client.delete(\n        f'/users/{other_user.id}',\n        headers={'Authorization': f'Bearer {token}'},\n    )\n    assert response.status_code == 400\n    assert response.json() == {'detail': 'Not enough permissions'}\n

    Como voc\u00ea pode ver, esse teste tenta deletar o user de um id diferente usando o token do user. Se nosso sistema estiver funcionando corretamente, ele dever\u00e1 rejeitar essa tentativa e retornar um status 400 com uma mensagem de erro indicando que o usu\u00e1rio n\u00e3o tem permiss\u00f5es suficientes para realizar essa a\u00e7\u00e3o.

    Vamos executar esse teste agora e ver o que acontece:

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

    \u00d3timo, nosso teste passou! Isso significa que nosso sistema est\u00e1 corretamente impedindo um usu\u00e1rio de deletar a conta de outro usu\u00e1rio.

    Agora que terminamos de testar a autoriza\u00e7\u00e3o, 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 \"congelar\" o tempo em um ponto espec\u00edfico ou avan\u00e7\u00e1-lo conforme necess\u00e1rio durante os testes. Isso \u00e9 especialmente \u00fatil para testar funcionalidades sens\u00edveis ao tempo, como a expira\u00e7\u00e3o de tokens, sem ter que esperar em tempo real.

    Primeiro, 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.

    Ao elaborarmos o teste, usaremos a funcionalidade de congelamento de tempo do freezegun. O objetivo \u00e9 simular a cria\u00e7\u00e3o de um token \u00e0s 12:00 e, em seguida, verificar sua expira\u00e7\u00e3o \u00e0s 12:31. Neste cen\u00e1rio, estamos utilizando o conceito de \"viajar no tempo\" para al\u00e9m do per\u00edodo de validade do token, garantindo que a tentativa subsequente de utiliz\u00e1-lo resultar\u00e1 em um erro de autentica\u00e7\u00e3o.

    tests/test_auth.py
    from freezegun import freeze_time\n\n# ...\n\ndef test_token_expired_after_time(client, user):\n    with freeze_time('2023-07-14 12:00:00'):\n        response = client.post(\n            '/auth/token',\n            data={'username': user.email, 'password': user.clean_password},\n        )\n        assert response.status_code == 200\n        token = response.json()['access_token']\n\n    with freeze_time('2023-07-14 12:31:00'):\n        response = client.put(\n            f'/users/{user.id}',\n            headers={'Authorization': f'Bearer {token}'},\n            json={\n                'username': 'wrongwrong',\n                'email': 'wrong@wrong.com',\n                'password': 'wrong',\n            },\n        )\n        assert response.status_code == 401\n        assert response.json() == {'detail': 'Could not validate credentials'}\n

    Lembre-se de que configuramos nosso token para expirar ap\u00f3s 30 minutos. Portanto, n\u00f3s avan\u00e7amos o tempo em 31 minutos para garantir que o token tenha expirado.

    Agora, vamos executar nosso teste e ver o que acontece:

    $ Execu\u00e7\u00e3o no terminal!
    task test\n\n# ...\n\ntests/test_users.py::test_token_expired_after_time 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):\n    response = client.post(\n        '/auth/token',\n        data={'username': 'no_user@no_domain.com', 'password': 'testtest'},\n    )\n    assert response.status_code == 400\n    assert 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):\n    response = client.post(\n        '/auth/token', data={'username': user.email, 'password': 'wrong_password'}\n    )\n    assert response.status_code == 400\n    assert response.json() == {'detail': 'Incorrect email or password'}\n

    Com esses testes, garantimos que nossas exce\u00e7\u00f5es est\u00e3o sendo lan\u00e7adas corretamente. Essa \u00e9 uma parte importante da constru\u00e7\u00e3o de um sistema de autentica\u00e7\u00e3o robusto, pois nos permite ter confian\u00e7a de que estamos tratando corretamente os casos de erro.

    "},{"location":"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 (\n    create_access_token,\n    get_current_user,\n    verify_password,\n)\n\n# ...\n\n@router.post('/refresh_token', response_model=Token)\ndef refresh_access_token(\n    user: User = Depends(get_current_user),\n):\n    new_access_token = create_access_token(data={'sub': user.email})\n\n    return {'access_token': new_access_token, 'token_type': 'bearer'}\n

    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):\n    response = client.post(\n        '/auth/refresh_token',\n        headers={'Authorization': f'Bearer {token}'},\n    )\n\n    data = response.json()\n\n    assert response.status_code == 200\n    assert 'access_token' in data\n    assert 'token_type' in data\n    assert data['token_type'] == 'bearer'\n

    Ainda \u00e9 importante garantir que nosso sistema trate corretamente os tokens expirados. Para isso, 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_expired_dont_refresh(client, user):\n    with freeze_time('2023-07-14 12:00:00'):\n        response = client.post(\n            '/auth/token',\n            data={'username': user.email, 'password': user.clean_password},\n        )\n        assert response.status_code == 200\n        token = response.json()['access_token']\n\n    with freeze_time('2023-07-14 12:31:00'):\n        response = client.post(\n            '/auth/refresh_token',\n            headers={'Authorization': f'Bearer {token}'},\n        )\n        assert response.status_code == 401\n        assert response.json() == {'detail': 'Could not validate credentials'}\n

    Agora, se executarmos nossos testes, todos eles devem passar, incluindo os novos testes que acabamos de adicionar.

    $ Execu\u00e7\u00e3o no terminal!
    task test\n\n# ...\n\ntests/test_app.py::test_root_deve_retornar_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_expired_after_time PASSED\ntests/test_db.py::test_create_user PASSED\ntests/test_users.py::test_create_user PASSED\ntests/test_users.py::test_read_users PASSED\ntests/test_users.py::test_read_users_with_users PASSED\ntests/test_users.py::test_update_user PASSED\ntests/test_users.py::test_update_user_with_wrong_user PASSED\ntests/test_users.py::test_delete_user PASSED\ntests/test_users.py::test_delete_user_wrong_user PASSED\ntests/test_users.py::test_token_expired_dont_refresh PASSED\n

    Com esses testes, podemos ter certeza de que cobrimos alguns casos importantes relacionados \u00e0 autentica\u00e7\u00e3o de usu\u00e1rios em nossa API.

    "},{"location":"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":"Criando Rotas CRUD para Gerenciamento de Tarefas em FastAPI","text":""},{"location":"08/#criando-rotas-crud-para-gerenciamento-de-tarefas-em-fastapi","title":"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\n\nrouter = APIRouter(prefix='/todos', tags=['todos'])\n

    Neste c\u00f3digo, criamos uma nova inst\u00e2ncia da classe APIRouter do FastAPI. Esta classe \u00e9 usada para definir as rotas de nossa aplica\u00e7\u00e3o. A inst\u00e2ncia router funcionar\u00e1 como um mini aplicativo FastAPI, que poder\u00e1 ter suas pr\u00f3prias rotas, modelos de resposta, etc.

    A op\u00e7\u00e3o prefix no construtor do APIRouter \u00e9 usada para definir um prefixo comum para todas as rotas definidas neste roteador. Isso significa que todas as rotas que definirmos neste roteador come\u00e7ar\u00e3o com /todos. Usamos um prefixo aqui porque queremos agrupar todas as rotas relacionadas a tarefas em um lugar. Isso torna nossa aplica\u00e7\u00e3o mais organizada e f\u00e1cil de entender.

    A op\u00e7\u00e3o tags \u00e9 usada para agrupar as rotas em se\u00e7\u00f5es no documento interativo de API gerado pelo FastAPI (como Swagger UI e ReDoc). Todas as rotas que definirmos neste roteador aparecer\u00e3o na se\u00e7\u00e3o \"todos\" da documenta\u00e7\u00e3o da API.

    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\n\nfrom fast_zero.routes import auth, todos, users\n\napp = FastAPI()\n\napp.include_router(users.router)\napp.include_router(auth.router)\napp.include_router(todos.router)\n\n\n@app.get('/')\ndef read_root():\n    return {'message': 'Ol\u00e1 Mundo!'}\n

    Neste c\u00f3digo, chamamos o m\u00e9todo include_router do FastAPI para cada roteador que definimos. Este m\u00e9todo adiciona todas as rotas do roteador \u00e0 nossa aplica\u00e7\u00e3o. Com isso, nossa aplica\u00e7\u00e3o agora ter\u00e1 todas as rotas definidas nos roteadores users, auth e todos.

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

    fast_zero/models.py
    from enum import Enum\n\nfrom sqlalchemy import ForeignKey\nfrom sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship\n\n\nclass TodoState(str, Enum):\n    draft = 'draft'\n    todo = 'todo'\n    doing = 'doing'\n    done = 'done'\n    trash = 'trash'\n\n\nclass Base(DeclarativeBase):\n    pass\n\n\nclass User(Base):\n    __tablename__ = 'users'\n\n    id: Mapped[int] = mapped_column(primary_key=True)\n    username: Mapped[str]\n    password: Mapped[str]\n    email: Mapped[str]\n\n    todos: Mapped[list['Todo']] = relationship(\n        back_populates='user', cascade='all, delete-orphan'\n    )\n\n\nclass Todo(Base):\n    __tablename__ = 'todos'\n\n    id: Mapped[int] = mapped_column(primary_key=True)\n    title: Mapped[str]\n    description: Mapped[str]\n    state: Mapped[TodoState]\n    user_id: Mapped[int] = mapped_column(ForeignKey('users.id'))\n\n    user: 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):\n    todo = Todo(\n        title='Test Todo',\n        description='Test Desc',\n        state='draft',\n        user_id=user.id,\n    )\n\n    session.add(todo)\n    session.commit()\n    session.refresh(todo)\n\n    user = session.scalar(select(User).where(User.id == user.id))\n\n    assert todo in user.todos\n

    Com isso, voc\u00ea pode executar os testes:

    $ Execu\u00e7\u00e3o no terminal!
    task test tests/test_db.py\n# ...\ntests/test_db.py::test_create_user_without_todos PASSED\ntests/test_db.py::test_create_todo PASSED\n

    Isso mostra que os testes foram bem-sucedidos. Mesmo sem testes mais extensivos, agora 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\n#...\n\nclass TodoSchema(BaseModel):\n    title: str\n    description: str\n    state: TodoState\n\nclass TodoPublic(BaseModel):\n    id: int\n    title: str\n    description: str\n    state: TodoState\n\nclass TodoList(BaseModel):\n    todos: list[TodoPublic]\n

    TodoSchema ser\u00e1 usado para validar os dados de entrada quando uma nova tarefa \u00e9 criada e TodoPublic ser\u00e1 usado para validar os dados de sa\u00edda quando uma tarefa \u00e9 retornada em um endpoint.

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

    fast_zero/routes/todos.py
    from typing import Annotated\n\nfrom fastapi import APIRouter, Depends\nfrom sqlalchemy.orm import Session\n\nfrom fast_zero.database import get_session\nfrom fast_zero.models import Todo, User\nfrom fast_zero.schemas import TodoPublic, TodoSchema\nfrom fast_zero.security import get_current_user\n\nrouter = APIRouter()\n\nCurrentUser = Annotated[User, Depends(get_current_user)]\n\nrouter = APIRouter(prefix='/todos', tags=['todos'])\n\n\n@router.post('/', response_model=TodoPublic)\ndef create_todo(\n    todo: TodoSchema,\n    user: CurrentUser,\n    session: Session = Depends(get_session),\n):\n    db_todo: Todo = Todo(\n        title=todo.title,\n        description=todo.description,\n        state=todo.state,\n        user_id=user.id,\n    )\n    session.add(db_todo)\n    session.commit()\n    session.refresh(db_todo)\n\n    return db_todo\n

    Neste endpoint, fazemos uso da depend\u00eancia get_current_user que garante que somente usu\u00e1rios autenticados possam criar tarefas, protegendo assim nossa aplica\u00e7\u00e3o.

    "},{"location":"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):\n    response = client.post(\n        '/todos/',\n        headers={'Authorization': f'Bearer {token}'},\n        json={\n            'title': 'Test todo',\n            'description': 'Test todo description',\n            'state': 'draft',\n        },\n    )\n    assert response.json() == {\n        'id': 1,\n        'title': 'Test todo',\n        'description': 'Test todo description',\n        'state': 'draft',\n    }\n

    No teste, fazemos uma requisi\u00e7\u00e3o POST para o endpoint '/todos' passando um token de autentica\u00e7\u00e3o v\u00e1lido e um JSON com os dados da tarefa a ser criada. Em seguida, verificamos se a resposta cont\u00e9m os dados corretos da tarefa criada.

    Para executar este teste, voc\u00ea deve usar o comando abaixo no terminal:

    $ Execu\u00e7\u00e3o no terminal!
    task test tests/test_todos.py\n# ...\ntests/test_todos.py::test_create_todo PASSED\n

    Com essa implementa\u00e7\u00e3o, os testes devem passar. Por\u00e9m, apesar do sucesso dos testes, nosso c\u00f3digo ainda n\u00e3o est\u00e1 completamente pronto. Ainda \u00e9 necess\u00e1rio criar uma migra\u00e7\u00e3o para a tabela de tarefas no banco de dados.

    "},{"location":"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\n# ...\n\nGenerating /<caminho>/fast_zero/migrations/versions/de865434f506_create_todos_table.py\n

    Este comando gera um arquivo de migra\u00e7\u00e3o, que se parece com o c\u00f3digo abaixo:

    migrations/versions/de865434f506_create_todos_table.py
    def upgrade() -> None:\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.create_table('todos',\n    sa.Column('id', sa.Integer(), nullable=False),\n    sa.Column('title', sa.String(), nullable=False),\n    sa.Column('description', sa.String(), nullable=False),\n    sa.Column('state', sa.Enum('draft', 'todo', 'doing', 'done', 'trash', name='todostate'), nullable=False),\n    sa.Column('user_id', sa.Integer(), nullable=False),\n    sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),\n    sa.PrimaryKeyConstraint('id')\n    )\n    # ### end Alembic commands ###\n\n\ndef downgrade() -> None:\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.drop_table('todos')\n    # ### end Alembic commands ###\n

    Depois que a migra\u00e7\u00e3o for criada, precisamos aplic\u00e1-la ao nosso banco de dados. Execute o comando alembic upgrade head para aplicar a migra\u00e7\u00e3o.

    $ Execu\u00e7\u00e3o no terminal!
    alembic upgrade head\nINFO  [alembic.runtime.migration] Context impl SQLiteImpl.\nINFO  [alembic.runtime.migration] Will assume non-transactional DDL.\nINFO  [alembic.runtime.migration] Running upgrade e018397cecf4 -> de865434f506, create todos table\n

    Agora que a migra\u00e7\u00e3o foi aplicada, nosso banco de dados deve ter uma nova tabela de tarefas. Para verificar, voc\u00ea pode abrir o banco de dados com o comando sqlite3 database.db e depois executar o comando .schema para ver o esquema do banco de dados.

    $ Execu\u00e7\u00e3o no terminal!
    sqlite3 database.db\n# ...\nsqlite> .schema\n# ...\nCREATE TABLE todos (\n    id INTEGER NOT NULL,\n    title VARCHAR NOT NULL,\n    description VARCHAR NOT NULL,\n    state VARCHAR(5) NOT NULL,\n    user_id INTEGER NOT NULL,\n    PRIMARY KEY (id),\n    FOREIGN KEY(user_id) REFERENCES users (id)\n);\n

    Finalmente, agora que temos a tabela de tarefas em nosso banco de dados, podemos testar nosso endpoint de cria\u00e7\u00e3o de tarefas no Swagger. Para fazer isso, execute nosso servidor FastAPI e abra o Swagger no seu navegador.

    "},{"location":"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 tarefas \u00e9 fazer um filtro de busca. Em alguns momentos queremos buscar uma tarefa por t\u00edtulo, em outro momento por descri\u00e7\u00e3o, \u00e0s vezes s\u00f3 pelo estado. Por exemplo, somente tarefas conclu\u00eddas.

    Para fazer isso, podemos contar com um recurso do FastAPI chamado Query. A Query permite que definamos par\u00e2metros espec\u00edficos na URL, que podem ser utilizados para filtrar os resultados retornados pelo endpoint. Isso \u00e9 feito atrav\u00e9s da inclus\u00e3o de par\u00e2metros como query strings na URL, 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=TodoList)\ndef list_todos(\n    session: Session,\n    user: CurrentUser,\n    title: str = Query(None),\n    description: str = Query(None),\n    state: str = Query(None),\n    offset: int = Query(None),\n    limit: int = Query(None),\n):\n    query = select(Todo).where(Todo.user_id == user.id)\n\n    if title:\n        query = query.filter(Todo.title.contains(title))\n\n    if description:\n        query = query.filter(Todo.description.contains(description))\n\n    if state:\n        query = query.filter(Todo.state == state)\n\n    todos = session.scalars(query.offset(offset).limit(limit)).all()\n\n    return {'todos': todos}\n

    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\n\nfrom fast_zero.models import Todo, TodoState, User\n\n# ...\n\nclass TodoFactory(factory.Factory):\n    class Meta:\n        model = Todo\n\n    title = factory.Faker('text')\n    description = factory.Faker('text')\n    state = factory.fuzzy.FuzzyChoice(TodoState)\n    user_id = 1\n

    A fixture acima pode ser usada em diversos testes, reduzindo a duplica\u00e7\u00e3o de c\u00f3digo e melhorando a manuten\u00e7\u00e3o. Por exemplo, em um teste que precisa criar v\u00e1rios objetos Todo, voc\u00ea pode simplesmente usar a TodoFactory para criar esses objetos com uma \u00fanica linha de c\u00f3digo. A factory j\u00e1 cont\u00e9m a l\u00f3gica necess\u00e1ria para criar um objeto v\u00e1lido, e voc\u00ea pode facilmente sobrescrever qualquer um dos atributos, se necess\u00e1rio, para o caso de teste espec\u00edfico.

    A utiliza\u00e7\u00e3o de f\u00e1bricas tamb\u00e9m promove uma melhor separa\u00e7\u00e3o entre a l\u00f3gica de cria\u00e7\u00e3o do objeto e a l\u00f3gica do teste, tornando os testes mais leg\u00edveis e f\u00e1ceis de seguir. Com a TodoFactory, somos capazes de simular e testar diversos cen\u00e1rios de busca e filtragem, garantindo que nosso endpoint de listagem funcione corretamente em todas as situa\u00e7\u00f5es poss\u00edveis, aumentando assim a robustez e confiabilidade de nosso sistema.

    "},{"location":"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):\n    session.bulk_save_objects(TodoFactory.create_batch(5, user_id=user.id))\n    session.commit()\n\n    response = client.get(\n        '/todos/',\n        headers={'Authorization': f'Bearer {token}'},\n    )\n\n    assert len(response.json()['todos']) == 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):\n    session.bulk_save_objects(TodoFactory.create_batch(5, user_id=user.id))\n    session.commit()\n\n    response = client.get(\n        '/todos/?offset=1&limit=2',\n        headers={'Authorization': f'Bearer {token}'},\n    )\n\n    assert len(response.json()['todos']) == 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):\n    session.bulk_save_objects(\n        TodoFactory.create_batch(5, user_id=user.id, title='Test todo 1')\n    )\n    session.commit()\n\n    response = client.get(\n        '/todos/?title=Test todo 1',\n        headers={'Authorization': f'Bearer {token}'},\n    )\n\n    assert len(response.json()['todos']) == 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):\n    session.bulk_save_objects(\n        TodoFactory.create_batch(5, user_id=user.id, description='description')\n    )\n    session.commit()\n\n    response = client.get(\n        '/todos/?description=desc',\n        headers={'Authorization': f'Bearer {token}'},\n    )\n\n    assert len(response.json()['todos']) == 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):\n    session.bulk_save_objects(\n        TodoFactory.create_batch(5, user_id=user.id, state=TodoState.draft)\n    )\n    session.commit()\n\n    response = client.get(\n        '/todos/?state=draft',\n        headers={'Authorization': f'Bearer {token}'},\n    )\n\n    assert len(response.json()['todos']) == 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):\n    session.bulk_save_objects(\n        TodoFactory.create_batch(\n            5,\n            user_id=user.id,\n            title='Test todo combined',\n            description='combined description',\n            state=TodoState.done,\n        )\n    )\n\n    session.bulk_save_objects(\n        TodoFactory.create_batch(\n            3,\n            user_id=user.id,\n            title='Other title',\n            description='other description',\n            state=TodoState.todo,\n        )\n    )\n    session.commit()\n\n    response = client.get(\n        '/todos/?title=Test todo combined&description=combined&state=done',\n        headers={'Authorization': f'Bearer {token}'},\n    )\n\n    assert len(response.json()['todos']) == 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/#endpoint-de-alteracao","title":"Endpoint de Altera\u00e7\u00e3o","text":"

    Para fazer a altera\u00e7\u00e3o de uma tarefa, precisamos de um modelo onde tudo seja opcional, j\u00e1 que poder\u00edamos querer atualizar apenas um ou alguns campos da tarefa. Vamos criar o esquema TodoUpdate, no qual todos os campos s\u00e3o opcionais:

    fast_zero/schemas.py
    class TodoUpdate(BaseModel):\n    title: str | None = None\n    description: str | None = None\n    completed: str | None = None\n

    Para podermos alterar somente os valores que recebemos no modelo, temos que fazer um dump somente dos valores que recebemos e os atualizar no objeto que pegamos da base de dados:

    fast_zero/routes/todos.py
    @router.patch('/{todo_id}', response_model=TodoPublic)\ndef patch_todo(\n    todo_id: int, session: Session, user: CurrentUser, todo: TodoUpdate\n):\n    db_todo = session.scalar(\n        select(Todo).where(Todo.user_id == user.id, Todo.id == todo_id)\n    )\n\n    if not db_todo:\n        raise HTTPException(status_code=404, detail='Task not found.')\n\n    for key, value in todo.model_dump(exclude_unset=True).items():\n        setattr(db_todo, key, value)\n\n    session.add(db_todo)\n    session.commit()\n    session.refresh(db_todo)\n\n    return db_todo\n

    A linha for key, value in todo.model_dump(exclude_unset=True).items(): est\u00e1 iterando atrav\u00e9s de todos os campos definidos na inst\u00e2ncia todo do modelo de atualiza\u00e7\u00e3o. A fun\u00e7\u00e3o model_dump \u00e9 um m\u00e9todo que vem do modelo BaseModel do Pydantic e permite exportar o modelo para um dicion\u00e1rio.

    O par\u00e2metro exclude_unset=True \u00e9 importante aqui, pois significa que apenas os campos que foram explicitamente definidos (ou seja, aqueles que foram inclu\u00eddos na solicita\u00e7\u00e3o PATCH) ser\u00e3o inclu\u00eddos no dicion\u00e1rio resultante. Isso permite que voc\u00ea atualize apenas os campos que foram fornecidos na solicita\u00e7\u00e3o, deixando os outros inalterados.

    Depois de obter a chave e o valor de cada campo definido, a linha setattr(db_todo, key, value) \u00e9 usada para atualizar o objeto db_todo que representa a tarefa no banco de dados. A fun\u00e7\u00e3o setattr \u00e9 uma fun\u00e7\u00e3o embutida do Python que permite definir o valor de um atributo em um objeto. Neste caso, ele est\u00e1 definindo o atributo com o nome igual \u00e0 chave (ou seja, o nome do campo) no objeto db_todo com o valor correspondente.

    Dessa forma, garantimos que somente os campos enviados ao schema sejam atualizados no objeto.

    "},{"location":"08/#testes-para-o-endpoint-de-alteracao","title":"Testes para o Endpoint de Altera\u00e7\u00e3o","text":"

    Os testes aqui incluem o caso de atualiza\u00e7\u00e3o bem-sucedida e o caso de erro quando a tarefa n\u00e3o \u00e9 encontrada:

    fast_zero/tests/test_todos.py
    def test_patch_todo_error(client, token):\n    response = client.patch(\n        '/todos/10',\n        json={},\n        headers={'Authorization': f'Bearer {token}'},\n    )\n    assert response.status_code == 404\n    assert response.json() == {'detail': 'Task not found.'}\n\ndef test_patch_todo(session, client, user, token):\n    todo = TodoFactory(user_id=user.id)\n\n    session.add(todo)\n    session.commit()\n\n    response = client.patch(\n        f'/todos/{todo.id}',\n        json={'title': 'teste!'},\n        headers={'Authorization': f'Bearer {token}'},\n    )\n    assert response.status_code == 200\n    assert response.json()['title'] == 'teste!'\n

    Agora precisamos executar os testes para ver se est\u00e1 tudo correto:

    $ Execu\u00e7\u00e3o no terminal!
    task test tests/test_todos.py\n\n# ...\n\ntests/test_todos.py::test_create_todo PASSED\ntests/test_todos.py::test_list_todos PASSED\ntests/test_todos.py::test_list_todos_pagination PASSED\ntests/test_todos.py::test_list_todos_filter_title PASSED\ntests/test_todos.py::test_list_todos_filter_description PASSED\ntests/test_todos.py::test_list_todos_filter_state PASSED\ntests/test_todos.py::test_list_todos_filter_combined PASSED\ntests/test_todos.py::test_patch_todo_error PASSED\ntests/test_todos.py::test_patch_todo PASSED\n

    Com tudo funcionando, podemos partir para o nosso endpoint de DELETE.

    "},{"location":"08/#endpoint-de-delecao","title":"Endpoint de Dele\u00e7\u00e3o","text":"

    A rota para deletar uma tarefa \u00e9 simples e direta. Caso o todo exista, vamos deletar ele com a sesion caso n\u00e3o, retornamos 404:

    fast_zero/routes/todos.py
    @router.delete('/{todo_id}', response_model=Message)\ndef delete_todo(todo_id: int, session: Session, user: CurrentUser):\n    todo = session.scalar(\n        select(Todo).where(Todo.user_id == user.id, Todo.id == todo_id)\n    )\n\n    if not todo:\n        raise HTTPException(status_code=404, detail='Task not found.')\n\n    session.delete(todo)\n    session.commit()\n\n    return {'detail': 'Task has been deleted successfully.'}\n
    "},{"location":"08/#testes-para-o-endpoint-de-delecao","title":"Testes para o Endpoint de Dele\u00e7\u00e3o","text":"

    Esses testes verificam tanto a remo\u00e7\u00e3o bem-sucedida quanto o caso de erro quando a tarefa n\u00e3o \u00e9 encontrada:

    fast_zero/tests/test_todos.py
    def test_delete_todo(session, client, user, token):\n    todo = TodoFactory(id=1, user_id=user.id)\n\n    session.add(todo)\n    session.commit()\n\n    response = client.delete(\n        f'/todos/{todo.id}', headers={'Authorization': f'Bearer {token}'}\n    )\n\n    assert response.status_code == 200\n    assert response.json() == {'detail': 'Task has been deleted successfully.'}\n\ndef test_delete_todo_error(client, token):\n    response = client.delete(\n        f'/todos/{10}', headers={'Authorization': f'Bearer {token}'}\n    )\n\n    assert response.status_code == 404\n    assert response.json() == {'detail': 'Task not found.'}\n

    Por fim, precisamos executar os testes para ver se est\u00e1 tudo correto:

    $ Execu\u00e7\u00e3o no terminal!
    task test tests/test_todos.py\n\n# ...\n\ntests/test_todos.py::test_create_todo PASSED\ntests/test_todos.py::test_list_todos PASSED\ntests/test_todos.py::test_list_todos_pagination PASSED\ntests/test_todos.py::test_list_todos_filter_title PASSED\ntests/test_todos.py::test_list_todos_filter_description PASSED\ntests/test_todos.py::test_list_todos_filter_state PASSED\ntests/test_todos.py::test_list_todos_filter_combined PASSED\ntests/test_todos.py::test_delete_todo PASSED\ntests/test_todos.py::test_delete_todo_error PASSED\ntests/test_todos.py::test_patch_todo_error PASSED\ntests/test_todos.py::test_patch_todo PASSED\n
    "},{"location":"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":"

    Nesta aula exploramos os aspectos essenciais para construir uma API completa e funcional para gerenciar tarefas, integrando-se ao sistema de autentica\u00e7\u00e3o que j\u00e1 t\u00ednhamos desenvolvido.

    Iniciamos criando a estrutura de banco de dados para as tarefas, incluindo tabelas e migra\u00e7\u00f5es, e em seguida definimos os schemas necess\u00e1rios. A partir da\u00ed, trabalhamos na cria\u00e7\u00e3o dos endpoints para as opera\u00e7\u00f5es CRUD: cria\u00e7\u00e3o, leitura (listagem com filtragem), atualiza\u00e7\u00e3o (edi\u00e7\u00e3o) e exclus\u00e3o (dele\u00e7\u00e3o).

    Em cada est\u00e1gio, focamos na qualidade e na robustez, utilizando testes rigorosos para assegurar que os endpoints se comportassem conforme esperado. Exploramos tamb\u00e9m t\u00e9cnicas espec\u00edficas como atualiza\u00e7\u00e3o parcial e filtragem avan\u00e7ada, tornando a API flex\u00edvel e poderosa.

    O resultado foi um sistema integrado de gerenciamento de tarefas, ou um \"todo list\", ligado aos usu\u00e1rios e \u00e0 autentica\u00e7\u00e3o que j\u00e1 hav\u00edamos implementado. Esta aula refor\u00e7ou a import\u00e2ncia de um design cuidadoso e uma implementa\u00e7\u00e3o criteriosa, ilustrando como a FastAPI pode ser usada para criar APIs eficientes e profissionais.

    Agora que a nossa aplica\u00e7\u00e3o est\u00e1 crescendo e ganhando mais funcionalidades, na pr\u00f3xima aula, vamos mergulhar no mundo da dockeriza\u00e7\u00e3o. Iremos aprender a colocar a nossa aplica\u00e7\u00e3o dentro de um container Docker, facilitando o deploy e o escalonamento. Este \u00e9 um passo vital no desenvolvimento moderno de aplica\u00e7\u00f5es e estou ansioso para gui\u00e1-lo atrav\u00e9s dele. At\u00e9 l\u00e1!

    "},{"location":"09/","title":"Dockerizando a nossa aplica\u00e7\u00e3o e introduzindo o PostgreSQL","text":""},{"location":"09/#dockerizando-a-nossa-aplicacao-e-introduzindo-o-postgresql","title":"Dockerizando a nossa aplica\u00e7\u00e3o e introduzindo o PostgreSQL","text":"

    Objetivos da aula:

    • Compreender os conceitos b\u00e1sicos do Docker
    • Entender como criar uma imagem Docker para a nossa aplica\u00e7\u00e3o FastAPI
    • Aprender a rodar a aplica\u00e7\u00e3o utilizando Docker
    • Introduzir o conceito de Docker Compose para gerenciamento de m\u00faltiplos cont\u00eaineres
    • Aprender o que \u00e9 um Dockerfile e sua estrutura
    • Entender os benef\u00edcios e motivos da mudan\u00e7a de SQLite para PostgreSQL
    Caso prefira ver a aula em v\u00eddeo

    Aula Slides C\u00f3digo

    Depois de implementar nosso gerenciador de tarefas na aula anterior, temos uma primeira vers\u00e3o est\u00e1vel da nossa aplica\u00e7\u00e3o. Nesta aula, al\u00e9m de aprendermos a \"dockerizar\" nossa aplica\u00e7\u00e3o FastAPI, tamb\u00e9m abordaremos a migra\u00e7\u00e3o do banco de dados SQLite para o PostgreSQL.

    "},{"location":"09/#o-docker-e-a-nossa-aplicacao","title":"O Docker e a nossa aplica\u00e7\u00e3o","text":"

    Docker \u00e9 uma plataforma aberta que permite automatizar o processo de implanta\u00e7\u00e3o, escalonamento e opera\u00e7\u00e3o de aplica\u00e7\u00f5es dentro de cont\u00eaineres. Ele serve para \"empacotar\" uma aplica\u00e7\u00e3o e suas depend\u00eancias em um cont\u00eainer virtual que pode ser executado em qualquer sistema operacional que suporte Docker. Isso facilita a implanta\u00e7\u00e3o, o desenvolvimento e o compartilhamento de aplica\u00e7\u00f5es, al\u00e9m de proporcionar um ambiente isolado e consistente.

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

    Para criar um container Docker, escrevemos uma lista de passos de como construir o ambiente para execu\u00e7\u00e3o da nossa aplica\u00e7\u00e3o em um arquivo chamado Dockerfile. Ele define o ambiente de execu\u00e7\u00e3o, os comandos necess\u00e1rios para preparar o ambiente e o comando a ser executado quando um cont\u00eainer \u00e9 iniciado a partir da imagem.

    Uma das coisas interessantes sobre Docker \u00e9 que existe um Hub de containers prontos onde a comunidade hospeda imagens \"prontas\", que podemos usar como ponto de partida. Por exemplo, a comunidade de python mant\u00e9m um grupo de imagens com o ambiente python pronto para uso. Podemos partir dessa imagem com o python j\u00e1 instalado adicionar os passos para que nossa aplica\u00e7\u00e3o seja executada.

    Aqui est\u00e1 um exemplo de Dockerfile para executar nossa aplica\u00e7\u00e3o:

    FROM python:3.11-slim\nENV POETRY_VIRTUALENVS_CREATE=false\n\nWORKDIR app/\nCOPY . .\n\nRUN pip install poetry\n\nRUN poetry config installer.max-workers 10\nRUN poetry install --no-interaction --no-ansi\n\nEXPOSE 800\nCMD [ \"poetry\", \"run\", \"uvicorn\", \"--host\", \"0.0.0.0\", \"fast_zero.app:app\" ]\n

    Aqui est\u00e1 o que cada linha faz:

    1. FROM python:3.11-slim: Define a imagem base para nosso cont\u00eainer. Estamos usando a vers\u00e3o slim da imagem do Python 3.11, que tem tudo que precisamos para rodar nossa aplica\u00e7\u00e3o.
    2. ENV POETRY_VIRTUALENVS_CREATE=false: Define uma vari\u00e1vel de ambiente que diz ao Poetry para n\u00e3o criar um ambiente virtual. (O container j\u00e1 \u00e9 um ambiente isolado)
    3. RUN pip install poetry: Instala o Poetry, nosso gerenciador de pacotes.
    4. WORKDIR app/: Define o diret\u00f3rio em que executaremos os comandos a seguir.
    5. COPY . .: Copia todos os arquivos do diret\u00f3rio atual para o cont\u00eainer.
    6. RUN poetry config installer.max-workers 10: Configura o Poetry para usar at\u00e9 10 workers ao instalar pacotes.
    7. RUN poetry install --no-interaction --no-ansi: Instala as depend\u00eancias do nosso projeto sem intera\u00e7\u00e3o e sem cores no output.
    8. EXPOSE 8000: Informa ao Docker que o cont\u00eainer vai escutar na porta 8000.
    9. CMD [ \"poetry\", \"run\", \"uvicorn\", \"--host\", \"0.0.0.0\", \"fast_zero.app:app\" ]: Define o comando que ser\u00e1 executado quando o cont\u00eainer for iniciado.

    Vamos entender melhor esse \u00faltimo comando:

    • 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. Especificamente, \"0.0.0.0\" \u00e9 um endere\u00e7o IP que permite que o servidor aceite conex\u00f5es de qualquer endere\u00e7o de rede dispon\u00edvel, tornando-o acess\u00edvel externamente.
    • 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/#executando-o-container","title":"Executando o container","text":"

    Para executar o cont\u00eainer, usamos o comando docker run. Especificamos o nome do cont\u00eainer com a flag --name, indicamos a imagem que queremos executar e a tag que queremos usar <nome_da_imagem>:<tag>. A flag -p serve para mapear a porta do host para a porta do cont\u00eainer <porta_do_host>:<porta_do_cont\u00eainer>. Portanto, teremos o seguinte comando:

    $ Execu\u00e7\u00e3o no terminal!
    docker run --name fastzeroapp -p 8000:8000 fast_zero:latest\n

    Este comando iniciar\u00e1 nossa aplica\u00e7\u00e3o dentro de um cont\u00eainer Docker, que estar\u00e1 escutando na porta 8000. Para testar se tudo est\u00e1 funcionando corretamente, voc\u00ea pode acessar http://localhost:8000 em um navegador ou usar um comando como:

    $ Execu\u00e7\u00e3o no terminal!
    curl http://localhost:8000\n
    Caso voc\u00ea fique preso no terminal

    Caso voc\u00ea tenha a aplica\u00e7\u00e3o travada no terminal e n\u00e3o consiga sair, voc\u00ea pode teclar Ctrl+C para parar a execu\u00e7\u00e3o do container.

    "},{"location":"09/#gerenciando-containers-docker","title":"Gerenciando Containers docker","text":"

    Quando voc\u00ea trabalha com Docker, \u00e9 importante saber como gerenciar os cont\u00eaineres. Aqui est\u00e3o algumas opera\u00e7\u00f5es b\u00e1sicas para gerenci\u00e1-los:

    1. Rodar um cont\u00eainer em background: Se voc\u00ea deseja executar o cont\u00eainer em segundo plano para que n\u00e3o ocupe o terminal, pode usar a op\u00e7\u00e3o -d:

      $ Execu\u00e7\u00e3o no terminal!
      docker run -d --name fastzeroapp -p 8000:8000 fast_zero:latest\n
    2. Parar um cont\u00eainer: Quando voc\u00ea \"para\" um cont\u00eainer, est\u00e1 essencialmente interrompendo a execu\u00e7\u00e3o do processo principal do cont\u00eainer. Isso significa que o cont\u00eainer n\u00e3o est\u00e1 mais ativo, mas ainda existe no sistema, junto com seus dados associados e configura\u00e7\u00e3o. Isso permite que voc\u00ea reinicie o cont\u00eainer posteriormente, se desejar.

      $ Execu\u00e7\u00e3o no terminal!
      docker stop fastzeroapp\n
    3. Remover um cont\u00eainer: Ao \"remover\" um cont\u00eainer, voc\u00ea est\u00e1 excluindo o cont\u00eainer do sistema. Isso significa que todos os dados associados ao cont\u00eainer s\u00e3o apagados. Uma vez que um cont\u00eainer \u00e9 removido, voc\u00ea n\u00e3o pode reinici\u00e1-lo; no entanto, voc\u00ea pode sempre criar um novo cont\u00eainer a partir da mesma imagem.

      $ Execu\u00e7\u00e3o no terminal!
      docker rm fastzeroapp\n

    Ambos os comandos (stop e rm) usam o nome do cont\u00eainer que definimos anteriormente com a flag --name. \u00c9 uma boa pr\u00e1tica manter a gest\u00e3o dos seus cont\u00eaineres, principalmente durante o desenvolvimento, para evitar um uso excessivo de recursos ou conflitos de nomes e portas.

    "},{"location":"09/#introduzindo-o-postgresql","title":"Introduzindo o postgreSQL","text":"

    O PostgreSQL \u00e9 um Sistema de Gerenciamento de Banco de Dados Objeto-Relacional (ORDBMS) poderoso e de c\u00f3digo aberto. Ele \u00e9 amplamente utilizado em produ\u00e7\u00e3o em muitos projetos devido \u00e0 sua robustez, escalabilidade e conjunto de recursos extensos.

    Mudar para um banco de dados como PostgreSQL tem v\u00e1rios benef\u00edcios:

    • Escalabilidade: SQLite n\u00e3o \u00e9 ideal para aplica\u00e7\u00f5es em larga escala ou com grande volume de dados. PostgreSQL foi projetado para lidar com uma grande quantidade de dados e requisi\u00e7\u00f5es.
    • Concorr\u00eancia: Diferentemente do SQLite, que tem limita\u00e7\u00f5es para grava\u00e7\u00f5es simult\u00e2neas, o PostgreSQL suporta m\u00faltiplas opera\u00e7\u00f5es simult\u00e2neas.
    • Funcionalidades avan\u00e7adas: PostgreSQL vem com v\u00e1rias extens\u00f5es e funcionalidades que o SQLite pode n\u00e3o oferecer.

    Al\u00e9m disso, SQLite tem algumas limita\u00e7\u00f5es que podem torn\u00e1-lo inadequado para produ\u00e7\u00e3o em alguns casos. Por exemplo, ele n\u00e3o suporta alta concorr\u00eancia e pode ter problemas de performance com grandes volumes de dados.

    Nota

    Embora para o escopo da nossa aplica\u00e7\u00e3o e os objetivos de aprendizado o SQLite pudesse ser suficiente, \u00e9 sempre bom nos prepararmos para cen\u00e1rios de produ\u00e7\u00e3o real. A ado\u00e7\u00e3o de PostgreSQL nos d\u00e1 uma pr\u00e9via das pr\u00e1ticas do mundo real e garante que nossa aplica\u00e7\u00e3o possa escalar sem grandes modifica\u00e7\u00f5es de infraestrutura.

    "},{"location":"09/#como-executar-o-postgres","title":"Como executar o postgres?","text":"

    Embora o PostgreSQL seja poderoso, sua instala\u00e7\u00e3o direta em uma m\u00e1quina real pode ser desafiadora e pode resultar em configura\u00e7\u00f5es diferentes entre os ambientes de desenvolvimento. Felizmente, podemos utilizar o Docker para resolver esse problema. No Docker Hub, est\u00e3o dispon\u00edveis imagens pr\u00e9-constru\u00eddas do PostgreSQL, permitindo-nos executar o PostgreSQL com um \u00fanico comando. Confira a imagem oficial do PostgreSQL.

    Para executar um cont\u00eainer do PostgreSQL, use o seguinte comando:

    $ Execu\u00e7\u00e3o no terminal!
    docker run -d \\\n    --name app_database \\\n    -e POSTGRES_USER=app_user \\\n    -e POSTGRES_DB=app_db \\\n    -e POSTGRES_PASSWORD=app_password \\\n    -p 5432:5432 \\\n    postgres\n
    "},{"location":"09/#explicando-as-flags-e-configuracoes","title":"Explicando as Flags e Configura\u00e7\u00f5es","text":"
    • Flag -e:

    Esta flag \u00e9 usada para definir vari\u00e1veis de ambiente no cont\u00eainer. No contexto do PostgreSQL, essas vari\u00e1veis s\u00e3o essenciais. Elas configuram o nome de usu\u00e1rio, nome do banco de dados, e senha durante a primeira execu\u00e7\u00e3o do cont\u00eainer. Sem elas, o PostgreSQL pode n\u00e3o iniciar da forma esperada. \u00c9 uma forma pr\u00e1tica de configurar o PostgreSQL sem interagir manualmente ou criar arquivos de configura\u00e7\u00e3o.

    • Porta 5432:

    O PostgreSQL, por padr\u00e3o, escuta por conex\u00f5es na porta 5432. Mapeando esta porta do cont\u00eainer para a mesma porta no host (usando -p), fazemos com que o PostgreSQL seja acess\u00edvel nesta porta na m\u00e1quina anfitri\u00e3, permitindo que outras aplica\u00e7\u00f5es se conectem a ele.

    Sobre as vari\u00e1veis

    Os valores acima (app_user, app_db, e app_password) s\u00e3o padr\u00f5es gen\u00e9ricos para facilitar a inicializa\u00e7\u00e3o do PostgreSQL em um ambiente de desenvolvimento. No entanto, \u00e9 altamente recomend\u00e1vel que voc\u00ea altere esses valores, especialmente app_password, para garantir a seguran\u00e7a do seu banco de dados.

    "},{"location":"09/#volumes-e-persistencia-de-dados","title":"Volumes e Persist\u00eancia de Dados","text":"

    Para garantir a persist\u00eancia dos dados entre execu\u00e7\u00f5es do cont\u00eainer, utilizamos volumes. Um volume mapeia um diret\u00f3rio do sistema host para um diret\u00f3rio no cont\u00eainer. Isso \u00e9 crucial para bancos de dados, pois sem um volume, ao remover o cont\u00eainer, todos os dados armazenados dentro dele se perderiam.

    No PostgreSQL, o diret\u00f3rio padr\u00e3o para armazenamento de dados \u00e9 /var/lib/postgresql/data. Mapeamos esse diret\u00f3rio para um volume (neste caso \"pgdata\") em nossa m\u00e1quina host para garantir a persist\u00eancia dos dados:

    $ Execu\u00e7\u00e3o no terminal!
    docker run -d \\\n    --name app_database \\\n    -e POSTGRES_USER=app_user \\\n    -e POSTGRES_DB=app_db \\\n    -e POSTGRES_PASSWORD=app_password \\\n    -v pgdata:/var/lib/postgresql/data \\\n    -p 5432:5432 \\\n    postgres\n

    O par\u00e2metro do volume \u00e9 passado ao cont\u00eainer usando o par\u00e2metro -v Dessa forma, os dados do banco continuar\u00e3o existindo, mesmo que o cont\u00eainer seja reiniciado ou removido.

    "},{"location":"09/#adicionando-o-suporte-ao-postgresql-na-nossa-aplicacao","title":"Adicionando o suporte ao PostgreSQL na nossa aplica\u00e7\u00e3o","text":"

    Para que o SQLAlchemy suporte o PostgreSQL, precisamos instalar uma depend\u00eancia chamada psycopg2-binary. Este \u00e9 o adaptador PostgreSQL para Python e \u00e9 crucial para fazer a comunica\u00e7\u00e3o.

    Para instalar essa depend\u00eancia, utilize o seguinte comando:

    $ Execu\u00e7\u00e3o no terminal!
    poetry add psycopg2-binary\n

    Uma das vantagens do SQLAlchemy enquanto ORM \u00e9 a flexibilidade. Com apenas algumas altera\u00e7\u00f5es m\u00ednimas, como a atualiza\u00e7\u00e3o da string de conex\u00e3o, podemos facilmente transicionar para um banco de dados diferente. Assim, ap\u00f3s ajustar o arquivo .env com a string de conex\u00e3o do PostgreSQL, a aplica\u00e7\u00e3o dever\u00e1 operar normalmente, mas desta vez utilizando o PostgreSQL.

    Para ajustar a conex\u00e3o com o PostgreSQL, modifique seu arquivo .env para incluir a seguinte string de conex\u00e3o:

    .env
    DATABASE_URL=\"postgresql://app_user:app_password@localhost:5432/app_db\"\n

    Caso tenha alterado as vari\u00e1veis de ambiente do cont\u00eainer

    Se voc\u00ea alterou app_user, app_password ou app_db ao inicializar o cont\u00eainer PostgreSQL, garanta que esses valores sejam refletidos na string de conex\u00e3o acima. A palavra localhost indica que o banco de dados PostgreSQL est\u00e1 sendo executado na mesma m\u00e1quina que sua aplica\u00e7\u00e3o. Se o banco de dados estiver em uma m\u00e1quina diferente, substitua localhost pelo endere\u00e7o IP correspondente e, se necess\u00e1rio, ajuste a porta 5432.

    "},{"location":"09/#executando-as-migracoes","title":"Executando as migra\u00e7\u00f5es","text":"

    Migra\u00e7\u00f5es s\u00e3o como vers\u00f5es para seu banco de dados, permitindo que voc\u00ea atualize sua estrutura de forma ordenada e controlada. Sempre que mudamos de banco de dados, ou at\u00e9 mesmo quando alteramos sua estrutura, as migra\u00e7\u00f5es precisam ser executadas para garantir que a base de dados esteja em sincronia com nosso c\u00f3digo.

    No contexto de cont\u00eaineres, rodar as migra\u00e7\u00f5es se torna ainda mais simples. Quando mudamos de banco de dados, como \u00e9 o caso de termos sa\u00eddo de um SQLite (por exemplo) para um PostgreSQL, as migra\u00e7\u00f5es s\u00e3o essenciais. O motivo \u00e9 simples: o novo banco de dados n\u00e3o ter\u00e1 a estrutura e os dados do antigo, a menos que migremos. As migra\u00e7\u00f5es ir\u00e3o garantir que o novo banco de dados tenha a mesma estrutura e rela\u00e7\u00f5es que o anterior.

    Antes de executar o proximo comando

    Assegure-se de que ambos os cont\u00eaineres, tanto da aplica\u00e7\u00e3o quanto do banco de dados, estejam ativos. O cont\u00eainer do banco de dados deve estar rodando para que a aplica\u00e7\u00e3o possa se conectar a ele.

    Assegure-se de que o cont\u00eainer da aplica\u00e7\u00e3o esteja ativo. Estamos usando a flag --network=host para que o cont\u00eainer use a rede do host. Isso pode ser essencial para evitar problemas de conex\u00e3o, j\u00e1 que n\u00e3o podemos prever como est\u00e1 configurada a rede do computador onde este comando ser\u00e1 executado.

    execu\u00e7\u00e3o no terminal
    docker run -d --network=host --name fastzeroapp -p 8000:8000 fast_zero:latest\n

    Para aplicar migra\u00e7\u00f5es em um ambiente com cont\u00eaineres, frequentemente temos comandos espec\u00edficos associados ao servi\u00e7o. Vejamos como executar migra\u00e7\u00f5es usando o Docker:

    $ Execu\u00e7\u00e3o no terminal!
    docker exec -it fastzeroapp poetry run alembic upgrade head\n

    O comando docker exec \u00e9 usado para invocar um comando espec\u00edfico dentro de um cont\u00eainer em execu\u00e7\u00e3o. A op\u00e7\u00e3o -it \u00e9 uma combina\u00e7\u00e3o de -i (interativo) e -t (pseudo-TTY), que juntas garantem um terminal interativo, permitindo a comunica\u00e7\u00e3o direta com o cont\u00eainer.

    Ap\u00f3s executar as migra\u00e7\u00f5es, voc\u00ea pode verificar a cria\u00e7\u00e3o das tabelas utilizando um sistema de gerenciamento de banco de dados. A seguir, apresentamos um exemplo com o Beekeeper Studio:

    Lembre-se: Embora as tabelas estejam agora criadas e estruturadas, o banco de dados ainda n\u00e3o cont\u00e9m os dados anteriormente presentes no SQLite ou em qualquer outro banco que voc\u00ea estivesse utilizando antes.

    "},{"location":"09/#simplificando-nosso-fluxo-com-docker-compose","title":"Simplificando nosso fluxo com docker-compose","text":"

    Docker Compose \u00e9 uma ferramenta que permite definir e gerenciar aplicativos multi-cont\u00eainer com Docker. \u00c9 como se voc\u00ea tivesse um maestro conduzindo uma orquestra: o maestro (ou Docker Compose) garante que todos os m\u00fasicos (ou cont\u00eaineres) toquem em harmonia. Definimos nossa aplica\u00e7\u00e3o e servi\u00e7os relacionados, como o PostgreSQL, em um arquivo docker-compose.yml e os gerenciamos juntos atrav\u00e9s de comandos simplificados.

    Ao adotar o Docker Compose, facilitamos o desenvolvimento e a execu\u00e7\u00e3o da nossa aplica\u00e7\u00e3o com seus servi\u00e7os dependentes utilizando um \u00fanico comando.

    "},{"location":"09/#criacao-do-docker-composeyml","title":"Cria\u00e7\u00e3o do docker-compose.yml","text":"docker-compose.yaml
    version: '3'\n\nservices:\n  fastzero_database:\n    image: postgres\n    volumes:\n      - pgdata:/var/lib/postgresql/data\n    environment:\n      POSTGRES_USER: app_user\n      POSTGRES_DB: app_db\n      POSTGRES_PASSWORD: app_password\n    ports:\n      - \"5432:5432\"\n\n  fastzero_app:\n    image: fastzero_app\n    build:\n      context: .\n      dockerfile: Dockerfile\n    ports:\n      - \"8000:8000\"\n    depends_on:\n      - fastzero_database\n    environment:\n      DATABASE_URL: postgresql://app_user:app_password@fastzero_database:5432/app_db\n\nvolumes:\n  pgdata:\n

    Explica\u00e7\u00e3o linha a linha:

    1. version: '3': Especifica a vers\u00e3o do formato do arquivo Compose. O n\u00famero '3' \u00e9 uma das vers\u00f5es mais recentes e amplamente usadas.

    2. services:: Define os servi\u00e7os (cont\u00eaineres) que ser\u00e3o gerenciados.

    3. fastzero_database:: Define nosso servi\u00e7o de banco de dados PostgreSQL.

    4. image: postgres: Usa a imagem oficial do PostgreSQL.

    5. volumes:: Mapeia volumes para persist\u00eancia de dados.

    6. pgdata:/var/lib/postgresql/data: Cria ou usa um volume chamado \"pgdata\" e o mapeia para o diret\u00f3rio /var/lib/postgresql/data no cont\u00eainer.

    7. environment:: Define vari\u00e1veis de ambiente para o servi\u00e7o.

    8. fastzero_app:: Define o servi\u00e7o para nossa aplica\u00e7\u00e3o.

    9. image: fastzero_app: Usa a imagem Docker da nossa aplica\u00e7\u00e3o.

    10. build: : Instru\u00e7\u00f5es para construir a imagem se n\u00e3o estiver dispon\u00edvel, nosso Dockerfile.

    11. ports:: Mapeia portas do cont\u00eainer para o host.

    12. \"8000:8000\": Mapeia a porta 8000 do cont\u00eainer para a porta 8000 do host.

    13. depends_on:: Especifica que fastzero_app depende de fastzero_database. Isto garante que o banco de dados seja iniciado antes da aplica\u00e7\u00e3o.

    14. DATABASE_URL: ...: \u00c9 uma vari\u00e1vel de ambiente que nossa aplica\u00e7\u00e3o usar\u00e1 para se conectar ao banco de dados. Aqui, ele se conecta ao servi\u00e7o fastzero_database que definimos anteriormente.

    15. volumes: (n\u00edvel superior): Define volumes que podem ser usados pelos servi\u00e7os.

    16. pgdata:: Define um volume chamado \"pgdata\". Este volume \u00e9 usado para persistir os dados do PostgreSQL entre as execu\u00e7\u00f5es do cont\u00eainer.

    Sobre o docker-compose

    Para usar o Docker Compose, voc\u00ea precisa t\u00ea-lo instalado em seu sistema. Ele n\u00e3o est\u00e1 inclu\u00eddo na instala\u00e7\u00e3o padr\u00e3o do Docker, ent\u00e3o lembre-se de instal\u00e1-lo separadamente!

    O guia oficial de instala\u00e7\u00e3o pode ser encontrado aqui

    Com este arquivo docker-compose.yml, voc\u00ea pode iniciar ambos os servi\u00e7os (aplica\u00e7\u00e3o e banco de dados) simultaneamente usando:

    docker-compose up\n

    Para parar os servi\u00e7os e manter os dados seguros nos volumes definidos, use:

    docker-compose down\n

    Esses comandos simplificam o fluxo de trabalho e garantem que os servi\u00e7os iniciem corretamente e se comuniquem conforme o esperado.

    Execu\u00e7\u00e3o em modo desanexado

    Voc\u00ea pode iniciar os servi\u00e7os em segundo plano com a flag -d usando docker-compose up -d. Isso permite que os cont\u00eaineres rodem em segundo plano, liberando o terminal para outras tarefas.

    "},{"location":"09/#rodando-as-migracoes-de-forma-automatica","title":"Rodando as migra\u00e7\u00f5es de forma autom\u00e1tica","text":"

    Automatizar as migra\u00e7\u00f5es do banco de dados \u00e9 uma pr\u00e1tica recomendada para garantir que sua aplica\u00e7\u00e3o esteja sempre sincronizada com o estado mais atual do seu esquema de banco de dados. \u00c9 como preparar todos os ingredientes antes de come\u00e7ar a cozinhar: voc\u00ea garante que tudo o que \u00e9 necess\u00e1rio est\u00e1 pronto para ser usado.

    Para automatizar as migra\u00e7\u00f5es em nossos cont\u00eaineres Docker, utilizamos um entrypoint. O entrypoint define o comando que ser\u00e1 executado quando o cont\u00eainer iniciar. Em outras palavras, \u00e9 o primeiro ponto de entrada de execu\u00e7\u00e3o do cont\u00eainer.

    Por que usar o Entrypoint?

    No Docker, o entrypoint permite que voc\u00ea configure um ambiente de cont\u00eainer que ser\u00e1 executado como um execut\u00e1vel. \u00c9 \u00fatil para preparar o ambiente, como realizar migra\u00e7\u00f5es de banco de dados, antes de iniciar a aplica\u00e7\u00e3o propriamente dita. Isso significa que qualquer comando definido no CMD do Dockerfile n\u00e3o ser\u00e1 executado automaticamente se um entrypoint estiver definido. Em vez disso, precisamos incluir explicitamente esse comando no script de entrypoint.

    Implementando o Entrypoint

    Criamos um script chamado entrypoint.sh que ir\u00e1 preparar nosso ambiente antes de a aplica\u00e7\u00e3o iniciar:

    entrypoin.sh
    #!/bin/sh\n\n# Executa as migra\u00e7\u00f5es do banco de dados\npoetry run alembic upgrade head\n\n# Inicia a aplica\u00e7\u00e3o\npoetry run uvicorn --host 0.0.0.0 --port 8000 fast_zero.app:app\n

    Explica\u00e7\u00e3o Detalhada do Script:

    • #!/bin/sh: Indica ao sistema operacional que o script deve ser executado no shell Unix.
    • poetry run alembic upgrade head: Roda as migra\u00e7\u00f5es do banco de dados at\u00e9 a \u00faltima vers\u00e3o.
    • poetry run uvicorn --host 0.0.0.0 --port 8000 fast_zero.app:app: Inicia a aplica\u00e7\u00e3o. Este \u00e9 o comando que normalmente estaria no CMD do Dockerfile, mas agora est\u00e1 inclu\u00eddo no entrypoint para garantir que as migra\u00e7\u00f5es sejam executadas antes do servidor iniciar.

    Como Funciona na Pr\u00e1tica?

    Quando o cont\u00eainer \u00e9 iniciado, o Docker executa o script de entrypoint, que por sua vez executa as migra\u00e7\u00f5es e s\u00f3 ent\u00e3o inicia a aplica\u00e7\u00e3o. Isso garante que o banco de dados esteja atualizado com as \u00faltimas migra\u00e7\u00f5es antes de qualquer intera\u00e7\u00e3o com a aplica\u00e7\u00e3o.

    Visualizando o Processo:

    Voc\u00ea pode pensar no entrypoint.sh como o ato de aquecer e verificar todos os instrumentos antes de uma apresenta\u00e7\u00e3o musical. Antes de a m\u00fasica come\u00e7ar, cada instrumento \u00e9 afinado e testado. Da mesma forma, nosso script assegura que o banco de dados est\u00e1 em harmonia com a aplica\u00e7\u00e3o antes de ela come\u00e7ar a receber requisi\u00e7\u00f5es.

    Adicionando o Entrypoint ao Docker Compose:

    Inclu\u00edmos o entrypoint no nosso servi\u00e7o no arquivo docker-compose.yml, garantindo que esteja apontando para o script correto:

    docker-compose.yaml
      fastzero_app:\n    image: fastzero_app\n    entrypoint: ./entrypoint.sh\n    build:\n      context: .\n      dockerfile: Dockerfile\n

    Reconstruindo e Executando com Novas Configura\u00e7\u00f5es:

    Para aplicar as altera\u00e7\u00f5es, reconstru\u00edmos e executamos os servi\u00e7os com a op\u00e7\u00e3o --build:

    $ Execu\u00e7\u00e3o no terminal!
    docker-compose up --build\n

    Observando o Comportamento Esperado:

    Quando o cont\u00eainer \u00e9 iniciado, voc\u00ea deve ver as migra\u00e7\u00f5es sendo aplicadas, seguidas pela inicializa\u00e7\u00e3o da aplica\u00e7\u00e3o:

    $ Exemplo do resultado no terminal!
    fastzero_app-1  | INFO  [alembic.runtime.migration] Context impl PostgresqlImpl.\nfastzero_app-1  | INFO  [alembic.runtime.migration] Will assume transactional DDL.\nfastzero_app-1  | INFO:     Started server process [10]\nfastzero_app-1  | INFO:     Waiting for application startup.\nfastzero_app-1  | INFO:     Application startup complete.\nfastzero_app-1  | INFO:     Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)\n

    Este processo garante que as migra\u00e7\u00f5es do banco de dados s\u00e3o realizadas automaticamente, mantendo a base de dados alinhada com a aplica\u00e7\u00e3o e pronta para a\u00e7\u00e3o assim que o servidor Uvicorn entra em cena.

    Nota de revis\u00e3o sobre vari\u00e1veis de ambiente

    Utilizar vari\u00e1veis de ambiente definidas em um arquivo .env \u00e9 uma pr\u00e1tica recomendada para cen\u00e1rios de produ\u00e7\u00e3o devido \u00e0 seguran\u00e7a que oferece. No entanto, para manter a simplicidade e o foco nas funcionalidades do FastAPI neste curso, optamos por explicitar essas vari\u00e1veis no docker-compose.yml. Isso \u00e9 particularmente relevante, pois o Docker Compose \u00e9 utilizado apenas para o ambiente de desenvolvimento; no deploy para fly.io, o qual \u00e9 o nosso foco, o compose n\u00e3o ser\u00e1 utilizado em produ\u00e7\u00e3o.

    Ainda assim, \u00e9 valioso mencionar como essa configura\u00e7\u00e3o mais segura seria realizada, especialmente para aqueles que planejam utilizar o Docker Compose em produ\u00e7\u00e3o.

    Em ambientes de produ\u00e7\u00e3o com Docker Compose, \u00e9 uma boa pr\u00e1tica gerenciar vari\u00e1veis de ambiente sens\u00edveis, como credenciais, por meio de um arquivo .env. Isso previne a exposi\u00e7\u00e3o dessas informa\u00e7\u00f5es diretamente no arquivo docker-compose.yml, contribuindo para a seguran\u00e7a do projeto.

    As vari\u00e1veis de ambiente podem ser definidas em nosso arquivo .env localizado na raiz do projeto:

    .env
    POSTGRES_USER=app_user\nPOSTGRES_DB=app_db\nPOSTGRES_PASSWORD=app_password\nDATABASE_URL=postgresql://app_user:app_password@fastzero_database:5432/app_db\n

    Para aplicar essas vari\u00e1veis, referencie o arquivo .env no docker-compose.yml:

    docker-compose.yaml
    services:\n  fastzero_database:\n    image: postgres\n    env_file:\n      - .env\n    # Restante da configura\u00e7\u00e3o...\n\n  fastzero_app:\n    build: .\n    env_file:\n      - .env\n    # Restante da configura\u00e7\u00e3o...\n

    Adotar essa abordagem evita a exposi\u00e7\u00e3o das vari\u00e1veis de ambiente no arquivo de configura\u00e7\u00e3o. Esta n\u00e3o foi a abordagem padr\u00e3o no curso devido \u00e0 complexidade adicional e \u00e0 inten\u00e7\u00e3o de evitar confus\u00f5es. Dependendo do ambiente estabelecido pela equipe de DevOps/SRE em um projeto real, essa gest\u00e3o pode variar entre vari\u00e1veis de ambiente, arquivos .env ou solu\u00e7\u00f5es mais avan\u00e7adas como Vault.

    Se optar por utilizar um arquivo .env com as configura\u00e7\u00f5es do PostgreSQL, configure o Pydantic para ignorar vari\u00e1veis de ambiente que n\u00e3o s\u00e3o necess\u00e1rias, adicionando extra='ignore' a chamada de SettingsConfigDic:

    fast_zero/settings.py
    from pydantic_settings import BaseSettings, SettingsConfigDict\n\n\nclass Settings(BaseSettings):\n    model_config = SettingsConfigDict(\n        env_file='.env', env_file_encoding='utf-8', extra='ignore'\n    )\n\n    DATABASE_URL: str\n    SECRET_KEY: str\n    ALGORITHM: str\n    ACCESS_TOKEN_EXPIRE_MINUTES: int\n

    Com essa configura\u00e7\u00e3o, o Pydantic ir\u00e1 ignorar quaisquer vari\u00e1veis no .env que n\u00e3o sejam explicitamente declaradas na classe Settings, evitando assim conflitos e erros inesperados.

    Agradecimentos especiais a @vcwild e @williangl pelas revis\u00f5es valiosas nesta aula que me fizeram criar essa nota.

    "},{"location":"09/#testes-com-docker","title":"Testes com Docker","text":"

    Agora que temos o docker-compose configurado, realizar testes tornou-se uma tarefa simplificada. Podemos executar toda a su\u00edte de testes com um \u00fanico comando, sem a necessidade de ajustes adicionais ou configura\u00e7\u00f5es complexas. Isso \u00e9 poss\u00edvel devido \u00e0 maneira como o docker-compose gerencia os servi\u00e7os e suas depend\u00eancias.

    Para executar os testes, utilizamos o comando:

    $ Execu\u00e7\u00e3o no terminal!
    docker-compose run --entrypoint=\"poetry run task test\" fastzero_app\n

    Vamos entender melhor o que cada parte do comando faz:

    • docker-compose run: Este comando executa um servi\u00e7o \u00fanico definido no seu arquivo docker-compose.yml. Ao contr\u00e1rio do docker-compose up, que inicia todos os servi\u00e7os, o run permite que voc\u00ea inicie um servi\u00e7o espec\u00edfico.

    • --entrypoint: A flag --entrypoint substitui o ponto de entrada padr\u00e3o do container. O ponto de entrada padr\u00e3o \u00e9 especificado no Dockerfile ou no docker-compose.yml e normalmente \u00e9 o comando que inicia sua aplica\u00e7\u00e3o. Ao substituir o entrypoint, voc\u00ea pode executar comandos diferentes, como neste caso, onde queremos rodar nossos testes.

    • \"poetry run task test\": Este \u00e9 o comando que substituir\u00e1 o entrypoint padr\u00e3o. Ele diz ao Docker para iniciar o container e executar a nossa su\u00edte de testes com o Poetry, uma ferramenta para gerenciamento de depend\u00eancias e pacotes em Python.

    • fastzero_app: Este \u00e9 o nome do servi\u00e7o que definimos no docker-compose.yml. \u00c9 o servi\u00e7o que cont\u00e9m a nossa aplica\u00e7\u00e3o FastAPI e onde nossos testes ser\u00e3o executados.

    Ao utilizar esse comando, o Docker Compose cuidar\u00e1 de iniciar os servi\u00e7os dos quais fastzero_app depende, neste caso, o servi\u00e7o fastzero_database do PostgreSQL. Isso \u00e9 importante porque nossos testes podem depender de um banco de dados ativo para funcionar corretamente. O Compose garante que a ordem de inicializa\u00e7\u00e3o dos servi\u00e7os seja respeitada e que o servi\u00e7o do banco de dados esteja pronto antes de iniciar os testes.

    Se executarmos o comando podemos ver que ele inicia o banco de dados, inicia o container da aplica\u00e7\u00e3o e na sequ\u00eancia executa o comando que passamos no --entreypoint que \u00e9 exatamente como executar os testes:

    $ Execu\u00e7\u00e3o no terminal!
    docker-compose run --entrypoint=\"poetry run task test\" fastzero_app\n\n# Resulado esperado\n[+] Building 0.0s (0/0)                                           docker:default\n[+] Creating 2/2\n \u2714 Network default                Created                      0.1s \n \u2714 Container fastzero_database-1  Created                      0.1s \n[+] Running 1/1\n \u2714 Container fastzero_database-1  Started                      0.3s \n[+] Building 0.0s (0/0)                                           docker:default\nAll done! \u2728 \ud83c\udf70 \u2728\n18 files would be left unchanged.\n================ test session starts ================\nplatform linux - Python 3.11.6, pytest-7.4.3, pluggy-1.3.0 - /app/.venv/bin/python\ncachedir: .pytest_cache\nrootdir: /app\nconfigfile: pyproject.toml\nplugins: cov-4.1.0, anyio-4.1.0, Faker-20.1.0\ncollected 27 items\n\ntests/test_app.py::test_root_deve_retornar_200_e_ola_mundo PASSED\n...\n

    \u00c9 importante notar que, embora o docker-compose run inicie as depend\u00eancias necess\u00e1rias para a execu\u00e7\u00e3o do servi\u00e7o especificado, ele n\u00e3o finaliza essas depend\u00eancias ap\u00f3s a conclus\u00e3o do comando. Isso significa que ap\u00f3s a execu\u00e7\u00e3o dos testes, o servi\u00e7o do banco de dados continuar\u00e1 ativo. Voc\u00ea precisar\u00e1 finaliz\u00e1-lo manualmente com docker-compose down para encerrar todos os servi\u00e7os e limpar o ambiente:

    $ Execu\u00e7\u00e3o no terminal!
    docker-compose down\n[+] Running 2/2\n \u2714 Container 09-fastzero_database-1  Removed    0.4s \n \u2714 Network 09_default                Removed\n

    Assim tendo o ambiente limpo novamente.

    "},{"location":"09/#executando-os-testes-no-postgresql","title":"Executando os testes no PostgreSQL","text":"

    Embora nosso docker-compose esteja configurado para levantar o banco de dados PostgreSQL ao executar os testes, \u00e9 importante ressaltar que o container do PostgreSQL n\u00e3o est\u00e1 sendo utilizado durante a execu\u00e7\u00e3o dos testes. Isso acontece porque a fixture respons\u00e1vel por criar a sess\u00e3o do banco de dados est\u00e1 com as instru\u00e7\u00f5es \"hardcoded\" para o SQLite, como no c\u00f3digo abaixo:

    tests/conftest.py
    @pytest.fixture\ndef session():\n    engine = create_engine(\n        'sqlite:///:memory:',\n        connect_args={'check_same_thread': False},\n        poolclass=StaticPool,\n    )\n    Base.metadata.create_all(engine)\n\n    Session = sessionmaker(bind=engine)\n\n    yield Session()\n\n    Base.metadata.drop_all(engine)\n

    Por conta disso, os testes t\u00eam sido executados no SQLite, mesmo com a presen\u00e7a do PostgreSQL no ambiente do Docker.

    No entanto, \u00e9 importante que os testes sejam executados no mesmo ambiente que o que rodar\u00e1 em produ\u00e7\u00e3o, para que n\u00e3o encontremos problemas relacionados a incompatibilidade de opera\u00e7\u00f5es no banco de dados. A altera\u00e7\u00e3o \u00e9 relativamente simples, temos que tornar a nossa fixture o mais pr\u00f3ximo poss\u00edvel do cliente da sess\u00e3o de produ\u00e7\u00e3o. Para fazer isso, precisamos alterar somente a chamada create_engine para carregar a var\u00e1vel de ambiente do banco de dados de testes. Desta forma:

    tests/conftest.py
    import pytest\nfrom fastapi.testclient import TestClient\nfrom sqlalchemy import create_engine\nfrom sqlalchemy.orm import sessionmaker\n\nfrom fast_zero.app import app\nfrom fast_zero.database import get_session\nfrom fast_zero.models import Base\nfrom fast_zero.settings import Settings\nfrom fast_zero.security import get_password_hash\nfrom tests.factories import UserFactory\n\n\n@pytest.fixture\ndef session():\n    database = \n    engine = create_engine(Settings().DATABASE_URL)\n    Session = sessionmaker(autocommit=False, autoflush=False, bind=engine)\n    Base.metadata.create_all(engine)\n    with Session() as session:\n        yield session\n        session.rollback()\n\n    Base.metadata.drop_all(engine)\n

    Com essa modifica\u00e7\u00e3o, agora estamos apontando para o banco de dados PostgreSQL, conforme definido nas configura\u00e7\u00f5es da nossa aplica\u00e7\u00e3o (Settings().DATABASE_URL). A transi\u00e7\u00e3o do SQLite para o PostgreSQL \u00e9 facilitada pela abstra\u00e7\u00e3o fornecida pelo SQLAlchemy, que nos permite mudar de um banco para outro sem problemas. \u00c9 importante notar que essa flexibilidade se deve ao fato de n\u00e3o termos utilizado recursos espec\u00edficos do PostgreSQL que n\u00e3o s\u00e3o suportados pelo SQLite. Caso contr\u00e1rio, a mudan\u00e7a poderia n\u00e3o ser t\u00e3o direta.

    Agora, com a nova configura\u00e7\u00e3o, os testes utilizar\u00e3o o PostgreSQL, proporcionando um ambiente de testes mais fiel ao ambiente de produ\u00e7\u00e3o e, consequentemente, aumentando a confiabilidade dos testes executados:

    $ Execu\u00e7\u00e3o no terminal!
    docker-compose run --entrypoint=\"poetry run task test\" fastzero_app\n\n# resultado esperado\ndocker-compose run --entrypoint=\"poetry run task test\" fastzero_app\n[+] Building 0.0s (0/0)                                          docker:default\n[+] Creating 1/0\n \u2714 Container 09-fastzero_database-1  Running                     0.0s \n[+] Building 0.0s (0/0)                                          docker:default\nAll done! \u2728 \ud83c\udf70 \u2728\n18 files would be left unchanged.\n======================= test session starts =======================\nplatform linux - Python 3.11.6, pytest-7.4.3, pluggy-1.3.0 - /app/.venv/bin/python\ncachedir: .pytest_cache\nrootdir: /app\nconfigfile: pyproject.toml\nplugins: cov-4.1.0, anyio-4.1.0, Faker-20.1.0\ncollected 27 items\n\ntests/test_app.py::test_root_deve_retornar_200_e_ola_mundo PASSED\n

    Dessa forma temos um ambiente mais coeso e podemos reproduzir nossas configura\u00e7\u00f5es de forma bastante simples em qualquer ambiente.

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

    Ap\u00f3s criar nosso arquivo Dockerfile e docker-compose.yaml, executar os testes e construir nosso ambiente, podemos fazer o commit das altera\u00e7\u00f5es no Git:

    1. Adicionando todos os arquivos modificados nessa aula com git add .
    2. Fa\u00e7a o commit das altera\u00e7\u00f5es com git commit -m \"Dockerizando nossa aplica\u00e7\u00e3o e alterando os testes para serem executados no PostgreSQL\"
    3. Envie 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, junto com o PostgreSQL, nos permite garantir consist\u00eancia em diferentes ambientes. A combina\u00e7\u00e3o de Docker e Docker Compose simplifica o processo de desenvolvimento e implanta\u00e7\u00e3o. Na pr\u00f3xima aula, vamos aprender como levar nossa aplica\u00e7\u00e3o para o pr\u00f3ximo n\u00edvel executando os testes de forma remota com a integra\u00e7\u00e3o cont\u00ednua do GitHub Actions.

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

    T\u00f3pico de manuten\u00e7\u00e3o: https://github.com/dunossauro/fastapi-do-zero/issues/34

    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]\n\njobs:\n  test:\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: Copia os arquivos do repo\n        uses: actions/checkout@v3\n\n      - name: Instalar o python\n        uses: actions/setup-python@v4\n        with:\n          python-version: '3.11.1'\n\n      - name: Instalar Poetry\n        run: pip install poetry\n\n      - name: Instalar depend\u00eancias do projeto\n        run: poetry install\n\n      - name: Rodar os testes\n        run: poetry run task test --cov-report=xml\n\n      - name: Subir cobertura para o codecov\n        uses: codecov/codecov-action@v3\n        with:\n          token: ${{ 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":"

    Antes de avan\u00e7armos, \u00e9 importante mencionar uma especificidade do Fly.io: para criar uma inst\u00e2ncia do PostgreSQL, a plataforma requer que um cart\u00e3o de cr\u00e9dito seja fornecido. Esta \u00e9 uma medida de seguran\u00e7a adotada para evitar o uso indevido de seus servi\u00e7os, como a execu\u00e7\u00e3o de ferramentas de minera\u00e7\u00e3o. Apesar dessa exig\u00eancia, o servi\u00e7o de PostgreSQL \u00e9 oferecido de forma gratuita. Mais detalhes podem ser encontrados neste artigo.

    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\n  run: |\n    flyctl deploy --local-only\n    flyctl 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":"Despedida do curso","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.
    • Apresentar outros materiais criados com FastAPI.
    "},{"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/#outros-materiais-produzidos-por-mim-sobre-fastapi","title":"Outros materiais produzidos por mim sobre FastAPI","text":"

    J\u00e1 cobrimos alguns temas n\u00e3o citados neste curso usando FastAPI em Algumas Lives de Python. Voc\u00ea pode assistir para aprender mais tamb\u00e9m.

    "},{"location":"12/#templates-e-websockets","title":"Templates e WebSockets","text":"

    Na Live de Python #164 conversamos sobre websockets com Python e usamos FastAPI para exemplificar o comportamento. Durante essa live criamos uma aplica\u00e7\u00e3o de chat e usamos os templates com Jinja2 e Brython.

    "},{"location":"12/#graphql-strawberry","title":"GraphQL (Strawberry)","text":"

    Na Live de Python #185 conversamos sobre GraphQL um padr\u00e3o alternativo a REST APIs. Todos os exemplos foram aplicados usando Strawberrye FastAPI

    "},{"location":"12/#sqlmodel","title":"SQLModel","text":"

    Na Live de Python #235 conversamos sobre SQLModel um ORM alternativo ao SQLAlchemy que se integra com o Pydantic. O SQLModel tamb\u00e9m foi desenvolvido pelo Sebastian (criador do FastAPI). Caminhando ao final dessa aula, podemos ver a implementa\u00e7\u00e3o do SQLModel em uma aplica\u00e7\u00e3o b\u00e1sica com FastAPI.

    "},{"location":"12/#proximos-passos","title":"Pr\u00f3ximos passos","text":"

    [WIP]

    "},{"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.gz b/sitemap.xml.gz index 77040ddc..f54f9583 100644 Binary files a/sitemap.xml.gz and b/sitemap.xml.gz differ