diff --git a/README.md b/README.md index 5c3393a9..6c3d662c 100644 --- a/README.md +++ b/README.md @@ -6,28 +6,28 @@ O desafio consiste em implementar um CRUD de filmes, utilizando [python](https:/ Rotas da API: - - `/filmes` - [GET] deve retornar todos os filmes cadastrados. - - `/filmes` - [POST] deve cadastrar um novo filme. - - `/filmes/{id}` - [GET] deve retornar o filme com ID especificado. +- `/filmes` - [GET] deve retornar todos os filmes cadastrados. +- `/filmes` - [POST] deve cadastrar um novo filme. +- `/filmes/{id}` - [GET] deve retornar o filme com ID especificado. O Objetivo é te desafiar e reconhecer seu esforço para aprender e se adaptar. Qualquer código enviado, ficaremos muito felizes e avaliaremos com toda atenção! -#### Sugestão de Ferramentas +#### Sugestão de Ferramentas + Não é obrigatório utilizar todas as as tecnologias sugeridas, mas será um diferencial =] - Orientação a objetos (utilizar objetos, classes para manipular os filmes) - [FastAPI](https://fastapi.tiangolo.com/) (API com documentação auto gerada) -- [Docker](https://www.docker.com/) / [Docker-compose](https://docs.docker.com/compose/install/) (Aplicação deverá ficar em um container docker, e o start deverá seer com o comando ``` docker-compose up ``` +- [Docker](https://www.docker.com/) / [Docker-compose](https://docs.docker.com/compose/install/) (Aplicação deverá ficar em um container docker, e o start deverá seer com o comando `docker-compose up` - Integração com banco de dados (persistir as informações em json (iniciante) /[SqLite](https://www.sqlite.org/index.html) / [SQLAlchemy](https://fastapi.tiangolo.com/tutorial/sql-databases/#sql-relational-databases) / outros DB) - #### Como começar? - Fork do repositório -- Criar branch com seu nome ``` git checkout -b feature/ana ``` -- Faça os commits de suas alterações ``` git commit -m "[ADD] Funcionalidade" ``` -- Envie a branch para seu repositório ``` git push origin feature/ana ``` -- Navegue até o [Github](https://github.com/), crie seu Pull Request apontando para a branch **```main```** +- Criar branch com seu nome `git checkout -b feature/ana` +- Faça os commits de suas alterações `git commit -m "[ADD] Funcionalidade"` +- Envie a branch para seu repositório `git push origin feature/ana` +- Navegue até o [Github](https://github.com/), crie seu Pull Request apontando para a branch **`main`** - Atualize o README.md descrevendo como subir sua aplicação #### Dúvidas? diff --git a/README2.md b/README2.md new file mode 100644 index 00000000..15933d88 --- /dev/null +++ b/README2.md @@ -0,0 +1,92 @@ +# README + +## Descrição do Projeto + +Este projeto é uma API simples para gerenciar um catálogo de filmes, utilizando FastAPI e SQLAlchemy. A API permite realizar operações básicas como criar, listar, atualizar e excluir filmes. Os dados são armazenados em um banco de dados SQLite. + +## Estrutura do Projeto + +O projeto é organizado da seguinte forma: + +- **app/**: Contém todos os arquivos da aplicação. + - **main.py**: O ponto de entrada da aplicação, onde as rotas da API são definidas. + - **crud.py**: Contém as funções que interagem com o banco de dados. + - **schemas.py**: Define os modelos de dados utilizados pela API. + - **models.py**: Define a estrutura do banco de dados. + - **db.py**: Configurações do banco de dados. + - \***\*init**.py\*\*: Inicializa o pacote. + +## Funcionalidades + +### 1. Listar Filmes + +- **GET /filmes**: Retorna uma lista de todos os filmes ativos. +- **GET /filmes/inativos**: Retorna uma lista de todos os filmes inativos. + +### 2. Criar Filme + +- **POST /filmes**: Cria um novo filme. O corpo da requisição deve conter os dados do filme (título, diretor, ano e ativo). Se um filme com o mesmo título e ano já existir, um erro será retornado. + +### 3. Obter Filme por ID + +- **GET /filmes/{id}**: Retorna os detalhes de um filme específico, identificado pelo seu ID. Se o filme não for encontrado, um erro 404 será retornado. + +### 4. Atualizar Filme + +- **PUT /filmes/{id}**: Atualiza os dados de um filme existente. O corpo da requisição deve conter os novos dados do filme. Se o filme não for encontrado, um erro 404 será retornado. + +### 5. Excluir Filme + +- **DELETE /filmes/{id}**: Inativa um filme existente. Em vez de remover o filme do banco de dados, ele é marcado como inativo. Isso é feito para manter um histórico de filmes, permitindo que você recupere informações sobre filmes que não estão mais ativos, se necessário. Essa abordagem é útil para evitar a perda de dados e para manter a integridade referencial em sistemas que podem ter relacionamentos com outros dados. + +## Como Funciona a Exclusão + +A exclusão de um filme é realizada através da rota **DELETE /filmes/{id}**. Quando essa rota é chamada, a API verifica se o filme existe. Se existir, o filme é marcado como inativo, alterando o campo `ativo` para `False`. Isso significa que o filme não será mais exibido nas listas de filmes ativos, mas ainda estará presente no banco de dados. + +Essa abordagem de "inativação" em vez de exclusão física é vantajosa por várias razões: + +- **Histórico**: Permite manter um registro de todos os filmes, mesmo aqueles que não estão mais ativos. +- **Integridade dos Dados**: Evita problemas de integridade referencial que podem ocorrer se outros dados dependem do filme que está sendo excluído. +- **Recuperação**: Facilita a recuperação de informações sobre filmes que foram inativados, caso seja necessário. + +## Como Executar o Projeto + +### Usando Python Local + +1. **Instalação das Dependências**: Certifique-se de ter o Python e o pip instalados. Em seguida, instale as dependências necessárias: + + ```bash + pip install fastapi[all] sqlalchemy + ``` + +2. **Executar a Aplicação**: Navegue até o diretório app e execute o seguinte comando: + + ```bash + uvicorn main:app --reload + ``` + +3. **Acessar a API**: A API estará disponível em `http://127.0.0.1:8000`. Você pode usar ferramentas como Postman ou Insomnia para testar as rotas, ou acessar a documentação automática gerada pelo FastAPI em `http://127.0.0.1:8000/docs`. + +### Usando Docker Compose + +1. **Pré-requisitos**: Certifique-se de ter o Docker e o Docker Compose instalados em sua máquina. + +2. **Executar o Container**: No diretório App do projeto, execute: + + ```bash + docker-compose up --build + ``` + + Este comando irá: + + - Construir a imagem do container + - Iniciar o serviço da API + - Mapear a porta 8000 do container para a porta 8000 do seu host + +3. **Acessar a API**: A API estará disponível em `http://localhost:8000`. A documentação pode ser acessada em `http://localhost:8000/docs`. + +4. **Parar o Container**: Para parar a execução, use: + + ```bash + docker-compose down + ``` diff --git a/app/Dockerfile b/app/Dockerfile new file mode 100644 index 00000000..352daf85 --- /dev/null +++ b/app/Dockerfile @@ -0,0 +1,14 @@ +# Use a imagem base do Python +FROM python:3.10-slim + +# Defina o diretório de trabalho +WORKDIR /app + +# Copie o conteúdo do diretório atual para o diretório de trabalho no container +COPY . /app + +# Instale as dependências +RUN pip install -r requirements.txt + +# Comando para iniciar o servidor ASGI, ajustando o caminho para 'app.main:app' +CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/app/__pycache__/__init__.cpython-310.pyc b/app/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 00000000..db124e54 Binary files /dev/null and b/app/__pycache__/__init__.cpython-310.pyc differ diff --git a/app/__pycache__/crud.cpython-310.pyc b/app/__pycache__/crud.cpython-310.pyc new file mode 100644 index 00000000..d7457d70 Binary files /dev/null and b/app/__pycache__/crud.cpython-310.pyc differ diff --git a/app/__pycache__/db.cpython-310.pyc b/app/__pycache__/db.cpython-310.pyc new file mode 100644 index 00000000..935944f5 Binary files /dev/null and b/app/__pycache__/db.cpython-310.pyc differ diff --git a/app/__pycache__/main.cpython-310.pyc b/app/__pycache__/main.cpython-310.pyc new file mode 100644 index 00000000..241247ef Binary files /dev/null and b/app/__pycache__/main.cpython-310.pyc differ diff --git a/app/__pycache__/models.cpython-310.pyc b/app/__pycache__/models.cpython-310.pyc new file mode 100644 index 00000000..549f07b7 Binary files /dev/null and b/app/__pycache__/models.cpython-310.pyc differ diff --git a/app/__pycache__/schemas.cpython-310.pyc b/app/__pycache__/schemas.cpython-310.pyc new file mode 100644 index 00000000..a534b1aa Binary files /dev/null and b/app/__pycache__/schemas.cpython-310.pyc differ diff --git a/app/crud.py b/app/crud.py new file mode 100644 index 00000000..7a6bd733 --- /dev/null +++ b/app/crud.py @@ -0,0 +1,267 @@ +# Importações necessárias do SQLAlchemy e dos modelos/schemas +from sqlalchemy.orm import Session +from sqlalchemy.exc import SQLAlchemyError, IntegrityError +from models import Filme +from schemas import FilmeResponse + + +# Função para buscar todos os filmes ativos do banco de dados +def get_filmes(db: Session): + try: + # Faz a query para buscar filmes ativos ou inativos + query = db.query(Filme).filter(Filme.ativo == True) + filmes = query.all() + # Converte cada filme do banco para o formato de resposta da API + return [ + FilmeResponse( + id=filme.id, + titulo=filme.titulo, + diretor=filme.diretor, + ano=filme.ano, + ativo=filme.ativo, + detail="Filme encontrado com sucesso", + status=200, + ) + for filme in filmes + ] + except SQLAlchemyError as e: + # Em caso de erro, desfaz as alterações e relança a exceção + db.rollback() + filme_response = FilmeResponse( + id=0, + titulo="", + diretor="", + ano=0, + ativo=False, + detail=f"Erro ao buscar filmes: {str(e)}", + status=500, + ) + return filme_response + + +# Função para buscar todos os filmes inativos do banco de dados +def get_filmes_inativos(db: Session): + try: + # Faz a query para buscar filmes inativos + query = db.query(Filme).filter(Filme.ativo == False) + filmes = query.all() + # Converte cada filme do banco para o formato de resposta da API + return [ + FilmeResponse( + id=filme.id, + titulo=filme.titulo, + diretor=filme.diretor, + ano=filme.ano, + ativo=filme.ativo, + detail="Filme inativo encontrado com sucesso", + status=200, + ) + for filme in filmes + ] + except SQLAlchemyError as e: + # Em caso de erro, desfaz as alterações e relança a exceção + db.rollback() + filme_response = FilmeResponse( + id=0, + titulo="", + diretor="", + ano=0, + ativo=False, + detail=f"Erro ao buscar filmes inativos: {str(e)}", + status=500, + ) + return filme_response + + +# Função para criar um novo filme no banco de dados +def create_filme(db: Session, filme: FilmeResponse): + try: + # Verifica se já existe um filme com mesmo título e ano + filme_existente = ( + db.query(Filme) + .filter(Filme.titulo == filme.titulo, Filme.ano == filme.ano) + .first() + ) + + if filme_existente: + filme_response = FilmeResponse( + id=0, + titulo="", + diretor="", + ano=0, + ativo=False, + detail="Filme já existe no banco de dados", + status=400, + ) + return filme_response + + # Cria uma nova instância do modelo Filme com os dados recebidos + novo_filme = Filme(titulo=filme.titulo, diretor=filme.diretor, ano=filme.ano) + # Adiciona o novo filme ao banco + db.add(novo_filme) + # Confirma as alterações + db.commit() + # Atualiza o objeto com os dados do banco + db.refresh(novo_filme) + # Retorna o filme criado no formato de resposta + filme_response = FilmeResponse( + id=novo_filme.id, + titulo=novo_filme.titulo, + diretor=novo_filme.diretor, + ano=novo_filme.ano, + detail="Filme criado com sucesso", + ativo=True, + status=201, + ) + return filme_response + except SQLAlchemyError as e: + # Outros erros do banco de dados + db.rollback() + filme_response = FilmeResponse( + id=0, + titulo="", + diretor="", + ano=0, + ativo=False, + detail=f"Erro ao criar filme: {str(e)}", + status=500, + ) + return filme_response + + +# Função para buscar um filme específico pelo ID +def get_filme_by_id( + db: Session, + id: int, +): + try: + # Busca o filme pelo ID + query = db.query(Filme).filter(Filme.id == id) + + filme = query.first() + if filme: + # Se encontrou, retorna no formato de resposta + filme_response = FilmeResponse( + id=filme.id, + titulo=filme.titulo, + diretor=filme.diretor, + ano=filme.ano, + detail="Filme encontrado com sucesso", + ativo=filme.ativo, + status=200, + ) + return filme_response + # Se não encontrou, retorna None + return None + except SQLAlchemyError as e: + db.rollback() + filme_response = FilmeResponse( + id=0, + titulo="", + diretor="", + ano=0, + ativo=False, + detail=f"Erro ao buscar filme por ID: {str(e)}", + status=500, + ) + return filme_response + + +# Função para atualizar um filme existente +def update_filme(db: Session, id: int, filme_data: dict): + try: + # Primeiro verifica se o filme existe e está ativo + db_filme = get_filme_by_id(db, id) + if not db_filme: + return None + + # Acesse os dados do dicionário diretamente + titulo = filme_data["titulo"] + diretor = filme_data["diretor"] + ano = filme_data["ano"] + ativo = filme_data["ativo"] + + # Busca o filme e atualiza seus campos + filme_obj = db.query(Filme).filter(Filme.id == id).first() + filme_obj.titulo = titulo + filme_obj.diretor = diretor + filme_obj.ano = ano + filme_obj.ativo = ativo + + # Confirma as alterações + db.commit() + db.refresh(filme_obj) + # Retorna o filme atualizado + filme_response = FilmeResponse( + id=filme_obj.id, + titulo=filme_obj.titulo, + diretor=filme_obj.diretor, + ano=filme_obj.ano, + detail="Filme atualizado com sucesso", + ativo=True, + status=200, + ) + return filme_response + except IntegrityError: + # Erro se os dados forem inválidos + db.rollback() + filme_response = FilmeResponse( + id=0, + titulo="", + diretor="", + ano=0, + ativo=False, + detail="Erro: Dados inválidos para atualização", + status=400, + ) + return filme_response + except SQLAlchemyError as e: + db.rollback() + filme_response = FilmeResponse( + id=0, + titulo="", + diretor="", + ano=0, + ativo=False, + detail=f"Erro ao atualizar filme: {str(e)}", + status=500, + ) + return filme_response + + +# Função para inativar um filme do banco de dados +def delete_filme(db: Session, id: int): + try: + # Verifica se o filme existe e está ativo + db_filme = get_filme_by_id(db, id) + if not db_filme: + return None + + # Busca e inativa o filme + filme_obj = db.query(Filme).filter(Filme.id == id).first() + filme_obj.ativo = False + db.commit() + db.refresh(filme_obj) + # Retorna o filme que foi inativado + filme_response = FilmeResponse( + id=id, + titulo=db_filme.titulo, + diretor=db_filme.diretor, + ano=db_filme.ano, + detail="Filme inativado com sucesso", + ativo=False, + status=200, + ) + return filme_response + except SQLAlchemyError as e: + db.rollback() + filme_response = FilmeResponse( + id=0, + titulo="", + diretor="", + ano=0, + ativo=False, + detail=f"Erro ao inativar filme: {str(e)}", + status=500, + ) + return filme_response diff --git a/app/db.py b/app/db.py new file mode 100644 index 00000000..1bf6d459 --- /dev/null +++ b/app/db.py @@ -0,0 +1,8 @@ +from sqlalchemy import Column, Integer, String, create_engine +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import sessionmaker + +DATABASE_URL = "sqlite:///./filmes.db" +engine = create_engine(DATABASE_URL) +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) +Base = declarative_base() diff --git a/app/docker-compose.yml b/app/docker-compose.yml new file mode 100644 index 00000000..23f30447 --- /dev/null +++ b/app/docker-compose.yml @@ -0,0 +1,8 @@ +version: "3.9" +services: + app: + build: . + ports: + - "8000:8000" + volumes: + - .:/app diff --git a/app/filmes.db b/app/filmes.db new file mode 100644 index 00000000..c1fb5ff9 Binary files /dev/null and b/app/filmes.db differ diff --git a/app/main.py b/app/main.py new file mode 100644 index 00000000..0f91f64b --- /dev/null +++ b/app/main.py @@ -0,0 +1,130 @@ +# Importações necessárias +from fastapi import FastAPI, HTTPException, Depends +from sqlalchemy.orm import Session +from sqlalchemy.exc import SQLAlchemyError, IntegrityError +from db import SessionLocal, Base, engine +from models import Filme +from schemas import FilmeBase, FilmeResponse +from crud import ( + get_filmes, + create_filme, + get_filme_by_id, + update_filme, + delete_filme, + get_filmes_inativos, +) + + +# Cria as tabelas no banco de dados +Base.metadata.create_all(bind=engine) +# Inicializa a aplicação FastAPI +app = FastAPI() + + +# Função para gerenciar a conexão com o banco de dados +def get_db(): + db = SessionLocal() + try: + yield db + finally: + # Garante que a conexão seja fechada mesmo se ocorrer erro + db.close() + + +# Rota GET para listar todos os filmes +@app.get("/filmes", response_model=list[FilmeResponse], status_code=200) +def listar_filmes(db: Session = Depends(get_db)): + try: + filmes = get_filmes(db) + for filme in filmes: + filme.status = 200 + return filmes + except SQLAlchemyError: + # Retorna erro 500 se houver problema no banco + raise HTTPException(status_code=500, detail="Erro ao acessar o banco de dados") + + +# Rota GET para listar filmes inativos +@app.get("/filmes/inativos", response_model=list[FilmeResponse], status_code=200) +def listar_filmes_inativos(db: Session = Depends(get_db)): + try: + filmes = get_filmes_inativos(db) + for filme in filmes: + filme.status = 200 + return filmes + except SQLAlchemyError: + # Retorna erro 500 se houver problema no banco + raise HTTPException(status_code=500, detail="Erro ao acessar o banco de dados") + + +# Rota POST para criar um novo filme +@app.post("/filmes", response_model=FilmeResponse, status_code=201) +def criar_filme(filme: FilmeBase, db: Session = Depends(get_db)): + try: + filme_criado = create_filme(db, filme) + filme_criado.status = 201 + return filme_criado + except IntegrityError: + # Erro 400 se tentar criar filme duplicado + raise HTTPException(status_code=400, detail="Filme já existe no banco de dados") + except SQLAlchemyError: + # Erro 500 para outros problemas no banco + raise HTTPException( + status_code=500, detail="Erro ao criar filme no banco de dados" + ) + + +# Rota GET para buscar um filme específico pelo ID +@app.get("/filmes/{id}", response_model=FilmeResponse, status_code=200) +def obter_filme(id: int, db: Session = Depends(get_db)): + try: + filme = get_filme_by_id(db, id) + if not filme: + # Erro 404 se o filme não for encontrado + raise HTTPException(status_code=404, detail="Filme não encontrado") + filme.status = 200 + return filme + except SQLAlchemyError: + # Erro 500 para problemas no banco + raise HTTPException( + status_code=500, detail="Erro ao buscar filme no banco de dados" + ) + + +# Rota PUT para atualizar um filme existente +@app.put("/filmes/{id}", response_model=FilmeResponse, status_code=200) +def editar_filme(id: int, filme: FilmeBase, db: Session = Depends(get_db)): + try: + filme_data = filme.model_dump() + filme_data["ativo"] = True # Mantém o filme como ativo na atualização + atualizado = update_filme(db, id, filme_data) + if not atualizado: + # Erro 404 se o filme não existir + raise HTTPException(status_code=404, detail="Filme não encontrado") + atualizado.status = 200 + return atualizado + except IntegrityError: + # Erro 400 se os dados forem inválidos + raise HTTPException(status_code=400, detail="Dados inválidos para atualização") + except SQLAlchemyError: + # Erro 500 para problemas no banco + raise HTTPException( + status_code=500, detail="Erro ao atualizar filme no banco de dados" + ) + + +# Rota DELETE para remover um filme +@app.delete("/filmes/{id}", response_model=FilmeResponse, status_code=200) +def excluir_filme(id: int, db: Session = Depends(get_db)): + try: + excluido = delete_filme(db, id) + if not excluido: + # Erro 404 se o filme não existir + raise HTTPException(status_code=404, detail="Filme não encontrado") + excluido.status = 200 + return excluido + except SQLAlchemyError: + # Erro 500 para problemas no banco + raise HTTPException( + status_code=500, detail="Erro ao excluir filme do banco de dados" + ) diff --git a/app/models.py b/app/models.py new file mode 100644 index 00000000..3b1951e7 --- /dev/null +++ b/app/models.py @@ -0,0 +1,11 @@ +from sqlalchemy import Column, Integer, String, Boolean +from db import Base + + +class Filme(Base): + __tablename__ = "filmes" + id = Column(Integer, primary_key=True, index=True) + titulo = Column(String, index=True) + diretor = Column(String) + ano = Column(Integer) + ativo = Column(Boolean, default=True) diff --git a/app/requirements.txt b/app/requirements.txt new file mode 100644 index 00000000..5aec34f9 --- /dev/null +++ b/app/requirements.txt @@ -0,0 +1,4 @@ +fastapi +uvicorn +sqlalchemy +pydantic diff --git a/app/schemas.py b/app/schemas.py new file mode 100644 index 00000000..71d8094c --- /dev/null +++ b/app/schemas.py @@ -0,0 +1,17 @@ +from pydantic import BaseModel + + +class FilmeBase(BaseModel): + titulo: str + diretor: str + ano: int + ativo: bool + + +class FilmeResponse(FilmeBase): + id: int + detail: str + status: int + + class Config: + from_attributes = True