Commit c53d72a9 authored by Pedro Carlucci's avatar Pedro Carlucci Committed by Arthur Sudbrack Ibarra
Browse files

US03-data-model-plus-crud

parent 045c0d14
Showing with 223 additions and 159 deletions
+223 -159
......@@ -19,6 +19,11 @@ variables:
# This is the prefix that will be used to tag the application image.
IMAGE_PREFIX: backend
# Set the environment variables for the application.
# DB_ENVIRONMENT is set to test to run the tests in the pipeline.
DB_ENVIRONMENT: test
# Stages.
# The stages are used to organize the jobs.
# They run in the order they are defined.
......@@ -52,6 +57,12 @@ test:
- if: '$CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "develop" || $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "main"'
when: on_success
after_script:
# If not running on a Merge Request, exit the job.
- |
if [ -z "$CI_MERGE_REQUEST_ID" ]; then
echo "The job is not running on a Merge Request. Skipping..."
exit 0
fi
# Download curl.
- echo "[Downloading curl]"
- apt update && apt install -y curl
......@@ -62,10 +73,6 @@ test:
- |
NEW_LINE=$'\n'
JOB_URL="$CI_PROJECT_URL/-/jobs/$CI_JOB_ID"
if [ -z "$CI_MERGE_REQUEST_ID" ]; then
echo "The job is not running on a Merge Request. Skipping..."
exit 0
fi
if [ "$CI_JOB_STATUS" == "success" ]; then
BOT_MESSAGE="### :heavy_check_mark: Os Testes Passaram!${NEW_LINE}E tu não fez mais que a tua obrigação, $CI_COMMIT_AUTHOR... Veja os [logs do job]($JOB_URL) para mais detalhes. O Merge Request agora está liberado para ser mergeado em \`$CI_MERGE_REQUEST_TARGET_BRANCH_NAME\`."
else
......
# from fastapi import HTTPException
from app.api.services.veiculo_service import VeiculoService
from app.api.models.veiculo import Veiculo
# Controller class.
#
# This class is responsible for handling the requests and responses.
# It will call the service layer to get the data and return the response.
# It will also handle any exceptions that are raised by the service layer.
class VeiculoController:
def __init__(self, service: VeiculoService):
self._service = service
def get_all(self) -> list[Veiculo]:
return self._service.get_all()
def get_by_sigla(self, sigla: str) -> Veiculo:
return self._service.get_by_sigla(sigla)
def create(self, veiculo_data: Veiculo) -> None:
self._service.create(veiculo_data)
def update(self, sigla: int, veiculo_data: Veiculo) -> None:
self._service.update(sigla, veiculo_data)
def delete(self, sigla: int) -> None:
self._service.delete(sigla)
from pydantic import BaseModel
from typing import List
# Most fields are optional because there will be times when it will not
# be possible to get all the information. It will depend on how the pdf
# extraction is done.
class Combustivel(BaseModel):
potencia: str
tipo_combustivel: str
potencia: str = ""
tipo_combustivel: str = ""
class Motor(BaseModel):
cilindradas: str
nro_cilindradas: str
combustiveis: list[Combustivel]
cilindradas: str = ""
nro_cilindradas: str = ""
combustiveis: List[Combustivel] = []
class Veiculo(BaseModel):
desc_cat: str
renavam_desc: str
sigla: str
pacote_def_modelo: str
versao: str
ano: str
marca: str
linha: str
motor: Motor
carga: str
num_passag: str
num_portas: str
num_renavam: str
status: str
pdf_names: list[str]
desc_cat: str = ""
renavam_desc: str = ""
sigla: str # Required.
pacote_def_modelo: str = ""
versao: str = ""
ano: str = ""
marca: str = ""
linha: str = ""
motor: Motor = Motor()
carga: str = ""
num_passag: str = ""
num_portas: str = ""
num_renavam: str = ""
status: str = "PENDENTE"
pdf_names: List[str] = []
from bson import ObjectId
from app.api.models.veiculo import Veiculo
from pymongo.database import Database
from pymongo.results import UpdateResult, DeleteResult
from pymongo.results import UpdateResult, DeleteResult, InsertOneResult
VEICULO_COLLECTION = "Veiculos"
# CarRepository class
#
# This class is responsible for accessing the database to get the cars.
# It will be called by the service layer to get the data.
# It will also handle any exceptions that are raised by the database.
VEICULO_COLLECTION = "Veiculos"
class VeiculoRepository:
def __init__(self, database: Database):
self._collection = database[VEICULO_COLLECTION]
......@@ -25,15 +26,17 @@ class VeiculoRepository:
def get_by_sigla(self, sigla: str) -> Veiculo:
veiculo_dict = self._collection.find_one({"sigla": sigla})
return Veiculo.parse_obj(veiculo_dict)
def create(self, veiculo_data: Veiculo):
def create(self, veiculo_data: Veiculo) -> InsertOneResult:
# On create we need to convert car_data to dict and then insert it into the database.
# Ex: db.insert_one(car_data.dict())
self._collection.insert_one(veiculo_data.dict())
return self._collection.insert_one(veiculo_data.dict())
def update(self, sigla: int, veiculo_data: Veiculo) -> UpdateResult:
def update(self, sigla: str, veiculo_data: Veiculo) -> UpdateResult:
return self._collection.update_one({"sigla": sigla}, {"$set": veiculo_data.dict()})
def delete(self, sigla: int) -> DeleteResult:
def delete(self, sigla: str) -> DeleteResult:
return self._collection.delete_one({"sigla": sigla})
def find_by_id(self, id: ObjectId):
return Veiculo.parse_obj(self._collection.find_one({"_id": id}))
from fastapi import APIRouter, HTTPException
from pydantic import ValidationError
from fastapi import FastAPI, File, UploadFile
from fastapi import APIRouter, File, UploadFile
from app.api.models.veiculo import Veiculo
from app.database.mongo import get_database
from app.api.repositories.veiculo_repository import VeiculoRepository
from app.api.services.veiculo_service import VeiculoService
from app.api.controllers.veiculo_controller import VeiculoController
# Car router.
#
......@@ -17,45 +13,47 @@ from app.api.controllers.veiculo_controller import VeiculoController
# They are not meant to be used outside of this file.
_database = get_database()
_repository = VeiculoRepository(_database)
_service = VeiculoService(_repository)
_veiculo_controller = VeiculoController(_service)
_veiculo_service = VeiculoService(_repository)
_veiculo_router = APIRouter(prefix="/veiculos")
@_veiculo_router.get("/")
def get_veiculos() -> list[Veiculo]:
return _veiculo_controller.get_all()
return _veiculo_service.get_all()
@_veiculo_router.get("/{sigla}")
def get_veiculo(sigla: str) -> Veiculo:
return _veiculo_controller.get_by_sigla(sigla)
return _veiculo_service.get_by_sigla(sigla)
@_veiculo_router.post("/")
def create_veiculo(veiculo_data: Veiculo) -> None:
_veiculo_controller.create(veiculo_data)
def create_veiculo(veiculo_data: Veiculo) -> Veiculo:
return _veiculo_service.create(veiculo_data)
@_veiculo_router.put("/{sigla}")
def update_veiculo(sigla: str, veiculo_data: Veiculo) -> None:
_veiculo_controller.update(sigla, veiculo_data)
def update_veiculo(sigla: str, veiculo_data: Veiculo) -> Veiculo:
return _veiculo_service.update(sigla, veiculo_data)
@_veiculo_router.delete("/{sigla}")
def delete_veiculo(sigla: str) -> None:
_veiculo_controller.delete(sigla)
def delete_veiculo(sigla: str) -> str:
return _veiculo_service.delete(sigla)
# It contains a single endpoint that receives the PDF file.
@_veiculo_router.post("/pdf/")
@_veiculo_router.post("/upload/pdf")
def create_upload_file(form_data: UploadFile = File(...)):
contents = form_data.file.read() # This function read the pdf bytes.
contents # Here we have the pdf bytes saved in the application memory. The ideia is to call a funtion which will handle the pdf bytes and extract them.
contents = form_data.file.read() # This function reads the pdf bytes.
# Here we have the pdf bytes saved in the application memory.
# The ideia is to call a funtion which will handle the pdf bytes and extract them.
# This is the file name in memory. It will be used to save the veiculo JSON in the database.
name = form_data.filename
name : str
name = form_data.filename # This is the file name in memory. It will be used to save the veiculo JSON in the database.
return {"filename": name}
return {"filename": form_data.filename}
# This function is used to get the car router.
# It is used in the main.py file to include the router in the FastAPI app.
......
......@@ -2,36 +2,40 @@ from fastapi import HTTPException
from app.api.repositories.veiculo_repository import VeiculoRepository
from app.api.models.veiculo import Veiculo
# Service class.
#
# This class is responsible for handling the business logic.
# It will call the repository layer to get the data and return the response.
# It will also handle any exceptions that are raised by the repository layer.
class VeiculoService:
def __init__(self, repository: VeiculoRepository):
self._repository = repository
def get_all(self) -> list[Veiculo]:
# Call the repository to get the cars.
return self._repository.get_all()
def get_by_sigla(self, sigla: str) -> Veiculo:
try:
try:
return self._repository.get_by_sigla(sigla)
except Exception as e:
raise HTTPException(status_code=404, detail="Veiculo nao encontrado")
def create(self, veiculo_data: Veiculo) -> None:
self._repository.create(veiculo_data)
def update(self, sigla: int, veiculo_data: Veiculo) -> None:
response = self._repository.update(sigla, veiculo_data)
if response.modified_count == 0:
raise HTTPException(status_code=400, detail="Nenhum dado encontrado ou modificado")
def delete(self, sigla: int) -> None:
response = self._repository.delete(sigla)
if response.deleted_count == 0:
raise HTTPException(status_code=400, detail="Dado nao encontrado para deletar")
except Exception:
raise HTTPException(
status_code=404, detail="Veiculo nao encontrado.")
def create(self, veiculo_data: Veiculo) -> Veiculo:
result = self._repository.create(veiculo_data)
return self._repository.find_by_id(result.inserted_id)
def update(self, sigla: str, veiculo_data: Veiculo) -> Veiculo:
result = self._repository.update(sigla, veiculo_data)
if result.modified_count == 0:
raise HTTPException(
status_code=400, detail="Nenhum dado encontrado ou modificado.")
return self._repository.get_by_sigla(sigla)
def delete(self, sigla: str) -> str:
result = self._repository.delete(sigla)
if result.deleted_count == 0:
raise HTTPException(
status_code=400, detail="Dado nao encontrado para deletar.")
return sigla
from os import getenv
from pydantic import BaseSettings
class Settings(BaseSettings):
# Api configs.
API_TITLE = "VEICULOS_VIA_MONTADORA"
API_HOST = "0.0.0.0"
API_PORT = 8000
# DB configs.
DB_HOST = getenv(
"MONGODB_HOST", "mongodb://mongo_user:mongo_password@localhost:27017")
DB_NAME = "veiculos-via-montadora"
# DB ENV for runing tests.
# Set the 'DB_ENVIRONMENT' environment variable to 'test' when running tests.
DB_ENVIRONMENT = "prod"
settings = Settings()
from pymongo import MongoClient
from pymongo.database import Database
from mongomock import MongoClient as MockMongoClient
from os import getenv
from app.config import settings
# The variable MONGODB_HOST is set dynamically.
#
......@@ -13,16 +14,6 @@ from os import getenv
# value will be used.
#
# Define the environment variable in Dockerfile / docker-compose.yml
MONGODB_HOST = getenv(
"MONGODB_HOST", "mongodb://mongo_user:mongo_password@localhost:27017")
DATABASE_NAME = "veiculos-via-montadora"
# Connect to MongoDB.
_client = MongoClient(MONGODB_HOST)
# Access database.
_database = _client[DATABASE_NAME]
# This function returns the database.
......@@ -32,15 +23,23 @@ _database = _client[DATABASE_NAME]
# If the MOCK_DATABASE environment variable is set to True,
# a mocked database will be returned instead of the real database.
def get_database() -> Database:
is_test = getenv("MOCK_DATABASE", "false")
if str.lower(is_test) == "true":
if str.lower(settings.DB_ENVIRONMENT) == "test":
return get_mock_database()
return get_real_database()
# This function returns the real database.
def get_real_database() -> Database:
# Connect to MongoDB.
_client = MongoClient(settings.DB_HOST)
# Access database.
_database = _client[settings.DB_NAME]
return _database
# This function returns a mock database.
# It is used for testing.
def get_mock_database():
def get_mock_database() -> Database:
_client = MockMongoClient()
_database = _client[DATABASE_NAME]
_database = _client[settings.DB_NAME]
return _database
......@@ -3,25 +3,35 @@ from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from mangum import Mangum
from app.api.routers.veiculo_router import get_veiculo_router
from app.config import settings
# Initialize the FastAPI app.
app = FastAPI()
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_methods=["OPTIONS", "GET", "POST", "PUT", "DELETE"],
# Headers should be pdf or json, but more testing is required
# "Content-Type", "application/pdf", "application/json"
allow_headers=["*"]
)
# Function to initialize the FastAPI app.
def init_app() -> FastAPI:
app = FastAPI(title=settings.API_TITLE)
# Include the car router.
app.include_router(get_veiculo_router())
# Include the CORS middleware.
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_methods=["OPTIONS", "GET", "POST", "PUT", "DELETE"],
# Headers should be pdf or json, but more testing is required
# "Content-Type", "???", "application/json"
allow_headers=["*"]
)
return app
# Create the FastAPI app.
app = init_app()
# Create a handler for AWS Lambda.
handler = Mangum(app)
# Include the car router.
app.include_router(get_veiculo_router())
# Start the server.
if __name__ == "__main__":
uvicorn.run(app, host="0.0.0.0", port=8000)
uvicorn.run(app,
host=settings.API_HOST,
reload=True,
port=settings.API_PORT)
from app.api.models.veiculo import Combustivel, Motor, Veiculo
# This function will return a Veiculo instance with mocked data.
def mock_veiculo_with_default_params() -> Veiculo:
return Veiculo(
desc_cat="desc",
renavam_desc="renavam",
sigla="1234Test",
pacote_def_modelo="pacote",
versao="versao",
ano="ano",
marca="marca",
linha="linha",
motor=Motor(
cilindradas="cilindradas",
nro_cilindradas="nro_cilindradas",
combustiveis=[Combustivel(
potencia="potencia", tipo_combustivel="tipo_combustivel")]
),
carga="carga",
num_passag="num_passag",
num_portas="num_portas",
num_renavam="num_renavam",
status="status",
pdf_names=["pdf_names"]
)
import pytest
from os import environ
from fastapi.testclient import TestClient
from app.api.models.veiculo import Veiculo
from fastapi import status
from app.main import app
from app.test.mockers.veiculo_mocker import mock_veiculo_with_default_params
# This fixture will be executed before each test.
......@@ -10,28 +10,47 @@ from app.main import app
# The test function will be able to send requests to the application.
@pytest.fixture(scope="module")
def test_app():
# Set the MOCK_DATABASE environment variable to True.
# This will make the get_database() function return a mocked database.
# THIS ENV APPROACH IS CURRENTLY NOT WORKING...
environ["MOCK_DATABASE"] = "true"
yield TestClient(app)
# Remove later.
# Test names MUST start with "test_".
def test_dummy(test_app: TestClient):
assert True
# def test_create_car(test_app: TestClient):
# # Send a POST request to the /cars endpoint.
# # The request body represents a car object.
# veiculo_data = Veiculo(
# id="1",
# name="Fiat Uno",
# description="Small and cheap car."
# )
# response = test_app.post("/veiculos", json=veiculo_data.dict())
# # Assert that the response status code is 200, for example.
# assert response.status_code == 200
# # Assert more things as needed...
yield TestClient(app=app)
# Veiculo Mock data.
veiculo_data = mock_veiculo_with_default_params()
# Test cases.
def test_create_car(test_app: TestClient):
response = test_app.post("/veiculos", json=veiculo_data.dict())
assert response.status_code == status.HTTP_200_OK
def test_get_all_veiculos(test_app: TestClient):
response = test_app.get("/veiculos")
assert response.status_code == status.HTTP_200_OK
assert len(response.json()) > 0
def test_get_veiculo_by_sigla(test_app: TestClient):
sigla = veiculo_data.sigla
response = test_app.get(f"/veiculos/{sigla}")
assert response.status_code == status.HTTP_200_OK
assert response.json()["sigla"] == sigla
def test_update_veiculo_by_sigla(test_app: TestClient):
veiculo_data.ano = "2023"
sigla = veiculo_data.sigla
response = test_app.put(f"/veiculos/{sigla}", json=veiculo_data.dict())
assert response.status_code == status.HTTP_200_OK
def test_delete_veiculo_by_sigla(test_app: TestClient):
sigla = veiculo_data.sigla
response = test_app.delete(f"/veiculos/{sigla}")
assert response.status_code == status.HTTP_200_OK
assert response.json() == sigla
def test_get_veiculo_by_sigla_not_found(test_app: TestClient):
sigla = veiculo_data.sigla
response = test_app.get(f"/veiculos/{sigla}")
assert response.status_code == status.HTTP_404_NOT_FOUND
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment