diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..dc12cb74 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.env +__pycache__ \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..f4b7728d --- /dev/null +++ b/Dockerfile @@ -0,0 +1,13 @@ +FROM python:3.12-slim + +WORKDIR /app + +COPY requirements.txt requirements.txt + +RUN pip install --no-cache-dir --upgrade -r /app/requirements.txt + +COPY . . + +EXPOSE 8000 + +CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"] \ No newline at end of file diff --git a/README.md b/README.md index 5c3393a9..545208b8 100644 --- a/README.md +++ b/README.md @@ -1,37 +1,44 @@ -![WATTIO](http://wattio.com.br/web/image/1204-212f47c3/Logo%20Wattio.png) +# FastAPI com MySQL -#### Descrição +Este é um projeto de exemplo de uma API com FastAPI e persistência em um banco de dados MySQL. -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. +## Inicialização com Docker -Rotas da API: +Para inicializar o projeto com docker, execute os comandos abaixo na raiz do projeto: +```bash +docker compose build +docker compose up +``` +Após 15 segundos, a aplicação deverá estar rodando. - - `/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. +## Estrutura do Projeto -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! +- `main.py`: Ponto de entrada da aplicação FastAPI. +- `controllers/`: Contém os controladores da API. +- `models/`: Define os modelos de dados e interfaces de repositório. +- `services/`: Contém serviços auxiliares, como a conexão com o banco de dados. +- `utils/`: Inclui utilitários, como a configuração do banco de dados. +- `docker-compose.yml`: Arquivo de configuração para o Docker Compose. +- `Dockerfile`: Define como a imagem Docker é construída. -#### Sugestão de Ferramentas -Não é obrigatório utilizar todas as as tecnologias sugeridas, mas será um diferencial =] +## Variáveis de Ambiente -- 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) +Certifique-se de definir as variáveis de ambiente necessárias no arquivo `.env` antes de executar o build do projeto: +- `MYSQL_USER`: Usuário do banco de dados MySQL. (padrão: `root` (não recomendado em produção, ajustar docker-compose)) +- `MYSQL_PASSWORD`: Senha do banco de dados MySQL. (padrão: `root` (não recomendado em produção, ajustar docker-compose)) +- `MYSQL_DATABASE`: Nome do banco de dados a ser utilizado. (padrão: `filmesDB`) +- `DB_TYPE`: Tipo de banco de dados (`mysql` é a única implementação atualmente). -#### Como começar? +## Testando a 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 +Após iniciar o projeto, você pode acessar a documentação interativa da API em `http://localhost:8000/docs`. -#### Dúvidas? +## Finalizando o Projeto -Qualquer dúvida / sugestão / melhoria / orientação adicional só enviar email para hendrix@wattio.com.br +Para parar e remover os contêineres do Docker, utilize o comando: + +```bash +docker compose down +``` -Salve! diff --git a/controllers/filmeController.py b/controllers/filmeController.py new file mode 100644 index 00000000..625424e4 --- /dev/null +++ b/controllers/filmeController.py @@ -0,0 +1,57 @@ +from typing import List +from fastapi import APIRouter +from models.interfaces.filmeRepoInterface import FilmeRepoInterface +from models.filmeModel import FilmeModel, FilmeResponse + +class FilmeController(): + + def __init__(self, filmeRepo : FilmeRepoInterface): + + """ + Construtor da classe FilmeController. + + Args: + filmeRepo (FilmeRepoInterface): Interface de repositório de filmes. + + Attributes: + filmeRepo (FilmeRepoInterface): Interface de repositório de filmes. + router (FastAPI.Routers): Roteador de endpoints da API. + """ + + self.filmeRepo = filmeRepo + + self.router = APIRouter(tags=["Filmes"]) + + self.router.add_api_route("/filmes", self.get_filmes, methods=["GET"], response_model = List[FilmeResponse]) + self.router.add_api_route("/filmes", self.post_filme, methods=["POST"]) + self.router.add_api_route("/filmes/{id}", self.get_filme_by_id, methods=["GET"], response_model = List[FilmeResponse]) + + def get_filmes(self): + """ + Retorna todos os filmes + + Returns: + List[FilmeResponse]: lista de filmes + """ + return self.filmeRepo.get_filmes() + + def post_filme(self, filme: FilmeModel): + """ + Cria um novo filme + """ + + return self.filmeRepo.post_filme(filme) + + def get_filme_by_id(self, id: int): + """ + Retorna um filme pelo seu id + + Args: + id (int): o id do filme a ser retornado + + Returns: + List[FilmeResponse]: o filme com o id especificado + """ + return self.filmeRepo.get_filme_by_id(id) + + diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..3812e7d6 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,22 @@ +version: '1.0' + +services: + fastapi: + build: . + ports: + - 8000:8000 + env_file: + - .env + volumes: + - .:/app + depends_on: + - mysql + entrypoint: sh -c "sleep 10 && uvicorn main:app --host 0.0.0.0 --port 8000" + + mysql: + container_name: mysqldb + image: mysql:8.0 + ports: + - 3307:3306 + environment: + MYSQL_ROOT_PASSWORD: root \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 00000000..cdf9add0 --- /dev/null +++ b/main.py @@ -0,0 +1,18 @@ +from fastapi import FastAPI +from utils.db import mySqlDb +from controllers.filmeController import FilmeController +from services.connectionService import determinar_conexao + +app = FastAPI() + +repo = determinar_conexao() +item_controller = FilmeController(repo) + +app.include_router(item_controller.router) + +@app.get("/") +def root(): + """ + Rota raiz que redireciona para a documentação da API. + """ + return {"message": "Acesse a documentação da API em /docs"} diff --git a/models/filmeModel.py b/models/filmeModel.py new file mode 100644 index 00000000..f7795a43 --- /dev/null +++ b/models/filmeModel.py @@ -0,0 +1,10 @@ +from pydantic import BaseModel, Field + +class FilmeModel(BaseModel): + titulo: str = Field(max_length=255) + diretor: str = Field(max_length=255) + ano_lancamento: int + genero: str = Field(max_length=255) + +class FilmeResponse(FilmeModel): + id: int \ No newline at end of file diff --git a/models/interfaces/filmeRepoInterface.py b/models/interfaces/filmeRepoInterface.py new file mode 100644 index 00000000..8fb5f954 --- /dev/null +++ b/models/interfaces/filmeRepoInterface.py @@ -0,0 +1,21 @@ +from typing import List +from models.filmeModel import FilmeModel + +class FilmeRepoInterface: + def get_filmes(self) -> List[FilmeModel]: + """ + Retorna todos os filmes + """ + pass + + def post_filme(self, filme: FilmeModel) -> FilmeModel: + """ + Cria um novo filme + """ + pass + + def get_filme_by_id(self, id: int) -> FilmeModel: + """ + Retorna um filme pelo seu id + """ + pass diff --git a/models/repos/filmeRepoMySQL.py b/models/repos/filmeRepoMySQL.py new file mode 100644 index 00000000..02fa620a --- /dev/null +++ b/models/repos/filmeRepoMySQL.py @@ -0,0 +1,58 @@ +from typing import List +from models.interfaces.filmeRepoInterface import FilmeRepoInterface +from models.filmeModel import FilmeModel, FilmeResponse + +class FilmeRepoMySQL(FilmeRepoInterface): + def __init__(self, conexao_mysql): + """ + Construtor da classe FilmeRepoMySQL + + Args: + conexao_mysql (conexao mysql): Conexao com o banco de dados mysql + """ + self.conexao = conexao_mysql + + def get_filmes(self) -> List[FilmeResponse]: + """ + Retorna todos os filmes + """ + cursor = self.conexao.cursor() + cursor.execute("SELECT * FROM filmes") + rows = cursor.fetchall() + filmes = [] + for row in rows: + filmes.append(FilmeResponse( + id=row[0], + titulo=row[1], + diretor=row[2], + ano_lancamento=row[3], + genero=row[4] + )) + return filmes + + def post_filme(self, filme: FilmeModel) -> FilmeModel: + """ + Cria um novo filme + """ + cursor = self.conexao.cursor() + cursor.execute("INSERT INTO filmes (titulo, diretor, ano_lancamento, genero) VALUES (%s, %s, %s, %s)", + (filme.titulo, filme.diretor, filme.ano_lancamento, filme.genero)) + self.conexao.commit() + return filme + + def get_filme_by_id(self, id: int) -> FilmeResponse: + """ + Retorna um filme pelo seu id + """ + cursor = self.conexao.cursor() + cursor.execute("SELECT * FROM filmes WHERE id = %s", (id,)) + row = cursor.fetchone() + if row is None: + return None + return FilmeResponse( + id=row[0], + titulo=row[1], + diretor=row[2], + ano_lancamento=row[3], + genero=row[4] + ) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000..ff9b5451 Binary files /dev/null and b/requirements.txt differ diff --git a/services/connectionService.py b/services/connectionService.py new file mode 100644 index 00000000..d2bbe63b --- /dev/null +++ b/services/connectionService.py @@ -0,0 +1,17 @@ +import os +from utils.db import mySqlDb +from models.repos.filmeRepoMySQL import FilmeRepoMySQL + + +def determinar_conexao(): + """ + Determina qual conexão a ser utilizada com base no valor da variável de ambiente DB_TYPE. + + Returns: + FilmeRepoInterface: Repositório de filmes + """ + if os.getenv('DB_TYPE') == 'mysql': + connectionMySql = mySqlDb.criar_conexao_mySql() + repo = FilmeRepoMySQL(connectionMySql) + + return repo \ No newline at end of file diff --git a/services/mySqlDbService.py b/services/mySqlDbService.py new file mode 100644 index 00000000..499fdc3f --- /dev/null +++ b/services/mySqlDbService.py @@ -0,0 +1,28 @@ +class mySqlDbService: + def __init__(self, connection): + """ + Construtor da classe mySqlDbService. + + Args: + connection (conexao mysql): Conexao com o banco de dados mysql + """ + self.connection = connection + + def criar_banco(self, nome_banco): + + """ + Cria um banco de dados no MySQL se ele não existir. + + Esta função utiliza a conexão MySQL fornecida para criar um banco de dados + com o nome especificado. Se o banco de dados já existir, ele não será recriado. + + Args: + nome_banco (str): O nome do banco de dados a ser criado. + """ + + cursor = self.connection.cursor() + cursor.execute(f"CREATE DATABASE IF NOT EXISTS {nome_banco}") + cursor.execute(f"USE {nome_banco}") + cursor.execute("CREATE TABLE IF NOT EXISTS filmes (id INT AUTO_INCREMENT PRIMARY KEY, titulo VARCHAR(255), diretor VARCHAR(255), ano_lancamento INT, genero VARCHAR(255))") + self.connection.commit() + print(f"Banco de dados '{nome_banco}' criado ou já existente.") \ No newline at end of file diff --git a/utils/db/mySqlDb.py b/utils/db/mySqlDb.py new file mode 100644 index 00000000..9279c7d2 --- /dev/null +++ b/utils/db/mySqlDb.py @@ -0,0 +1,37 @@ +import mysql.connector +import os +from services.mySqlDbService import mySqlDbService + +def criar_conexao_mySql(): + """ + Cria uma conex o com o banco de dados mySql. + + Esta fun o cria uma conex o com o banco de dados mySql + utilizando a biblioteca mysql-connector-python. Ela + verifica se a conex o foi estabelecida com sucesso e + retorna a conex o criada. + + Returns: + connection: Uma conex o com o banco de dados mySql + Raises: + Error: Se houver um erro ao conectar com o banco de dados + """ + try: + connection = mysql.connector.connect( + host='mysqldb', + port=3306, + user=os.getenv('MYSQL_USER'), + password=os.getenv('MYSQL_PASSWORD') + ) + if connection.is_connected(): + print("Conexão com o banco de dados mySql estabelecida com sucesso!") + + dbService = mySqlDbService(connection) + + dbService.criar_banco(os.getenv('MYSQL_DATABASE')) + connection.database = os.getenv('MYSQL_DATABASE') + + return connection + except Exception as e: + print(f"Erro ao conectar com o banco de dados: {e}") + raise e \ No newline at end of file