diff --git a/README.md b/README.md deleted file mode 100644 index 5c3393a97..000000000 --- a/README.md +++ /dev/null @@ -1,37 +0,0 @@ -![WATTIO](http://wattio.com.br/web/image/1204-212f47c3/Logo%20Wattio.png) - -#### Descrição - -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. - -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 -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) - - -#### 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```** -- Atualize o README.md descrevendo como subir sua aplicação - -#### Dúvidas? - -Qualquer dúvida / sugestão / melhoria / orientação adicional só enviar email para hendrix@wattio.com.br - -Salve! diff --git a/fastapi-filmes/.gitignore b/fastapi-filmes/.gitignore new file mode 100644 index 000000000..e4d82faf2 --- /dev/null +++ b/fastapi-filmes/.gitignore @@ -0,0 +1,80 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +*.egg-info/ +.installed.cfg +*.egg + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ + +# Jupyter Notebook +.ipynb_checkpoints + +# Environments +.env +.venv/ +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# VSCode settings +.vscode/ + +# Docker +*.log +docker-compose.override.yml +docker-compose.override.yaml + +# SQLite database file +*.db +*.sqlite3 + +# Prisma client (se estiver usando) +node_modules/ +prisma/client/ + +# Logs +logs/ +*.log + +# MacOS +.DS_Store + +# Windows +Thumbs.db diff --git a/fastapi-filmes/Dockerfile b/fastapi-filmes/Dockerfile new file mode 100644 index 000000000..2fbc1f8b6 --- /dev/null +++ b/fastapi-filmes/Dockerfile @@ -0,0 +1,20 @@ +# Use imagem oficial do Python 3.11 (ou outra versão compatível) +FROM python:3.11-slim + +# Defina diretório de trabalho no container +WORKDIR /app + +# Copie arquivos de dependências +COPY requirements.txt . + +# Instale dependências +RUN pip install --no-cache-dir -r requirements.txt + +# Copie o restante do código +COPY . . + +# Exponha a porta que a FastAPI vai rodar +EXPOSE 8000 + +# Comando para rodar a aplicação +CMD ["uvicorn", "src.main:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/fastapi-filmes/README.md b/fastapi-filmes/README.md new file mode 100644 index 000000000..5147e79b5 --- /dev/null +++ b/fastapi-filmes/README.md @@ -0,0 +1,109 @@ +# 🎬 FastAPI Filmes +API de gerenciamento de filmes desenvolvida em Python utilizando FastAPI, SQLAlchemy e SQLite. +O projeto foi estruturado seguindo boas práticas de orientação a objetos e inclui testes automatizados com pytest, além de suporte a Docker e docker-compose. + +--- + +## 🚀 Tecnologias Utilizadas +- FastAPI – Framework para APIs rápidas + +- SQLAlchemy – ORM para persistência de dados + +- SQLite – Banco de dados leve + +- Pydantic – Validação de dados + +- Docker – Containerização + +- Pytest – Testes automatizados + +--- + +## 📂 Estrutura do Projeto +```bash +fastapi-filmes/ +│── src/ +│ ├── api/ # Rotas da aplicação +│ ├── core/ # Configurações +│ ├── db/ # Banco de dados +│ ├── schemas/ # Schemas Pydantic +│ ├── models/ # Tabelas do banco +│ ├── services/ # Regras de negócio +│ └── main.py # Ponto de entrada da API +│ +│── tests/ # Testes automatizados +│── Dockerfile +│── docker-compose.yml +│── requirements.txt +│── README.md +└── .gitignore +``` + +## ⚙️ Configuração do Ambiente +### 1️⃣ Clonar o repositório +```bash +git clone https://github.com/seu-usuario/fastapi-filmes.git +cd fastapi-filmes +``` + +### 2️⃣ Criar ambiente virtual (opcional) +```bash +python -m venv venv +source venv/bin/activate # Linux/Mac +venv\Scripts\activate # Windows +``` + +### 3️⃣ Instalar dependências +```bash +pip install -r requirements.txt +``` + +--- + +### ▶️ Executando a API Localmente +```bash +uvicorn src.main:app --reload +``` +Acesse: + +- Swagger: http://localhost:8000/docs + +- ReDoc: http://localhost:8000/redoc + +## 🐳 Executando com Docker +### 1️⃣ Build da imagem +```bash +docker-compose up --build +``` + +A API estará disponível em http://localhost:8000 + +## 🧪 Rodando Testes +Para rodar os testes unitários: + +```bash +pytest tests/ --asyncio-mode=auto -v +``` + +##📌 Endpoints Disponíveis +- GET /filmes + +Retorna a lista de filmes. + +- POST /filmes + +Cria um novo filme. + +Exemplo: + +```json +{ + "title": "Matrix", + "director": "Wachowski", + "year": 1999 +} +``` + +- GET /filmes/{id} + +Retorna um filme específico pelo ID. \ No newline at end of file diff --git a/fastapi-filmes/docker-compose.yml b/fastapi-filmes/docker-compose.yml new file mode 100644 index 000000000..c71cff22a --- /dev/null +++ b/fastapi-filmes/docker-compose.yml @@ -0,0 +1,12 @@ +version: "3.8" + +services: + api: + build: . + ports: + - "8000:8000" + volumes: + - ./:/app + environment: + - PYTHONUNBUFFERED=1 + command: uvicorn src.main:app --host 0.0.0.0 --port 8000 --reload diff --git a/fastapi-filmes/requirements.txt b/fastapi-filmes/requirements.txt new file mode 100644 index 000000000..770e0ba53 --- /dev/null +++ b/fastapi-filmes/requirements.txt @@ -0,0 +1,7 @@ +fastapi +uvicorn +sqlalchemy +pydantic +pytest +pytest-asyncio +httpx diff --git a/fastapi-filmes/src/api/__init__.py b/fastapi-filmes/src/api/__init__.py new file mode 100644 index 000000000..237804332 --- /dev/null +++ b/fastapi-filmes/src/api/__init__.py @@ -0,0 +1 @@ +from .router import router diff --git a/fastapi-filmes/src/api/__pycache__/__init__.cpython-313.pyc b/fastapi-filmes/src/api/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 000000000..6ab72f213 Binary files /dev/null and b/fastapi-filmes/src/api/__pycache__/__init__.cpython-313.pyc differ diff --git a/fastapi-filmes/src/api/__pycache__/router.cpython-313.pyc b/fastapi-filmes/src/api/__pycache__/router.cpython-313.pyc new file mode 100644 index 000000000..a7d9ad3b5 Binary files /dev/null and b/fastapi-filmes/src/api/__pycache__/router.cpython-313.pyc differ diff --git a/fastapi-filmes/src/api/router.py b/fastapi-filmes/src/api/router.py new file mode 100644 index 000000000..bc9a6b138 --- /dev/null +++ b/fastapi-filmes/src/api/router.py @@ -0,0 +1,31 @@ +from fastapi import APIRouter, Depends +from typing import List +from src.db.database import SessionLocal +from sqlalchemy.orm import Session +from src.schemas import Film, FilmCreate +from src.services import FilmService + +router = APIRouter(prefix="/filmes", tags=["Filmes"]) + + +def get_db(): + db = SessionLocal() + try: + yield db + finally: + db.close() + +@router.get("/", response_model=List[Film]) +async def get_films(db: Session = Depends(get_db)): + film_service = FilmService(db) + return film_service.get_all_films() + +@router.post("/", response_model=Film) +async def create_film(film: FilmCreate,db: Session = Depends(get_db)): + film_service = FilmService(db) + return film_service.create_film(film) + +@router.get("/{id}") +def get_film(id: int, db: Session = Depends(get_db)): + service = FilmService(db) + return service.get_film(id) diff --git a/fastapi-filmes/src/core/__init__.py b/fastapi-filmes/src/core/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/fastapi-filmes/src/core/__pycache__/__init__.cpython-313.pyc b/fastapi-filmes/src/core/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 000000000..ff017dc17 Binary files /dev/null and b/fastapi-filmes/src/core/__pycache__/__init__.cpython-313.pyc differ diff --git a/fastapi-filmes/src/core/__pycache__/config.cpython-313.pyc b/fastapi-filmes/src/core/__pycache__/config.cpython-313.pyc new file mode 100644 index 000000000..8effcf380 Binary files /dev/null and b/fastapi-filmes/src/core/__pycache__/config.cpython-313.pyc differ diff --git a/fastapi-filmes/src/core/config.py b/fastapi-filmes/src/core/config.py new file mode 100644 index 000000000..cdfada5ba --- /dev/null +++ b/fastapi-filmes/src/core/config.py @@ -0,0 +1,7 @@ +import os + +class Settings: + PROJECT_NAME: str = "FastAPI Filmes" + DATABASE_URL: str = os.getenv("DATABASE_URL", "sqlite:///./films.db") + +settings = Settings() diff --git a/fastapi-filmes/src/db/__init__.py b/fastapi-filmes/src/db/__init__.py new file mode 100644 index 000000000..d2177c529 --- /dev/null +++ b/fastapi-filmes/src/db/__init__.py @@ -0,0 +1 @@ +from .database import Base, engine, SessionLocal diff --git a/fastapi-filmes/src/db/__pycache__/__init__.cpython-313.pyc b/fastapi-filmes/src/db/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 000000000..45552ce86 Binary files /dev/null and b/fastapi-filmes/src/db/__pycache__/__init__.cpython-313.pyc differ diff --git a/fastapi-filmes/src/db/__pycache__/database.cpython-313.pyc b/fastapi-filmes/src/db/__pycache__/database.cpython-313.pyc new file mode 100644 index 000000000..450f238b8 Binary files /dev/null and b/fastapi-filmes/src/db/__pycache__/database.cpython-313.pyc differ diff --git a/fastapi-filmes/src/db/database.py b/fastapi-filmes/src/db/database.py new file mode 100644 index 000000000..bba58822c --- /dev/null +++ b/fastapi-filmes/src/db/database.py @@ -0,0 +1,8 @@ +from sqlalchemy import create_engine +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import sessionmaker +from src.core.config import settings + +engine = create_engine(settings.DATABASE_URL, connect_args={"check_same_thread": False}) +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) +Base = declarative_base() diff --git a/fastapi-filmes/src/main.py b/fastapi-filmes/src/main.py new file mode 100644 index 000000000..4396b932b --- /dev/null +++ b/fastapi-filmes/src/main.py @@ -0,0 +1,13 @@ +from fastapi import FastAPI +from src.api import router +from src.db import Base, engine + +app = FastAPI(title="API Filmes") + +Base.metadata.create_all(bind=engine) + +app.include_router(router) + +@app.get("/") +async def root(): + return {"message": "API Filmes rodando!"} diff --git a/fastapi-filmes/src/models/film.py b/fastapi-filmes/src/models/film.py new file mode 100644 index 000000000..5cc7fca03 --- /dev/null +++ b/fastapi-filmes/src/models/film.py @@ -0,0 +1,10 @@ +from sqlalchemy import Column, Integer, String +from src.db.database import Base + +class FilmModel(Base): + __tablename__ = "films" + + id = Column(Integer, primary_key=True, index=True) + title = Column(String, index=True) + director = Column(String, index=True) + year = Column(Integer) diff --git a/fastapi-filmes/src/schemas/__init__.py b/fastapi-filmes/src/schemas/__init__.py new file mode 100644 index 000000000..a133a4721 --- /dev/null +++ b/fastapi-filmes/src/schemas/__init__.py @@ -0,0 +1 @@ +from .film import Film, FilmCreate diff --git a/fastapi-filmes/src/schemas/__pycache__/__init__.cpython-313.pyc b/fastapi-filmes/src/schemas/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 000000000..da4717753 Binary files /dev/null and b/fastapi-filmes/src/schemas/__pycache__/__init__.cpython-313.pyc differ diff --git a/fastapi-filmes/src/schemas/__pycache__/film.cpython-313.pyc b/fastapi-filmes/src/schemas/__pycache__/film.cpython-313.pyc new file mode 100644 index 000000000..525ebdbe0 Binary files /dev/null and b/fastapi-filmes/src/schemas/__pycache__/film.cpython-313.pyc differ diff --git a/fastapi-filmes/src/schemas/film.py b/fastapi-filmes/src/schemas/film.py new file mode 100644 index 000000000..a0998654d --- /dev/null +++ b/fastapi-filmes/src/schemas/film.py @@ -0,0 +1,15 @@ +from pydantic import BaseModel + +class FilmBase(BaseModel): + title: str + director: str + year: int + +class FilmCreate(FilmBase): + pass + +class Film(FilmBase): + id: int + + class Config: + orm_mode = True diff --git a/fastapi-filmes/src/services/__init__.py b/fastapi-filmes/src/services/__init__.py new file mode 100644 index 000000000..b69b2867a --- /dev/null +++ b/fastapi-filmes/src/services/__init__.py @@ -0,0 +1 @@ +from .film_service import FilmService diff --git a/fastapi-filmes/src/services/__pycache__/__init__.cpython-313.pyc b/fastapi-filmes/src/services/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 000000000..7032198b7 Binary files /dev/null and b/fastapi-filmes/src/services/__pycache__/__init__.cpython-313.pyc differ diff --git a/fastapi-filmes/src/services/__pycache__/film_service.cpython-313.pyc b/fastapi-filmes/src/services/__pycache__/film_service.cpython-313.pyc new file mode 100644 index 000000000..065f0308c Binary files /dev/null and b/fastapi-filmes/src/services/__pycache__/film_service.cpython-313.pyc differ diff --git a/fastapi-filmes/src/services/film_service.py b/fastapi-filmes/src/services/film_service.py new file mode 100644 index 000000000..14d7447fd --- /dev/null +++ b/fastapi-filmes/src/services/film_service.py @@ -0,0 +1,20 @@ +from sqlalchemy.orm import Session +from src.schemas import FilmCreate +from src.models.film import FilmModel + +class FilmService: + def __init__(self, db: Session): + self.db = db + + def get_all_films(self): + return self.db.query(FilmModel).all() + + def create_film(self, film_create: FilmCreate): + film = FilmModel(**film_create.dict()) + self.db.add(film) + self.db.commit() + self.db.refresh(film) + return film + + def get_film(self, id: int): + return self.db.query(FilmModel).filter(FilmModel.id == id).first() diff --git a/fastapi-filmes/tests/__init__.py b/fastapi-filmes/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/fastapi-filmes/tests/__pycache__/__init__.cpython-313.pyc b/fastapi-filmes/tests/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 000000000..ed9a32a30 Binary files /dev/null and b/fastapi-filmes/tests/__pycache__/__init__.cpython-313.pyc differ diff --git a/fastapi-filmes/tests/__pycache__/test_films.cpython-313-pytest-8.4.1.pyc b/fastapi-filmes/tests/__pycache__/test_films.cpython-313-pytest-8.4.1.pyc new file mode 100644 index 000000000..223d1e6fa Binary files /dev/null and b/fastapi-filmes/tests/__pycache__/test_films.cpython-313-pytest-8.4.1.pyc differ diff --git a/fastapi-filmes/tests/test_films.py b/fastapi-filmes/tests/test_films.py new file mode 100644 index 000000000..e6cefeede --- /dev/null +++ b/fastapi-filmes/tests/test_films.py @@ -0,0 +1,55 @@ +import sys +import os +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) + +import pytest +from httpx import AsyncClient, ASGITransport +from src.main import app + +@pytest.mark.asyncio +async def test_root(): + transport = ASGITransport(app=app) + async with AsyncClient(transport=transport, base_url="http://test") as ac: + response = await ac.get("/") + assert response.status_code == 200 + assert response.json() == {"message": "API Filmes rodando!"} + +@pytest.mark.asyncio +async def test_create_film(): + transport = ASGITransport(app=app) + film_data = { + "title": "Matrix", + "director": "Wachowski", + "year": 1999 + } + async with AsyncClient(transport=transport, base_url="http://test") as ac: + response = await ac.post("/filmes/", json=film_data) + assert response.status_code == 200 + data = response.json() + assert data["title"] == film_data["title"] + assert "id" in data + +@pytest.mark.asyncio +async def test_get_films(): + transport = ASGITransport(app=app) + async with AsyncClient(transport=transport, base_url="http://test") as ac: + response = await ac.get("/filmes/") + assert response.status_code == 200 + data = response.json() + assert isinstance(data, list) + assert len(data) >= 1 + +@pytest.mark.asyncio +async def test_get_film_by_id(): + transport = ASGITransport(app=app) + async with AsyncClient(transport=transport, base_url="http://test") as ac: + # Cria um filme para garantir que existe + film_data = {"title": "Matrix", "director": "Wachowski", "year": 1999} + post_response = await ac.post("/filmes/", json=film_data) + id = post_response.json()["id"] # sobrescrevendo apenas aqui + + # Busca pelo ID + get_response = await ac.get(f"/filmes/{id}") + assert get_response.status_code == 200 + +