Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"python-envs.pythonProjects": []
}
11 changes: 11 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
FROM python:3.9-slim

WORKDIR /app

COPY requirements.txt .

RUN pip install --no-cache-dir -r requirements.txt

COPY ./app/* ./

CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
120 changes: 96 additions & 24 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,37 +1,109 @@
![WATTIO](http://wattio.com.br/web/image/1204-212f47c3/Logo%20Wattio.png)
# Filmes API

#### Descrição
API REST para cadastro e consulta de filmes, desenvolvida com FastAPI e SQLAlchemy, para desafio proposto por Watt-io.

O desafio consiste em implementar um CRUD de filmes, utilizando [python](https://www.python.org/ "python") integrando com uma API REST e uma possível persistência de dados.

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.
## Pré-requisitos

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!
- Python 3.8 ou superior
- pip (gerenciador de pacotes do Python)
- Banco de dados SQLite (padrão)
- (Opcional) Docker e Docker Compose

#### 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 ```
- 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)

## Instalação

#### Como começar?
1. **Clone o repositório**
```sh
git clone <URL_DO_REPOSITORIO>
cd filmes_api
```

- 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```**
- Atualize o README.md descrevendo como subir sua aplicação
2. **Crie e ative um ambiente virtual**
```sh
python -m venv venv
venv\Scripts\activate
```

#### Dúvidas?
3. **Instale as dependências**
```sh
pip install -r requirements.txt
```

Qualquer dúvida / sugestão / melhoria / orientação adicional só enviar email para [email protected]
4. **Sobre o banco de dados**
- Por padrão, a API usa SQLite. Se quiser usar outro banco, ajuste a string de conexão no arquivo de configuração.

Salve!


## Execução

Rode a aplicação com Docker, usando o comando dentro da pasta raíz do projeto:
```sh
docker run -d -p 8000:8000 filmes_api
```

ou

Execute pelo servidor de desenvolvimento, usando o comando dentro da pasta raíz do projeto:
```sh
uvicorn app.main:app --reload
```

Acesse a documentação auto-gerada com swagger em: [http://localhost:8000/docs]



## Estrutura do Projeto

```
filmes_api/
├── app/
│ ├── api/
| ├── v1/
| ├── routes_movies.py
│ ├── core/
| ├── config.py
│ ├── crud/
| ├── movie.py
│ ├── data/
│ ├── db/
| ├── base.py
| ├── init_db.py
| ├── session.py
│ ├── models/
| ├── movie.py
│ ├── schemas/
| ├── movie.py
│ ├── __init__.py
│ └── main.py
├── data/
├── docker-compose.yml
├── Dockerfile
├── README.md
└── requirements.txt
```



## Endpoints Principais

- `GET /api/v1/routes_movies` — Lista todos os filmes
- `GET /api/v1/routes_movies/{id}` — Consulta filme por ID
- `GET /api/v1/routes_movies/title/{title}` — Lista filmes cujo o titulo contem string passada em {title}
- `POST /api/v1/routes_movies` — Cria um novo filme
- `PUT /api/v1/routes_movies/{id}` — Atualiza filme pelo ID
- `DELETE /api/v1/routes_movies/{id}` — Remove filme pelo ID

Consulte a documentação do Swagger para detalhes e exemplos.



## Observações

- Escrevi o código todo em ingles, por padrão pessoal.
- Adicionei rotas para Update e Delete (na descrição do desafio era obrigatório apenas rotas para retornar todos filmes | retornar um especifico | inserir), fiz dessa maneira para atender os requisitos de [C]reate [R]ead [U]pdate [D]elete.
- Para dúvidas ou comentários, contate-me via email ([email protected]) ou celular (+5535984325692).

---
1 change: 1 addition & 0 deletions app/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
#empty
86 changes: 86 additions & 0 deletions app/api/v1/routes_movies.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
from typing import List

from app.schemas.movie import Movie, MovieCreate, MovieUpdate
from app.crud.movie import (
get_movies,
get_movie,
get_movies_by_title,
create_movie,
update_movie,
delete_movie,
)
from app.db.session import get_db

router = APIRouter()

# Define the API routes for listing movies
@router.get(
"/",
response_model=List[Movie],
summary="List movies",
description="Returns a list of all registered movies."
)
def read_movies(skip: int = 0, limit: int = 10, db: Session = Depends(get_db)):
return get_movies(db, skip=skip, limit=limit)


@router.get(
"/{movie_id}",
response_model=Movie,
summary="Get movie by ID",
description="Returns a specific movie by its ID."
)
def read_movie(movie_id: int, db: Session = Depends(get_db)):
db_movie = get_movie(db, movie_id)
if db_movie is None:
raise HTTPException(status_code=404, detail="Movie not found")
return db_movie


@router.get(
"/title/{title}",
response_model=List[Movie],
summary="Get movies by title",
description="Returns a list of movies that match the given title."
)
def read_movies_by_title(title: str, db: Session = Depends(get_db)):
return get_movies_by_title(db, title)


@router.post(
"/",
response_model=Movie,
status_code=201,
summary="Create new movie",
description="Adds a new movie."
)
def create_new_movie(movie: MovieCreate, db: Session = Depends(get_db)):
return create_movie(db, movie)


@router.put(
"/{movie_id}",
response_model=Movie,
summary="Update movie",
description="Updates an existing movie by its ID."
)
def update_existing_movie(movie_id: int, movie: MovieUpdate, db: Session = Depends(get_db)):
db_movie = update_movie(db, movie_id, movie)
if db_movie is None:
raise HTTPException(status_code=404, detail="Movie not found")
return db_movie


@router.delete(
"/{movie_id}",
response_model=Movie,
summary="Delete movie",
description="Removes a movie by its ID."
)
def delete_existing_movie(movie_id: int, db: Session = Depends(get_db)):
db_movie = delete_movie(db, movie_id)
if db_movie is None:
raise HTTPException(status_code=404, detail="Movie not found")
return db_movie
11 changes: 11 additions & 0 deletions app/core/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from pydantic_settings import BaseSettings

class Settings(BaseSettings):
PROJECT_NAME: str = "Movies API"
PROJECT_VERSION: str = "1.0.1"
DATABASE_URL: str = "sqlite:///./movies.db"

class Config:
env_file = ".env"

settings = Settings()
43 changes: 43 additions & 0 deletions app/crud/movie.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
from sqlalchemy.orm import Session
from typing import List, Optional
from app.models.movie import Movie
from app.schemas.movie import MovieCreate, MovieUpdate

def get_movies(db: Session, skip: int = 0, limit: int = 10) -> List[Movie]:
return db.query(Movie).offset(skip).limit(limit).all()


def get_movie(db: Session, movie_id: int) -> Optional[Movie]:
return db.query(Movie).filter(Movie.id == movie_id).first()


def get_movies_by_title(db: Session, title: str) -> List[Movie]:
return db.query(Movie).filter(Movie.title.ilike(f"%{title}%")).all()


def create_movie(db: Session, movie: MovieCreate) -> Movie:
db_movie = Movie(**movie.dict())
db.add(db_movie)
db.commit()
db.refresh(db_movie)
return db_movie


def update_movie(db: Session, movie_id: int, movie: MovieUpdate) -> Optional[Movie]:
db_movie = db.query(Movie).filter(Movie.id == movie_id).first()
if not db_movie:
return None
for key, value in movie.dict(exclude_unset=True).items():
setattr(db_movie, key, value)
db.commit()
db.refresh(db_movie)
return db_movie


def delete_movie(db: Session, movie_id: int) -> Optional[Movie]:
db_movie = db.query(Movie).filter(Movie.id == movie_id).first()
if not db_movie:
return None
db.delete(db_movie)
db.commit()
return db_movie
5 changes: 5 additions & 0 deletions app/db/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from sqlalchemy.ext.declarative import declarative_base


Base = declarative_base()

6 changes: 6 additions & 0 deletions app/db/init_db.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from app.db.session import engine
from app.db.base import Base
from app.models.movie import Movie

def init_db():
Base.metadata.create_all(bind=engine)
20 changes: 20 additions & 0 deletions app/db/session.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
import os

SQLALCHEMY_DATABASE_URL = "sqlite:///./data/movies.db"

os.makedirs("data", exist_ok=True)

engine = create_engine(
SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False}
)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)


def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
12 changes: 12 additions & 0 deletions app/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from fastapi import FastAPI
from app.api.v1.routes_movies import router as movies_router
from app.core.config import settings
from app.db.init_db import init_db

app = FastAPI(title=settings.PROJECT_NAME, version=settings.PROJECT_VERSION)

#Starts the database
init_db()

#Routes
app.include_router(movies_router, prefix="/api/v1/movies", tags=["Movies"])
15 changes: 15 additions & 0 deletions app/models/movie.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from sqlalchemy import Column, Integer, String, Text, DateTime
from sqlalchemy.sql import func
from app.db.base import Base

class Movie(Base):
__tablename__ = "movies"

id = Column(Integer, primary_key=True, index=True)
title = Column(String(255), nullable=False, index=True)
director = Column(String(255), nullable=True)
year = Column(Integer, nullable=False)
synopsis = Column(Text, nullable=True)
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
genre = Column(String(255), nullable=True)
30 changes: 30 additions & 0 deletions app/schemas/movie.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
from pydantic import BaseModel
from typing import Optional
from datetime import datetime

class MovieBase(BaseModel):
title: str
director: str
year: int
synopsis: Optional[str] = None
genre: Optional[str] = None


class MovieCreate(MovieBase):
pass


class MovieUpdate(BaseModel):
title: Optional[str] = None
director: Optional[str] = None
year: Optional[int] = None
synopsis: Optional[str] = None


class Movie(MovieBase):
id: int
created_at: datetime
updated_at: Optional[datetime]

class Config:
orm_mode = True
Loading