diff --git a/.env b/.env new file mode 100644 index 000000000..cdb9b57eb --- /dev/null +++ b/.env @@ -0,0 +1,3 @@ +#esse arquivo env vai subir para o git por ser um projeto de exemplo + +SQLALCHEMY_DATABASE_URL = "sqlite:///./movie.db" \ No newline at end of file diff --git a/README.md b/README.md index 5c3393a97..fac7573ec 100644 --- a/README.md +++ b/README.md @@ -1,37 +1,62 @@ -![WATTIO](http://wattio.com.br/web/image/1204-212f47c3/Logo%20Wattio.png) +# 🎬 API de Filmes -#### Descrição +Uma API REST para cadastro de filmes, desenvolvida com **FastAPI**, utilizando **SQLAlchemy** para persistência de dados em **SQLite**. -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. +Este projeto faz parte de um desafio técnico com o objetivo de implementar um CRUD completo de filmes, expondo uma API REST funcional, organizada e de fácil manutenção. -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. +## 🚀 Tecnologias utilizadas -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! +- [FastAPI](https://fastapi.tiangolo.com/) +- [SQLAlchemy](https://www.sqlalchemy.org/) +- [SQLite](https://www.sqlite.org/) +- [Pydantic](https://docs.pydantic.dev/) +- [Uvicorn](https://www.uvicorn.org/) +- [Docker](https://www.docker.com/) -#### Sugestão de Ferramentas -Não é obrigatório utilizar todas as as tecnologias sugeridas, mas será um diferencial =] +📚 **Referência complementar**: +[Guia sobre ORMs em Python – Real Python](https://realpython.com/python-sql-libraries/#object-relational-mappers-orms) -- 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) +--- +## 🗂️ Funcionalidades -#### Como começar? +- ✅ Criar um novo filme (`POST /filmes/`) +- ✅ Listar todos os filmes cadastrados (`GET /filmes/`) +- ✅ Buscar um filme por ID (`GET /filmes/{id}`) +- ✅ Atualizar um filme (`PUT /filmes/{id}`) +- ✅ Deletar um filme (`DELETE /filmes/{id}`) -- 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 +--- -#### Dúvidas? +## 📁 Estrutura de diretórios +movies-api/ +├── app/ +│ ├── api/ # Arquivos de rota (endpoints) +│ ├── crud/ # Funções de acesso ao banco +│ ├── db/ # Conexão e base do SQLAlchemy +│ ├── models/ # Modelos do banco (ORM) +│ ├── schemas/ # Schemas Pydantic +│ └── main.py # Ponto de entrada da aplicação +├── movies.db # Arquivo SQLite com os dados +├── .env # Variavel de conexão com o banco +├── dockerfile # Containerização +├── docker-compose.yml +└── README.md # Este arquivo +├── requirements.txt # Dependências do projeto -Qualquer dúvida / sugestão / melhoria / orientação adicional só enviar email para hendrix@wattio.com.br +--- -Salve! +## 🐳 Como rodar com Docker + +1. Clone o repositório: + +```bash +git clone https://github.com/gMoraes1/API-Filmes.git +cd API-Filmes +Construa e execute a aplicação com Docker Compose: +docker compose up +A aplicação estará disponível em: http://localhost:8000/docs +⚠️ Atenção: O arquivo .env foi incluído neste repositório apenas por ser um desafio técnico. +Nunca suba esse tipo de arquivo em projetos reais, pois ele pode conter informações sensíveis, como senhas e logins. diff --git a/app/__pycache__/main.cpython-310.pyc b/app/__pycache__/main.cpython-310.pyc new file mode 100644 index 000000000..e1ba9c6fe Binary files /dev/null and b/app/__pycache__/main.cpython-310.pyc differ diff --git a/app/__pycache__/main.cpython-311.pyc b/app/__pycache__/main.cpython-311.pyc new file mode 100644 index 000000000..302ab8b2c Binary files /dev/null and b/app/__pycache__/main.cpython-311.pyc differ diff --git a/app/api/__pycache__/movies.cpython-310.pyc b/app/api/__pycache__/movies.cpython-310.pyc new file mode 100644 index 000000000..772eaf8ec Binary files /dev/null and b/app/api/__pycache__/movies.cpython-310.pyc differ diff --git a/app/api/__pycache__/movies.cpython-311.pyc b/app/api/__pycache__/movies.cpython-311.pyc new file mode 100644 index 000000000..7944ca379 Binary files /dev/null and b/app/api/__pycache__/movies.cpython-311.pyc differ diff --git a/app/api/movies.py b/app/api/movies.py new file mode 100644 index 000000000..07b82a8a5 --- /dev/null +++ b/app/api/movies.py @@ -0,0 +1,36 @@ +from fastapi import APIRouter, Depends +from sqlalchemy.orm import Session +from app.db.session import get_db +from app.schemas.movie import * +from app.crud import movies as crud +from fastapi import HTTPException + +router = APIRouter() + +@router.post("/filmes/", response_model=Movie) +def create_movie(movie: MovieCreate, db: Session = Depends(get_db)): + return crud.create_movie(db, movie) + +@router.get("/filmes/", response_model=list[Movie]) +def list_movies(skip: int = 0, limit: int = 10, db: Session = Depends(get_db)): + return crud.get_movies(db, skip, limit) + +@router.get("/filmes/{movie_id}", response_model=Movie) +def get_movie(movie_id: int, db: Session = Depends(get_db)): + movie = crud.get_movie_by_id(db, movie_id) + if movie: + return movie + else: + raise HTTPException(status_code=404, detail="Movie not found") + +@router.delete("/filmes/{movie_id}", response_model=Movie) +def delete_movie(movie_id: int, db: Session = Depends(get_db)): + return crud.delete_movie(db, movie_id) + +@router.put("/filmes/{movie_id}", response_model=Movie) +def update_movie(movie_id: int, movie:MovieUpdate, db: Session = Depends(get_db)): + updated = crud.change_movie(db, movie_id, movie) + if updated: + return updated + else: + raise HTTPException(status_code=404, detail="Movie not found") diff --git a/app/crud/__pycache__/movies.cpython-310.pyc b/app/crud/__pycache__/movies.cpython-310.pyc new file mode 100644 index 000000000..77b6ef5d0 Binary files /dev/null and b/app/crud/__pycache__/movies.cpython-310.pyc differ diff --git a/app/crud/__pycache__/movies.cpython-311.pyc b/app/crud/__pycache__/movies.cpython-311.pyc new file mode 100644 index 000000000..4fac38f05 Binary files /dev/null and b/app/crud/__pycache__/movies.cpython-311.pyc differ diff --git a/app/crud/movies.py b/app/crud/movies.py new file mode 100644 index 000000000..a11ccfd6c --- /dev/null +++ b/app/crud/movies.py @@ -0,0 +1,49 @@ +from sqlalchemy.orm import Session +from app.models.movie import Movie +from app.schemas.movie import MovieCreate + + + +def create_movie(db: Session, movie: MovieCreate): + db_movie = Movie( + title=movie.title, + director=movie.director, + year=movie.year + + + ) + db.add(db_movie) + db.commit() + db.refresh(db_movie) + return db_movie + +def get_movies(db: Session, skip: int = 0, limit: int = 10): + return db.query(Movie).offset(skip).limit(limit).all() + + + +def get_movie_by_id(db: Session, movie_id: int): + return db.query(Movie).filter(Movie.id == movie_id).first() + +def delete_movie(db: Session, movie_id: int): + db_movie = db.query(Movie).filter(Movie.id == movie_id).first() + if db_movie: + db.delete(db_movie) + db.commit() + return db_movie + return None + + +def change_movie(db: Session, movie_id: int, movie_update: MovieCreate): + db_movie = db.query(Movie).filter(Movie.id == movie_id).first() + if db_movie: + if movie_update.title is not None: + db_movie.title = movie_update.title + if movie_update.director is not None: + db_movie.director = movie_update.director + if movie_update.year is not None: + db_movie.year = movie_update.year + db.commit() + db.refresh(db_movie) + return db_movie + return None \ No newline at end of file diff --git a/app/db/__pycache__/base.cpython-310.pyc b/app/db/__pycache__/base.cpython-310.pyc new file mode 100644 index 000000000..51dba62bd Binary files /dev/null and b/app/db/__pycache__/base.cpython-310.pyc differ diff --git a/app/db/__pycache__/base.cpython-311.pyc b/app/db/__pycache__/base.cpython-311.pyc new file mode 100644 index 000000000..17ab47fe3 Binary files /dev/null and b/app/db/__pycache__/base.cpython-311.pyc differ diff --git a/app/db/__pycache__/session.cpython-310.pyc b/app/db/__pycache__/session.cpython-310.pyc new file mode 100644 index 000000000..05ea19e9c Binary files /dev/null and b/app/db/__pycache__/session.cpython-310.pyc differ diff --git a/app/db/__pycache__/session.cpython-311.pyc b/app/db/__pycache__/session.cpython-311.pyc new file mode 100644 index 000000000..7cfa2851e Binary files /dev/null and b/app/db/__pycache__/session.cpython-311.pyc differ diff --git a/app/db/base.py b/app/db/base.py new file mode 100644 index 000000000..860e54258 --- /dev/null +++ b/app/db/base.py @@ -0,0 +1,3 @@ +from sqlalchemy.ext.declarative import declarative_base + +Base = declarative_base() diff --git a/app/db/session.py b/app/db/session.py new file mode 100644 index 000000000..a38d0b136 --- /dev/null +++ b/app/db/session.py @@ -0,0 +1,18 @@ +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker +from dotenv import load_dotenv +import os +load_dotenv() + + +engine = create_engine( + os.getenv("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() diff --git a/app/main.py b/app/main.py new file mode 100644 index 000000000..eaa687d0c --- /dev/null +++ b/app/main.py @@ -0,0 +1,10 @@ +from fastapi import FastAPI +from app.api import movies +from app.db.base import Base +from app.db.session import engine + +app = FastAPI(title="Filmes API") + +Base.metadata.create_all(bind=engine) + +app.include_router(movies.router) diff --git a/app/models/__pycache__/movie.cpython-310.pyc b/app/models/__pycache__/movie.cpython-310.pyc new file mode 100644 index 000000000..da6a183d7 Binary files /dev/null and b/app/models/__pycache__/movie.cpython-310.pyc differ diff --git a/app/models/__pycache__/movie.cpython-311.pyc b/app/models/__pycache__/movie.cpython-311.pyc new file mode 100644 index 000000000..8760a316e Binary files /dev/null and b/app/models/__pycache__/movie.cpython-311.pyc differ diff --git a/app/models/movie.py b/app/models/movie.py new file mode 100644 index 000000000..96f4aae59 --- /dev/null +++ b/app/models/movie.py @@ -0,0 +1,13 @@ +from sqlalchemy import Column, Integer, String +from app.db.base import Base + +class Movie(Base): + __tablename__ = "movies" + + id = Column(Integer, primary_key=True, index=True) + title = Column(String, index=True) + director = Column(String) + year = Column(Integer) + + class Config: + from_attributes = True diff --git a/app/schemas/__pycache__/movie.cpython-310.pyc b/app/schemas/__pycache__/movie.cpython-310.pyc new file mode 100644 index 000000000..9853c1ae8 Binary files /dev/null and b/app/schemas/__pycache__/movie.cpython-310.pyc differ diff --git a/app/schemas/__pycache__/movie.cpython-311.pyc b/app/schemas/__pycache__/movie.cpython-311.pyc new file mode 100644 index 000000000..79b2e16c2 Binary files /dev/null and b/app/schemas/__pycache__/movie.cpython-311.pyc differ diff --git a/app/schemas/movie.py b/app/schemas/movie.py new file mode 100644 index 000000000..4086ed39f --- /dev/null +++ b/app/schemas/movie.py @@ -0,0 +1,23 @@ +from pydantic import BaseModel + +class MovieBase(BaseModel): + title: str + director: str + year: int + +class MovieCreate(MovieBase): + pass + +class Movie(MovieBase): + id: int + + class Config: + from_attributes = True + +class MovieUpdate(BaseModel): + title: str | None = None + director: str | None = None + year: int | None = None + + class Config: + from_attributes = True diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 000000000..f2e2b8916 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,22 @@ + +services: + api: + build: . + container_name: movies_api + ports: + - "8000:8000" + env_file: + - .env + volumes: + - ./movies.db:/app/movies.db + restart: always + + redoc: + image: redocly/redoc + container_name: redocly_doc + ports: + - "8080:80" + environment: + SPEC_URL: "http://localhost:8000/openapi.json" + depends_on: + - api diff --git a/dockerfile b/dockerfile new file mode 100644 index 000000000..5c6ed6259 --- /dev/null +++ b/dockerfile @@ -0,0 +1,12 @@ + +FROM python:3.11-slim + +WORKDIR /app + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY ./app ./app +COPY movie.db . + +CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/movie.db b/movie.db new file mode 100644 index 000000000..80f392444 Binary files /dev/null and b/movie.db differ diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 000000000..127779e56 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,5 @@ +fastapi==0.115.3 +pydantic==2.11.1 +SQLAlchemy==2.0.32 +uvicorn==0.30.1 +python-dotenv==1.0.0