Ir para o conteúdo

Projeto final

Você chegou ao final, PARABAINS 🎉

No aprendizado, nada melhor que praticar! Para isso, vamos fazer nosso "TCC" ou como gostam de chamar no mundo "interprize bizines": um teste técnico.

A ideia deste projeto final é simplesmente extrair tudo que aprendemos no curso para um grande exercício de fixação em formato de projeto.

O projeto

Neste projeto vamos construir uma API que segue os mesmos moldes da que desenvolvemos durante o curso, porém, com outra proposta. Iremos fazer uma versão simplificado de um acervo digital de livros. Chamaremos de MADR (Mader), uma sigla para "Meu Acervo Digital de Romances".

O objetivo do projeto é criarmos um gerenciador de livros e relacionar com seus autores. Tudo isso em um contexto bastante simplificado. Usando somente as funcionalidades que aprendemos no curso.

A implementação será baseada em 3 pilares:

graph
    MADR --> A["Controle de acesso / Gerenciamento de contas"]
    MADR --> B["Gerenciamento de Livros"]
    MADR --> C["Gerenciamento de Romancistas"]
    A --> D["Gerenciamento de contas"]
    D --> Criação
    D --> Atualização
    A --> G["Acesso via JWT"]
    D --> Deleção
    B --> E["CRUD"]
    C --> F["CRUD"]

A API

Dividiremos os endpoints em três routers:

  1. contas: Gerenciamento de contas e de acesso à API
  2. livros: Gerenciamento de livros
  3. romancistas: Gerenciamento de romancistas

Contas

O router de conta deve ser responsável pelas operações referentes a criação, alteração e deleção de contas. Os endpoints:

  • POST /conta: deve ser responsável pela criação de uma nova conta

    • O schema responsável para criação desse endpoint deve ser:
      {
          "username": "fausto",
          "email": "fausto@fausto.com",
          "senha": "1234567",
      }
      
    • Esses schema deve ser validado com pydantic
    • O retorno para o caso de sucesso deve ser 201 e com o schema de exemplo:
      {
          "id": 10,
          "email": "fausto@fausto.com",
          "username": "fausto"
      }
      
    • A senha deve ser criptografada antes de ser inserida no banco de dados
    • obs: Não é necessário fazer o login no sistema para enviar uma requisição para esse enpoint
    • 🚨 Caso o registro já exista na base, conflito
    • ⚠ Antes de inserir no banco, o nome deve ser sanitizado
  • PUT /conta/{id}: deve ser responsável pela alteração de uma conta especificada por id

    • O schema responsável para criação desse endpoint deve ser:
      {
          "username": "fausto",
          "email": "fausto@fausto.com",
          "senha": "1234567",
      }
      
    • Esses schema deve ser validado com pydantic
    • O retorno para o caso de sucesso deve ser 200 e com o schema de exemplo:
      {
          "id": 10,
          "email": "fausto@fausto.com",
          "username": "fausto"
      }
      
    • 🚨 O acesso só pode ocorrer via um Bearer token válido enviado nos headers, erro
    • 🚨 Somente a pessoa detentora da sua própria conta pode alterar seus dados
    • 🚨 Caso as alterações no registro já existam na base, conflito
    • ⚠ Antes de inserir no banco, o nome deve ser sanitizados
  • DELETE /conta/{id}: deve ser responsável pela deleção de uma conta especificada por id

    • O retorno para o caso de sucesso deve ser 200 e com o schema de exemplo:
      {
          "message": "Conta deletada com sucesso"
      }
      
    • 🚨 O acesso só pode ocorrer via um Bearer token válido enviado nos headers, erro
    • 🚨 Somente a pessoa detentora da sua própria conta pode alterar seus dados
  • POST /token: Responsável pelo login

    • O endpoint deverá receber o seguinte schema via OAuth2PasswordRequestForm:
      {
          "username": "fausto@fausto.com",
          "password": "12345"
      }
      
    • O conta deve ser validada com "username" e "password"
    • 🚨 O acesso só pode ocorrer via um Bearer token válido enviado nos headers, erro
    • O retorno para o caso de sucesso deve ser 200 e com o schema de exemplo:
      {
          "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ0ZXN0ZUB0ZXN0LmNvbSIsImV4cCI6MTY5MDI1ODE1M30.Nx0P_ornVwJBH_LLLVrlJoh6RmJeXR-Nr7YJ_mlGY04",
          "token_type": "bearer"
      }
      
  • POST /refresh-token: Responsável por atualizar o token

    • O endpoint deverá receber os headers:
      {
          "Authorization": " Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ0ZXN0ZUB0ZXN0LmNvbSIsImV4cCI6MTY5MDI1ODE1M30.Nx0P_ornVwJBH_LLLVrlJoh6RmJeXR-Nr7YJ_mlGY04"
      }
      
    • O retorno para o caso de sucesso deve ser 200 e com o schema de exemplo:
      {
          "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ0ZXN0ZUB0ZXN0LmNvbSIsImV4cCI6MTY5MDI1ODE1M30.Nx0P_ornVwJBH_LLLVrlJoh6RmJeXR-Nr7YJ_mlGY04",
          "token_type": "bearer"
      }
      
    • 🚨 Caso as coisas não ocorram como o esperado: Erros
As condições do token JWT

O tempo de expiração do token deve ser de 60 minutos, o algorítimo usado deve ser HS256 e o subject deve ser o email.

Livros

  • POST /livro: Responsável pela adição de um livro no MADR

    • O livro deve ser criado com base no seguinte schema:
      {
          "ano": 1973,
          "titulo": "Café Da Manhã Dos Campeões",
          "romancista_id": 42
      }
      
    • O retorno de sucesso 200 deve ser:
      {
          "id": 3,
          "ano": 1973,
          "titulo": "café da manhã dos campeões",
          "romancista_id": 42
      }
      
    • ⚠ Disponível somente via autenticação, caso contrário erro
    • ⚠ Antes de inserir no banco, os nomes devem ser sanitizados
    • 🚨 Caso o novo nome já exista na base, conflito
  • DELETE /livro/{id}: Responsável por deletar um livro usando o id como base

    • O caso de sucesso 200 deve retornar o schema:
      {
          "message": "Livro deletado no MADR"
      }
      
    • ⚠ Disponível somente via autenticação, caso contrário erro
    • 🚨 Caso o id não exista no MADR, erro
  • PATCH /livro/{id}: Responsável por alterar um livro usando o id como base

    • O livro deve ser alterado com o seguinte schema:
      {
          "ano": 1974
      }
      
    • O schema para o caso de sucesso 200 deve ser:
      {
          "ano": 1974,
          "titulo": "café da manhã dos campeões",
          "romancista_id": 1
      }
      
    • ⚠ Disponível somente via autenticação, caso contrário erro
    • ⚠ Antes de inserir no banco, os nomes devem ser sanitizados
    • 🚨 Caso o id não exista no MADR, erro
    • 🚨 Caso o novo nome já exista na base, conflito
  • GET /livro/{id}: Busca um livro por id

    • O retorno deve ser 200 OK com o schema:
      {
          "id": 1,
          "ano": 1974,
          "titulo": "café da manhã dos campeões",
          "romancista_id": 1
      }
      
    • 🚨 Caso o id não exista no MADR, erro
  • GET /livro?nome=xxxx&ano=xxxx: Busca por livros quando query parameters

    • Deve ser capaz de filtrar por nome de forma parcial
    • Deve ser capaz de filtrar por ano
    • Deve paginar os resultados maiores que 20
    • Exemplo de chamada:
      /livro/?titulo=a&ano=1900
      
    • Exemplo do schema de resposta:
      {
          "livros": [
              {"ano": 1900, "titulo": "café da manhã dos campeões", "romancista_id": 1, "id": 1},
              {"ano": 1900, "titulo": "memórias póstumas de brás cubas", "romancista_id": 2, "id": 2}
          ]
      }
      
    • ⚠ Caso não encontre nenhuma correspondência, deverá retornar 200 OK com a lista vazia:
      {
          "livros": []
      }
      

Romancistas

  • POST /romancista: Responsável pela adição de romancistas no MADR

    • Romancista devem ser criadas com base no seguinte schema:
      {
          "nome": "Clarice Lispector"
      }
      
    • A resposta padrão deve retornar 201 com o schema:
      {
          "id": 42,
          "nome": "Clarice Lispector"
      }
      
    • ⚠ Disponível somente via autenticação, caso contrário erro
    • ⚠ Antes de inserir no banco, os nomes devem ser sanitizados
    • 🚨 Caso o novo nome já exista na base, conflito
  • DELETE /romancista/{id}: responsável pela deleção de romancistas por id

    • O retorno para o caso de sucesso deve ser 200 e com o schema de exemplo:
      {
          "message": "Romancista deletada no MADR"
      }
      
    • ⚠ Disponível somente via autenticação, caso contrário erro
    • 🚨 Caso o id não exista no MADR, erro
  • PATCH /romancista/{id}: responsável pela alteração de romancistas por id

    • Romancista devem ser alteradas com base no seguinte schema:
      {
          "nome": "Clarice Lispector"
      }
      
    • A resposta padrão deve retornar 200 com o schema:
      {
          "id": 42,
          "nome": "Clarice Lispector"
      }
      
    • ⚠ Disponível somente via autenticação, caso contrário erro
    • ⚠ Antes de inserir no banco, os nomes devem ser sanitizados
    • 🚨 Caso o id não exista no MADR, erro
    • 🚨 Caso o novo nome já exista na base, conflito
  • GET /romancista/{id}: Busca um romancista por id

    • O retorno deve ser 200 OK com o schema:
      {
          "id": 1,
          "nome": "machado de assis"
      }
      
    • 🚨 Caso o id não exista no MADR, erro
  • GET /romancista?: Busca romancistas baseado em nomes parciais

    • Deve ser capaz de filtrar por nome de forma parcial
    • Deve paginar os resultados maiores que 20
    • Exemplo de chamada:
      /romancista/?nome=a
      
    • Exemplo do schema de resposta:
      {
          "romancistas": [
              {"nome": "machado de assis", "id": 1},
              {"nome": "clarice lispector", "id": 2},
              {"nome": "josé de alencar", "id": 3},
          ]
      }
      
    • ⚠ Caso não encontre nenhuma correspondência, deverá retornar 200 OK com a lista vazia:
      {
          "romancistas": []
      }
      

Sanitização de dados

Antes de inserir no banco, os nomes de romancistas ou livros devem ser sanitizados.

Exemplos para os nomes:

Entrada Sanitizado
"Machado de Assis" machado de assis
"Manuel        Bandeira" manuel bandeira
"Edgar Alan Poe         " edgar alan poe
"Androides Sonham Com Ovelhas Elétricas?" androides sonham com ovelhas elétricas
"  breve  história  do tempo " breve história do tempo
"O mundo assombrado pelos demônios" o mundo assombrado pelos demônios

Erros

Erros de autenticação

Todos os erros relativos à autenticação devem retornar o status code 400 BAD REQUEST com o seguinte schema:

{
    "message": "Email ou senha incorretos"
}

Erros de permissão

Caso uma pessoa tente fazer uma operação sem a permissão necessária, o status code401 Unauthorized deverá ser retornado com o json:

{
    "message": "Não autorizado"
}

Erro não encontrado

Caso o id não exista no MADR, um erro 404 NOT FOUND deve ser retornado com o json:

{
    "message": "Romancista não consta no MADR"
}

ou então

{
    "message": "Livro não consta no MADR"
}

Erro de conflito

Caso o recurso já exista, devemos retornar 409 CONFLICT com o json:

{
    "message": "{recurso} já consta no MADR"
}

Onde a variável recurso é relativa ao recurso que está duplicado. Exemplos para:

  • contas: "conta já consta no MADR"
  • livros: "livro já consta no MADR"
  • romancista: "romancista já consta no MADR"

O banco de dados / ORM

A modelagem do banco deve contar com três tabelas: User, Livro e Romancista. Onde Livro e Romancista se relacionam da forma que romancistas podem estar relacionado a diversos livros e diversos livros devem ser associados a uma única romancista. Como sugere o DER:

erDiagram
  Romancista |o -- |{ Livro : livros
  User {
      int id PK
      string email UK
      string username UK
      string senha
  }
  Livro {
      int id  PK
      string ano
      string titulo UK
      string id_romancista FK
  }
  Romancista {
      int id PK
      string nome UK
      string livros
  }

Relacionamentos no ORM

Alguns problemas podem ser encontrados durante a criação dos relacionamentos com SQLAlchemy, então segue uma cola simples caso sinta que travou.

Em caso de emergência quebre o vidro
class Livro:
    ...

    autoria: Mapped[Romancista] = relationship(
        init=False, back_populates='livros'
    )

class Romancista:
    ...

    livros: Mapped[list['Livro']] = relationship(
        init=False, back_populates='romancista', cascade='all, delete-orphan'
    )

Cenários de teste

O ideal é que esse projeto tenha uma cobertura de testes de 100%. Afinal, foi dessa forma que passamos nosso tempo no curso, testando absolutamente tudo e garantindo que o código funcione da maneira como deveria.

Nesse tópico separei alguns cenários de testes usando a linguagem gherkin para te ajudar a pensar em como as requisições serão recebidas e devem ser respondidas pela aplicação.

Esses cenários podem te guiar tanto para escrever a aplicação, quanto os testes.

Gerenciamento de contas

Funcionalidade: Gerenciamento de conta

Cenário: Criação de conta
    Quando enviar um "POST" em "/user"
    """
    {
        "username": "dunossauro",
        "email": "dudu@dudu.com",
        "password": "123456"
    }
    """
    Então devo receber o status "201"
    E o json contendo
    """
    {
        "email": "dudu@dudu.com",
        "username": "dunossauro"
    }
    """

Cenário: Alteração de conta
    Quando enviar um "POST" em "/user"
    """
    {
        "username": "dunossauro",
        "email": "dudu@dudu.com",
        "password": "123456"
    }
    """
    Quando enviar um "PUT" em "/user/1"
    """
    {
        "username": "dunossauro",
        "email": "dudu@dudu.com",
        "password": "654321"
    }
    """
    Então devo receber o status "200"
    E o json contendo
    """
    {
        "username": "dunossauro",
        "email": "dudu@dudu.com"
    }
    """

Cenário: Deleção da conta
    Quando enviar um "POST" em "/user"
    """
    {
        "username": "dunossauro",
        "email": "dudu@dudu.com",
        "password": "123456"
    }
    """
    Quando enviar um "DELETE" em "/user/1"
    Então devo receber o status "200"
    E o json contendo
    """
    {
        "message": "Conta deletada com sucesso"
    }
    """
Cenário: Criação de conta já existente
    Quando enviar um "POST" em "/user"
    """
    {
        "username": "dunossauro",
        "email": "dudu@dudu.com",
        "password": "123456"
    }
    """
    Quando enviar um "POST" em "/user"
    """
    {
        "username": "dunossauro",
        "email": "dudu@dudu.com",
        "password": "123456"
    }
    """
    Então devo receber o status "400"
    E o json contendo
    """
    {
        "message": "Conta já cadastrada"
    }
    """
TODO

Gerenciamento de livros

Funcionalidade: Livro

Cenário: Registro de livro
    Quando enviar um "POST" em "/livro/"
    """
    {
        "ano": 1973,
        "titulo": "Café Da Manhã Dos Campeões",
        "romancista_id": 1
    }
    """

    Então devo receber o status "201"
    E o json contendo
    """
    {
        "ano": 1973,
        "titulo": "café da manhã dos campeões",
        "romancista_id": 1
    }
    """


Cenário: Alteração de livro
    Quando enviar um "PATCH" em "/livro/1"
    """
    {
        "ano": 1974
    }
    """

    Então devo receber o status "200"
    E o json contendo
    """
    {
        "ano": 1974,
        "titulo": "café da manhã dos campeões",
        "romancista_id": 1
    }
    """

Cenário: Buscar livro por ID
    Quando enviar um "GET" em "/livro/1"
    Então devo receber o status "200"
    E o json contendo
    """
    {
        "ano": 1974,
        "titulo": "café da manhã dos campeões",
        "romancista_id": 1
    }
    """

Cenário: Deleção de livro
    Quando enviar um "DELETE" em "/livro/1"

    Então devo receber o status "200"
    E o json contendo
    """
    {
        "message": "Livro deletado no MADR"
    }
    """

Cenário: Filtro de livros
    Quando enviar um "POST" em "/livro/"
    """
    {
        "ano": 1900,
        "titulo": "Café Da Manhã Dos Campeões",
        "romancista_id": 1
    }
    """
    E enviar um "POST" em "/livro/"
    """
    {
        "ano": 1900,
        "titulo": "Memórias Póstumas de Brás Cubas",
        "romancista_id": 2
    }
    """
    E enviar um "POST" em "/livro/"
    """
    {
        "ano": 1865,
        "titulo": "Iracema",
        "romancista_id": 3
    }
    """
    E enviar um "GET" em "/livro/?titulo=a&ano=1900"

    Então devo receber o status "200"
    E o json contendo
    """
    {
        "livros": [
            {"ano": 1900, "titulo": "café da manhã dos campeões", "romancista_id": 1, "id": 1},
            {"ano": 1900, "titulo": "memórias póstumas de brás cubas", "romancista_id": 2, "id": 2}
        ]
    }
    """

Gerenciamento de romancistas

Funcionalidade: Romancistas


Cenário: Criação de Romancista
    Quando enviar um "POST" em "/romancista"
    """
    {
        "nome": "Clarice Lispector"
    }
    """

    Então devo receber o status "201"
    E o json contendo
    """
    {
        "nome": "clarice lispector"
    }
    """

Cenário: Buscar romancista por ID
    Quando enviar um "GET" em "/romancista/1"
    Então devo receber o status "200"
    E o json contendo
    """
    {
        "nome": "clarice lispector"
    }
    """

Cenário: Alteração de Romancista
    Quando enviar um "PUT" em "/romancista/1"
    """
    {
        "nome": "manuel bandeira"
    }
    """
    Então devo receber o status "200"
    E o json contendo
    """
    {
        "nome": "manuel bandeira"
    }
    """

Cenário: Deleção de Romancista
    Quando enviar um "DELETE" em "/romancista/1"
    Então devo receber o status "200"
    E o json contendo
    """
    {
        "message": "Romancista deletada no MADR"
    }
    """

Cenário: Busca de romancistas por filtro
    Quando enviar um "POST" em "/romancista"
    """
    {
        "nome": "Clarice Lispector"
    }
    """

    E enviar um "POST" em "/romancista"
    """
    {
        "nome": "Manuel Bandeira"
    }
    """

    E enviar um "POST" em "/romancista"
    """
    {
        "nome": "Paulo Leminski"
    }
    """

    Quando enviar um "GET" em "/romancista?nome=a"
    Então devo receber o status "200"
    E o json contendo
    """
    {
        "romancistas": [
            {"nome": "clarice lispector", "id": 1},
            {"nome": "manuel bandeira", "id": 2},
            {"nome": "paulo leminski", "id": 3}
        ]
    }
    """

Ferramentas

Gostaria que você se sentissem livres para escolher o conjunto de ferramentas que mais gostarem para fazer esse projeto. O formatador preferido, o servidor de aplicação preferido, projeto de variáveis de ambiente preferido, etc.

As únicas coisas exigidas para a criação desse projeto são:

  1. Python 3.11+
  2. FastAPI
  3. SQLAlchemy
  4. Alguma ferramenta para gerenciamento de projeto que suporte pyproject.toml
  5. PostgreSQL
  6. Containers (a ferramenta que preferir. Podman/docker/k8s/...)
  7. Pytest

Entrega do projeto final

Criar um projeto utilizando git e hospedado em alguma plataforma (github/gitlab/codeberg/...) e postar nessa issue. Ao final, juntarei todos os projetos finais em uma tabela nesse site para que as pessoas possam aprender com as diferenças entre os projetos.

É imprescindível que seu projeto tenha um README.md explicando quais foram as suas escolhas e como executar o seu projeto. Para podermos rodar e aprender com ele.