diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..b8be4ea6a --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +__pycache__ + +.env +venv \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 000000000..d959494ef --- /dev/null +++ b/Dockerfile @@ -0,0 +1,13 @@ +FROM python:3.11-slim + +WORKDIR /app + +COPY requirements.txt . + +RUN pip install --no-cache-dir -r requirements.txt + +COPY . . + +EXPOSE 8000 + +CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"] diff --git a/README.md b/README.md index 5c3393a97..548d7291d 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,37 @@ -![WATTIO](http://wattio.com.br/web/image/1204-212f47c3/Logo%20Wattio.png) +![WATTIO](./img/banner-preview.png) + +## Como executar a aplicação + +Para executar a aplicação, siga os seguintes passos: + +1. **Pré-requisitos:** + * [Docker](https://www.docker.com/) + * [Docker Compose](https://docs.docker.com/compose/install/) + +2. **Clone o repositório:** + ```bash + git clone https://github.com/lucasaguiar-la/desafio-backend-wattio + ``` + +3. **Inicie a aplicação:** + ```bash + docker-compose up + ``` +A API estará disponível em `http://localhost:8000/` + +4. **Cadastrar um filme** +Ao fazer uma requisição [POST] para `/filmes`, envie o seguinte JSON no corpo da requisição: + + ```json + { + "movie_name": "O Senhor dos Anéis: O Retorno do Rei", + "synopsis": "Gandalf e Aragorn lideram o Mundo dos Homens contra o exército de Sauron para desviar o olhar de Frodo e Sam quando eles se aproximam á Montanha da Perdição com o Um Anel.", + "duration": 201, + "year_release": 2003, + "movie_gender": "Fantasia" + } + ``` +--- #### Descrição @@ -15,23 +48,9 @@ O Objetivo é te desafiar e reconhecer seu esforço para aprender e se adaptar. #### 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 +- [x] Orientação a objetos (utilizar objetos, classes para manipular os filmes) +- [x] [FastAPI](https://fastapi.tiangolo.com/) (API com documentação auto gerada) +- [x] [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 ``` +- [x] 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) -Salve! +--- diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 000000000..f7824e9c2 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,37 @@ +version: "3.8" + +services: + db: + image: postgres:15 + container_name: postgres_movies + restart: always + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: moviesdb + volumes: + - postgres_data:/var/lib/postgresql/data + - ./src/database/schema.sql:/docker-entrypoint-initdb.d/schema.sql + ports: + - "5433:5432" + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres"] + interval: 5s + timeout: 5s + retries: 5 + + api: + build: . + container_name: fastapi_movies + depends_on: + db: + condition: service_healthy + ports: + - "8000:8000" + env_file: + - .env + volumes: + - .:/app + +volumes: + postgres_data: diff --git a/img/banner-preview.png b/img/banner-preview.png new file mode 100644 index 000000000..ef5a7290b Binary files /dev/null and b/img/banner-preview.png differ diff --git a/main.py b/main.py new file mode 100644 index 000000000..a0a913e91 --- /dev/null +++ b/main.py @@ -0,0 +1,11 @@ +from fastapi import FastAPI +from src.controller import movie_controller + +app = FastAPI() +app.include_router(movie_controller.router) + +@app.get('/') +def root(): + return { + 'Status': 'API rodando...' + } \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 000000000..e18021ac6 Binary files /dev/null and b/requirements.txt differ diff --git a/src/controller/movie_controller.py b/src/controller/movie_controller.py new file mode 100644 index 000000000..362af2983 --- /dev/null +++ b/src/controller/movie_controller.py @@ -0,0 +1,32 @@ +from src.models.movie import MovieModel +from src.repositories.movie_repository import MovieRepository + +from fastapi import APIRouter, HTTPException +from psycopg2.errors import UniqueViolation + +router = APIRouter() +movie_repository = MovieRepository() + +@router.post('/filmes', status_code=201) +def register_movie(movie: MovieModel): + try: + movie_repository.create(movie) + return { + 'Mensagem': 'Filme registrado com sucesso!' + } + except UniqueViolation: + raise HTTPException(status_code=409, detail=f'O filme {movie.movie_name} já está cadastrado.') + except Exception as e: + raise HTTPException(status_code=500, detail='Ocorreu um erro interno no servidor.') + +@router.get('/filmes') +def get_all_movies(): + return movie_repository.get_all() + +@router.get('/filmes/{id}') +def get_one_movie(id: int): + movie = movie_repository.get_by_id(id) + if movie: + return movie + else: + raise HTTPException(status_code=404, detail='Filme não encontrado :(') \ No newline at end of file diff --git a/src/database/connection.py b/src/database/connection.py new file mode 100644 index 000000000..d3a86d6d4 --- /dev/null +++ b/src/database/connection.py @@ -0,0 +1,8 @@ +import psycopg2 +from dotenv import load_dotenv +import os + +load_dotenv() + +def get_connection(): + return psycopg2.connect(os.getenv('DATABASE_URL')) \ No newline at end of file diff --git a/src/database/schema.sql b/src/database/schema.sql new file mode 100644 index 000000000..a31412d9e --- /dev/null +++ b/src/database/schema.sql @@ -0,0 +1,8 @@ +CREATE TABLE IF NOT EXISTS movies( + id SERIAL PRIMARY KEY, + movie_name TEXT NOT NULL UNIQUE, + synopsis TEXT, + duration INT, + year_release INT, + movie_gender TEXT +); \ No newline at end of file diff --git a/src/models/movie.py b/src/models/movie.py new file mode 100644 index 000000000..36407ca70 --- /dev/null +++ b/src/models/movie.py @@ -0,0 +1,8 @@ +from pydantic import BaseModel + +class MovieModel(BaseModel): + movie_name: str + synopsis: str + duration: int + year_release: int + movie_gender: str \ No newline at end of file diff --git a/src/repositories/movie_repository.py b/src/repositories/movie_repository.py new file mode 100644 index 000000000..8aa7710d7 --- /dev/null +++ b/src/repositories/movie_repository.py @@ -0,0 +1,71 @@ +from src.database.connection import get_connection +from src.models.movie import MovieModel + +class MovieRepository: + def __init__(self): + self.conn = get_connection() + + def get_all(self): + cur = self.conn.cursor() + + cur.execute(''' + SELECT id, movie_name, synopsis, duration, year_release, movie_gender + FROM movies; + ''') + + rows = cur.fetchall() + cur.close() + + movies = [] + for row in rows: + movies.append({ + 'id': row[0], + 'movie_name': row[1], + 'synopsis': row[2], + 'duration': row[3], + 'year_release': row[4], + 'movie_gender': row[5] + }) + + return movies + + def get_by_id(self, id: int): + cur = self.conn.cursor() + + cur.execute(''' + SELECT id, movie_name, synopsis, duration, year_release, movie_gender + FROM movies + WHERE id = %s; + ''', (id,)) + + row = cur.fetchone() + cur.close() + + if row: + return { + 'id': row[0], + 'movie_name': row[1], + 'synopsis': row[2], + 'duration': row[3], + 'year_release': row[4], + 'movie_gender': row[5] + } + return None + + def create(self, movie: MovieModel): + cur = self.conn.cursor() + + cur.execute(''' + INSERT INTO movies (movie_name, synopsis, duration, year_release, movie_gender) + VALUES (%s, %s, %s, %s, %s); + ''', (movie.movie_name, movie.synopsis, movie.duration, movie.year_release, movie.movie_gender)) + + self.conn.commit() + cur.close() + + def __del__(self): + try: + if hasattr(self, 'conn') and self.conn: + self.conn.close() + except: + pass \ No newline at end of file