diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..34bd98e1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,134 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +/data +__pycache__ +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# 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/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyderworkspace + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ \ No newline at end of file diff --git a/README.md b/README.md index 5c3393a9..0191bcc1 100644 --- a/README.md +++ b/README.md @@ -1,37 +1,99 @@ -![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: +### Como utilizar + +#### Iniciando + +1. Faça o clone do projeto: +```bash +git clone git@github.com:danpqdan/backend.git +``` + +#### Executando + +2. 1. Certifique-se do docker e o docker-compose estar funcionando: +```bash +docker --version +docker-compose --version +``` + +2. 2. Localize a pasta do projeto e abra o terminal na pasta raiz como administrador + +2. 3. Executando o projeto: +```bash +# -d sair do console de build +# --build construi o projeto e as novas alterações +docker-compose up -d --build +``` +ou + +```bash +# Caso queira iniciar o projeto sem as futuras atualizações e com console de logs +docker compose up +``` + +#### Consumindo + +3. 1. Servidor estará rodando na porta: **8000** + +3. 2. Acesse a documentação via navegador: +```bash +http://0.0.0.0:8000/docs +``` + +3. 3. Realizando seu primeiro post via terminal: +```bash +curl -X POST "http://127.0.0.1:8000/filmes" + -H "Content-Type: application/json" + -d '{ + "titulo": "A procura da felicidade", + "descricao": "Chris enfrenta sérios problemas financeiros e sua esposa, Linda, decide partir. Agora solteiro, ele precisa cuidar de Christopher, seu filho de cinco anos. Chris tenta usar sua habilidade como vendedor para conseguir um emprego melhor, mas só consegue um estágio não-remunerado.", + "ano": 2007 + }' +``` + + + +
+ Descrição do Desafio + ![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. + - `/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! + 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 =] + #### 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) + - 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? + #### 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 + - 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? + #### Dúvidas? -Qualquer dúvida / sugestão / melhoria / orientação adicional só enviar email para hendrix@wattio.com.br + Qualquer dúvida / sugestão / melhoria / orientação adicional só enviar email para hendrix@wattio.com.br Salve! +
diff --git a/controllers/FilmeController.py b/controllers/FilmeController.py new file mode 100644 index 00000000..b18f6a8d --- /dev/null +++ b/controllers/FilmeController.py @@ -0,0 +1,53 @@ +from typing import List +from fastapi import APIRouter, HTTPException +from pydantic import ValidationError +from entities.Filmes import Filme +from services.FilmeService import FilmeService +from fastapi.responses import JSONResponse + +filmeService = FilmeService() + +filme_router = APIRouter(prefix="/filmes", tags=["Filmes"]) + +@filme_router.get("", response_model=List[Filme]) +def retornar_filmes(): + try: + filmes = filmeService.listar_filmes() + return JSONResponse( + content={"Filmes: ": filmes}, + status_code=200 + ) + except Exception: + raise HTTPException( + status_code=500, + detail="Ocorreu um erro inesperado. Por favor, tente novamente mais tarde." + ) + +@filme_router.get("/{id}", response_model=Filme) +def retornar_um_filme(id: int): + try: + filme = FilmeService.buscar_filme_por_id(id) + if not filme: + raise HTTPException(status_code=404, detail="Filme não encontrado") + return filme + except Exception as e: + raise HTTPException(status_code=400, detail=f"Erro ao buscar filme: {str(e)}") + +@filme_router.post("", response_model=Filme) +def criar_filme(filme: Filme): + try: + filmeService.adicionar_filme((filme.titulo, filme.descricao, filme.ano)) + return JSONResponse( + content={"message": "Filme adicionado com sucesso","filme": filme.model_dump(exclude={"id"})}, + status_code=201 + ) + except ValidationError: + raise HTTPException( + status_code=400, + detail="Erro de validação: Os campos 'titulo', 'descricao' e 'ano' precisam ser preenchidos corretamente." + ) + except Exception: + raise HTTPException( + status_code=500, + detail="Ocorreu um erro inesperado. Por favor, tente novamente mais tarde." + ) diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..de2fb005 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,11 @@ +version: '3.8' +services: + app: + build: + context: . + volumes: + - ./data:/app/data + environment: + - DB_FOLDER=/app/data + ports: + - "8000:8000" diff --git a/dockerfile b/dockerfile new file mode 100644 index 00000000..e3170e5e --- /dev/null +++ b/dockerfile @@ -0,0 +1,17 @@ +# Usar uma imagem Python como base +FROM python:3.12-slim + +# Update da imagem e evitar que a imagem contenha arquivos temporários ou desnecessários +RUN apt-get update && apt-get install -y sqlite3 && rm -rf /var/lib/apt/lists/* + +# Configurar o diretório de trabalho no contêiner +WORKDIR /app + +# Copiar os arquivos principais para o contêiner +COPY . /app + +# Instalar dependências Python +RUN pip install --no-cache-dir -r requirements.txt + +# Comando padrão para rodar a aplicação +CMD ["python", "main.py"] diff --git a/entities/Filmes.py b/entities/Filmes.py new file mode 100644 index 00000000..acf54140 --- /dev/null +++ b/entities/Filmes.py @@ -0,0 +1,14 @@ +from pydantic import BaseModel, Field, model_validator +from typing import Optional + +class Filme(BaseModel): + id: Optional[int] = None + titulo: str + descricao: str + ano: int + + @model_validator(mode='before') + def check_required_fields(cls, values): + if not values.get('titulo') or not values.get('descricao') or not values.get('ano'): + raise ValueError("Os campos 'titulo', 'descricao' e 'ano' precisam ser preenchidos corretamente.") + return values diff --git a/interfaces/FilmeDB.py b/interfaces/FilmeDB.py new file mode 100644 index 00000000..0da76b1d --- /dev/null +++ b/interfaces/FilmeDB.py @@ -0,0 +1,67 @@ +import os +import sqlite3 + +class Conexao: + def __init__(self, db_name="filme.db"): + self.db_folder = os.getenv("DB_FOLDER", "./data") + os.makedirs(self.db_folder, exist_ok=True) + self.db_file = os.path.join(self.db_folder, db_name) + self.db = None + self.connect() + + def connect(self): + print(f"Conexão estabelecida com o banco: {self.db_file}") + self.db = sqlite3.connect(self.db_file) + + def create_tables(self): + cursor = self.db.cursor() + cursor.execute(''' + CREATE TABLE IF NOT EXISTS Filme ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + titulo TEXT NOT NULL, + descricao TEXT, + ano INTEGER + ) + ''') + self.db.commit() + self.db.close() + + def fechar(self): + if self.db: + self.db.close() + print("Conexão encerrada.") + + + def gravar(self, sql, params=None): + try: + cursor = self.db.cursor() + if params: + cursor.execute(sql, params) + else: + cursor.execute(sql) + self.db.commit() + print("Gravação bem-sucedida.") + except sqlite3.Error as e: + print(f"Erro ao gravar no banco: {e}") + return False + return True + + def consultar_um(self, sql, params=None): + try: + cursor = self.db.cursor() + cursor.execute(sql, params or []) + response = cursor.fetchone() + return response + except sqlite3.Error as e: + print(f"Erro ao consultar no banco: {e}") + return None + + def consultar_lista(self, sql, params=None): + try: + cursor = self.db.cursor() + cursor.execute(sql, params or []) + response = cursor.fetchall() + return response + except sqlite3.Error as e: + print(f"Erro ao consultar no banco: {e}") + return None diff --git a/main.py b/main.py new file mode 100644 index 00000000..4447aad6 --- /dev/null +++ b/main.py @@ -0,0 +1,21 @@ +from fastapi import FastAPI +from interfaces.FilmeDB import Conexao +from controllers.FilmeController import filme_router + +def lifespan(app: FastAPI): + conexao = Conexao() + conexao.connect() + conexao.create_tables() + print("Startup tasks completed") + yield + print("Shutdown tasks completed") + conexao.fechar() + + +app = FastAPI(title="CRUD de Filmes", version="1.0", lifespan=lifespan) + +app.include_router(filme_router) + +if __name__ == "__main__": + import uvicorn + uvicorn.run(app, host="0.0.0.0", port=8000) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000..6f745c3f --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +fastapi +pydantic +uvicorn \ No newline at end of file diff --git a/services/FilmeService.py b/services/FilmeService.py new file mode 100644 index 00000000..7220c870 --- /dev/null +++ b/services/FilmeService.py @@ -0,0 +1,49 @@ +from pydantic import ValidationError +from entities.Filmes import Filme +from interfaces.FilmeDB import Conexao +from typing import List, Optional, Tuple +class FilmeService: + + @staticmethod + def listar_filmes() -> List[Filme]: + try: + conexão = Conexao() + sql = "SELECT * FROM Filme" + filmes = conexão.consultar_lista(sql) + if not filmes: + return [] + return filmes + except Exception as e: + return f'Erro ao consultar lista de filme: {str(e)}' + + @staticmethod + def buscar_filme_por_id(id: int) -> Optional[Filme]: + try: + conexão = Conexao() + sql = "SELECT * FROM Filme WHERE id = ?" + filme = conexão.consultar_um(sql, (id,)) + if filme: + return Filme(id=filme[0], titulo=filme[1], descricao=filme[2], ano=filme[3]) + return None + except Exception as e: + print(f"Erro ao buscar filme por ID: {str(e)}") + return None + + @staticmethod + def adicionar_filme(values: Tuple[str, str, int]): + titulo, descricao, ano = values + try: + conexao = Conexao() + sql = ("INSERT INTO Filme (titulo, descricao, ano) VALUES (?, ?, ?) ") + params = (titulo, descricao, ano) + if conexao.gravar(sql, params): + return "sucesso" + else: + return "Erro ao gravar no banco." + except Exception as e: + return f'Erro ao salvar o filme: {str(e)}' + + + + + \ No newline at end of file