From 86dd9fd76c5de940e3e08f4028b03bd19d17d59c Mon Sep 17 00:00:00 2001 From: Gabriel Barberini Date: Sun, 9 Feb 2025 13:39:42 -0300 Subject: [PATCH 01/34] Introduce interfaces --- lib/api.py | 17 +- lib/controllers/environment.py | 259 ++---------- lib/controllers/flight.py | 368 ++---------------- lib/controllers/interface.py | 124 ++++++ lib/controllers/motor.py | 274 ++----------- lib/controllers/rocket.py | 274 ++----------- lib/models/environment.py | 37 +- lib/models/flight.py | 48 ++- lib/models/interface.py | 51 +++ lib/models/motor.py | 120 +++--- lib/models/rocket.py | 49 ++- lib/models/{ => sub}/aerosurfaces.py | 0 lib/models/sub/tanks.py | 70 ++++ lib/repositories/environment.py | 154 ++------ lib/repositories/flight.py | 220 +---------- lib/repositories/{repo.py => interface.py} | 98 ++++- lib/repositories/motor.py | 156 +------- lib/repositories/rocket.py | 161 +------- lib/routes/environment.py | 95 ++--- lib/routes/flight.py | 96 ++--- lib/routes/motor.py | 64 +-- lib/routes/rocket.py | 68 ++-- lib/services/rocket.py | 2 +- lib/views/environment.py | 25 +- lib/views/flight.py | 27 +- lib/views/interface.py | 5 + lib/views/motor.py | 29 +- lib/views/rocket.py | 31 +- .../test_controller_interface.py | 225 +++++++++++ .../test_repository_interface.py | 286 ++++++++++++++ 30 files changed, 1449 insertions(+), 1984 deletions(-) create mode 100644 lib/controllers/interface.py create mode 100644 lib/models/interface.py rename lib/models/{ => sub}/aerosurfaces.py (100%) create mode 100644 lib/models/sub/tanks.py rename lib/repositories/{repo.py => interface.py} (59%) create mode 100644 lib/views/interface.py create mode 100644 tests/test_controllers/test_controller_interface.py create mode 100644 tests/test_repositories/test_repository_interface.py diff --git a/lib/api.py b/lib/api.py index 91afdb6..2ee22bf 100644 --- a/lib/api.py +++ b/lib/api.py @@ -1,10 +1,5 @@ -""" -This is the main API file for the RocketPy API. -""" - from fastapi import FastAPI, Request, status from fastapi.exceptions import RequestValidationError -from fastapi.middleware.cors import CORSMiddleware from fastapi.openapi.utils import get_openapi from fastapi.responses import RedirectResponse, JSONResponse @@ -21,14 +16,6 @@ "syntaxHighlight.theme": "obsidian", } ) - -app.add_middleware( - CORSMiddleware, - allow_origins=["*"], - allow_credentials=True, - allow_methods=["*"], - allow_headers=["*"], -) app.include_router(flight.router) app.include_router(environment.router) app.include_router(motor.router) @@ -46,7 +33,7 @@ def custom_openapi(): return app.openapi_schema openapi_schema = get_openapi( title="RocketPy Infinity-API", - version="2.2.0", + version="3.0.0", description=( "

RocketPy Infinity-API is a RESTful Open API for RocketPy, a rocket flight simulator.

" "
" @@ -87,7 +74,7 @@ async def __perform_healthcheck(): return {"health": "Everything OK!"} -# Errors +# Global exception handler @app.exception_handler(RequestValidationError) async def validation_exception_handler( request: Request, exc: RequestValidationError diff --git a/lib/controllers/environment.py b/lib/controllers/environment.py index 2da51c2..7169d53 100644 --- a/lib/controllers/environment.py +++ b/lib/controllers/environment.py @@ -1,123 +1,29 @@ -from typing import Union - -from fastapi import HTTPException, status -from pymongo.errors import PyMongoError - -from lib import logger, parse_error -from lib.models.environment import Env -from lib.services.environment import EnvironmentService -from lib.repositories.environment import EnvRepository -from lib.views.environment import ( - EnvSummary, - EnvCreated, - EnvDeleted, - EnvUpdated, +from lib.controllers.interface import ( + ControllerInterface, + controller_exception_handler, ) +from lib.views.environment import EnvSummary +from lib.models.environment import EnvironmentModel +from lib.services.environment import EnvironmentService -class EnvController: +class EnvironmentController(ControllerInterface): """ Controller for the Environment model. Enables: - - Simulation of a RocketPy Environment from models.Env - - CRUD operations over models.Env on the database + - Simulation of a RocketPy Environment. + - CRUD for Environment BaseApiModel. """ - @staticmethod - async def create_env(env: Env) -> Union[EnvCreated, HTTPException]: - """ - Create a env in the database. + def __init__(self): + super().__init__(models=[EnvironmentModel]) - Returns: - views.EnvCreated - """ - env_repo = None - try: - async with EnvRepository(env) as env_repo: - await env_repo.create_env() - except PyMongoError as e: - logger.error( - f"controllers.environment.create_env: PyMongoError {e}" - ) - raise HTTPException( - status_code=status.HTTP_503_SERVICE_UNAVAILABLE, - detail="Failed to create environment in db", - ) from e - except HTTPException as e: - raise e from e - except Exception as e: - exc_str = parse_error(e) - logger.error(f"controllers.environment.create_env: {exc_str}") - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail=f"Failed to create environment: {exc_str}", - ) from e - else: - return EnvCreated(env_id=env_repo.env_id) - finally: - env_id = ( - getattr(env_repo, 'env_id', 'unknown') - if env_repo - else 'unknown' - ) - if env_repo: - logger.info( - f"Call to controllers.environment.create_env completed for Env {env_id}" - ) - - @staticmethod - async def get_env_by_id(env_id: str) -> Union[Env, HTTPException]: - """ - Get a env from the database. - - Args: - env_id: str - - Returns: - models.Env - - Raises: - HTTP 404 Not Found: If the env is not found in the database. - """ - try: - async with EnvRepository() as env_repo: - await env_repo.get_env_by_id(env_id) - env = env_repo.env - except PyMongoError as e: - logger.error( - f"controllers.environment.get_env_by_id: PyMongoError {e}" - ) - raise HTTPException( - status_code=status.HTTP_503_SERVICE_UNAVAILABLE, - detail="Failed to read environment from db", - ) from e - except HTTPException as e: - raise e from e - except Exception as e: - exc_str = parse_error(e) - logger.error(f"controllers.environment.get_env_by_id: {exc_str}") - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail=f"Failed to read environment: {exc_str}", - ) from e - else: - if env: - return env - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail="Environment not found", - ) - finally: - logger.info( - f"Call to controllers.environment.get_env_by_id completed for Env {env_id}" - ) - - @classmethod + @controller_exception_handler async def get_rocketpy_env_binary( - cls, + self, env_id: str, - ) -> Union[bytes, HTTPException]: + ) -> bytes: """ Get rocketpy.Environmnet dill binary. @@ -130,117 +36,14 @@ async def get_rocketpy_env_binary( Raises: HTTP 404 Not Found: If the env is not found in the database. """ - try: - env = await cls.get_env_by_id(env_id) - env_service = EnvironmentService.from_env_model(env) - except HTTPException as e: - raise e from e - except Exception as e: - exc_str = parse_error(e) - logger.error( - f"controllers.environment.get_rocketpy_env_as_binary: {exc_str}" - ) - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail=f"Failed to read environment: {exc_str}", - ) from e - else: - return env_service.get_env_binary() - finally: - logger.info( - f"Call to controllers.environment.get_rocketpy_env_binary completed for Env {env_id}" - ) - - @staticmethod - async def update_env_by_id( - env_id: str, env: Env - ) -> Union[EnvUpdated, HTTPException]: - """ - Update a models.Env in the database. - - Args: - env_id: str - - Returns: - views.EnvUpdated - - Raises: - HTTP 404 Not Found: If the env is not found in the database. - """ - try: - async with EnvRepository(env) as env_repo: - await env_repo.update_env_by_id(env_id) - except PyMongoError as e: - logger.error( - f"controllers.environment.update_env: PyMongoError {e}" - ) - raise HTTPException( - status_code=status.HTTP_503_SERVICE_UNAVAILABLE, - detail="Failed to update environment from db", - ) from e - except HTTPException as e: - raise e from e - except Exception as e: - exc_str = parse_error(e) - logger.error(f"controllers.environment.update_env: {exc_str}") - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail=f"Failed to update environment: {exc_str}", - ) from e - else: - return EnvUpdated(env_id=env_id) - finally: - logger.info( - f"Call to controllers.environment.update_env completed for Env {env_id}" - ) - - @staticmethod - async def delete_env_by_id( - env_id: str, - ) -> Union[EnvDeleted, HTTPException]: - """ - Delete a models.Env from the database. - - Args: - env_id: str - - Returns: - views.EnvDeleted - - Raises: - HTTP 404 Not Found: If the env is not found in the database. - """ - try: - async with EnvRepository() as env_repo: - await env_repo.delete_env_by_id(env_id) - except PyMongoError as e: - logger.error( - f"controllers.environment.delete_env: PyMongoError {e}" - ) - raise HTTPException( - status_code=status.HTTP_503_SERVICE_UNAVAILABLE, - detail="Failed to delete environment from db", - ) from e - except HTTPException as e: - raise e from e - except Exception as e: - exc_str = parse_error(e) - logger.error(f"controllers.environment.delete_env: {exc_str}") - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail=f"Failed to delete environment: {exc_str}", - ) from e - else: - return EnvDeleted(env_id=env_id) - finally: - logger.info( - f"Call to controllers.environment.delete_env completed for Env {env_id}" - ) + env = await self.get_env_by_id(env_id) + env_service = EnvironmentService.from_env_model(env) + return env_service.get_env_binary() - @classmethod + @controller_exception_handler async def simulate_env( - cls, env_id: str - ) -> Union[EnvSummary, HTTPException]: + self, env_id: str + ) -> EnvSummary: """ Simulate a rocket environment. @@ -253,22 +56,6 @@ async def simulate_env( Raises: HTTP 404 Not Found: If the env does not exist in the database. """ - try: - env = await cls.get_env_by_id(env_id) - env_service = EnvironmentService.from_env_model(env) - env_summary = env_service.get_env_summary() - except HTTPException as e: - raise e from e - except Exception as e: - exc_str = parse_error(e) - logger.error(f"controllers.environment.simulate: {exc_str}") - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail=f"Failed to simulate environment, parameters may contain data that is not physically coherent: {exc_str}", - ) from e - else: - return env_summary - finally: - logger.info( - f"Call to controllers.environment.simulate completed for Env {env_id}" - ) + env = await self.get_env_by_id(env_id) + env_service = EnvironmentService.from_env_model(env) + return env_service.get_env_summary() diff --git a/lib/controllers/flight.py b/lib/controllers/flight.py index 42fe456..0612eb5 100644 --- a/lib/controllers/flight.py +++ b/lib/controllers/flight.py @@ -1,232 +1,30 @@ -from typing import Union -from fastapi import HTTPException, status -from pymongo.errors import PyMongoError - - -from lib import logger, parse_error -from lib.controllers.rocket import RocketController -from lib.models.environment import Env -from lib.models.rocket import Rocket -from lib.models.flight import Flight -from lib.views.motor import MotorView -from lib.views.rocket import RocketView -from lib.views.flight import ( - FlightSummary, - FlightCreated, - FlightUpdated, - FlightDeleted, - FlightView, +from lib.controllers.interface import ( + ControllerInterface, + controller_exception_handler, ) -from lib.repositories.flight import FlightRepository +from lib.views.flight import FlightSummary, FlightUpdated +from lib.models.flight import FlightModel +from lib.models.environment import EnvironmentModel +from lib.models.rocket import RocketModel from lib.services.flight import FlightService -class FlightController: +class FlightController(ControllerInterface): """ Controller for the Flight model. Enables: - - Create a RocketPyFlight object from a Flight model object. - - Generate trajectory simulation from a RocketPyFlight object. - - Create both Flight model and RocketPyFlight objects in the database. - - Update both Flight model and RocketPyFlight objects in the database. - - Delete both Flight model and RocketPyFlight objects from the database. - - Read a Flight model from the database. - - Read a RocketPyFlight object from the database. - + - Simulation of a RocketPy Flight. + - CRUD for Flight BaseApiModel. """ - @staticmethod - def guard(flight: Flight): - RocketController.guard(flight.rocket) - - @classmethod - async def create_flight( - cls, flight: Flight - ) -> Union[FlightCreated, HTTPException]: - """ - Create a flight in the database. - - Returns: - views.FlightCreated - """ - flight_repo = None - try: - cls.guard(flight) - async with FlightRepository(flight) as flight_repo: - await flight_repo.create_flight() - except PyMongoError as e: - logger.error(f"controllers.flight.create_flight: PyMongoError {e}") - raise HTTPException( - status_code=status.HTTP_503_SERVICE_UNAVAILABLE, - detail="Failed to create flight in db", - ) from e - except HTTPException as e: - raise e from e - except Exception as e: - exc_str = parse_error(e) - logger.error(f"controllers.flight.create_flight: {exc_str}") - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail=f"Failed to create flight: {exc_str}", - ) from e - else: - return FlightCreated(flight_id=flight_repo.flight_id) - finally: - flight_id = ( - getattr(flight_repo, 'flight_id', 'unknown') - if flight_repo - else 'unknown' - ) - logger.info( - f"Call to controllers.flight.create_flight completed for Flight {flight_id}" - ) - - @staticmethod - async def get_flight_by_id( - flight_id: str, - ) -> Union[FlightView, HTTPException]: - """ - Get a flight from the database. - - Args: - flight_id: str - - Returns: - models.Flight - - Raises: - HTTP 404 Not Found: If the flight is not found in the database. - """ - try: - async with FlightRepository() as flight_repo: - await flight_repo.get_flight_by_id(flight_id) - flight = flight_repo.flight - except PyMongoError as e: - logger.error( - f"controllers.flight.get_flight_by_id: PyMongoError {e}" - ) - raise HTTPException( - status_code=status.HTTP_503_SERVICE_UNAVAILABLE, - detail="Failed to read flight from db", - ) from e - except HTTPException as e: - raise e from e - except Exception as e: - exc_str = parse_error(e) - logger.error(f"controllers.flight.get_flight_by_id: {exc_str}") - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail=f"Failed to read flight: {exc_str}", - ) from e - else: - if flight: - motor_view = MotorView( - **flight.rocket.motor.dict(), - selected_motor_kind=flight.rocket.motor.motor_kind, - ) - updated_rocket = flight.rocket.dict() - updated_rocket.update(motor=motor_view) - rocket_view = RocketView( - **updated_rocket, - ) - updated_flight = flight.dict() - updated_flight.update(rocket=rocket_view) - flight_view = FlightView(**updated_flight) - return flight_view - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail="Flight not found.", - ) - finally: - logger.info( - f"Call to controllers.flight.get_flight_by_id completed for Flight {flight_id}" - ) - - @classmethod - async def get_rocketpy_flight_binary( - cls, - flight_id: str, - ) -> Union[bytes, HTTPException]: - """ - Get rocketpy.flight as dill binary. - - Args: - flight_id: str + def __init__(self): + super().__init__(models=[FlightModel]) - Returns: - bytes - - Raises: - HTTP 404 Not Found: If the flight is not found in the database. - """ - try: - flight = await cls.get_flight_by_id(flight_id) - flight_service = FlightService.from_flight_model(flight) - except HTTPException as e: - raise e from e - except Exception as e: - exc_str = parse_error(e) - logger.error( - f"controllers.flight.get_rocketpy_flight_binary: {exc_str}" - ) - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail=f"Failed to read flight: {exc_str}", - ) from e - else: - return flight_service.get_flight_binary() - finally: - logger.info( - f"Call to controllers.flight.get_rocketpy_flight_binary completed for Flight {flight_id}" - ) - - @classmethod - async def update_flight_by_id( - cls, flight_id: str, flight: Flight - ) -> Union[FlightUpdated, HTTPException]: - """ - Update a models.Flight in the database. - - Args: - flight_id: str - - Returns: - views.FlightUpdated - - Raises: - HTTP 404 Not Found: If the flight is not found in the database. - """ - try: - cls.guard(flight) - async with FlightRepository(flight) as flight_repo: - await flight_repo.update_flight_by_id(flight_id) - except PyMongoError as e: - logger.error(f"controllers.flight.update_flight: PyMongoError {e}") - raise HTTPException( - status_code=status.HTTP_503_SERVICE_UNAVAILABLE, - detail="Failed to update flight in db", - ) from e - except HTTPException as e: - raise e from e - except Exception as e: - exc_str = parse_error(e) - logger.error(f"controllers.flight.update_flight: {exc_str}") - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail=f"Failed to update flight: {exc_str}", - ) from e - else: - return FlightUpdated(flight_id=flight_id) - finally: - logger.info( - f"Call to controllers.flight.update_flight completed for Flight {flight_id}" - ) - - @classmethod + @controller_exception_handler async def update_env_by_flight_id( - cls, flight_id: str, *, env: Env - ) -> Union[FlightUpdated, HTTPException]: + self, flight_id: str, *, env: EnvironmentModel + ) -> FlightUpdated: """ Update a models.Flight.env in the database. @@ -240,39 +38,15 @@ async def update_env_by_flight_id( Raises: HTTP 404 Not Found: If the flight is not found in the database. """ - try: - flight = await cls.get_flight_by_id(flight_id) - flight.environment = env - async with FlightRepository(flight) as flight_repo: - await flight_repo.update_env_by_flight_id(flight_id) - except PyMongoError as e: - logger.error( - f"controllers.flight.update_env_by_flight_id: PyMongoError {e}" - ) - raise HTTPException( - status_code=status.HTTP_503_SERVICE_UNAVAILABLE, - detail="Failed to update environment from db", - ) from e - except HTTPException as e: - raise e from e - except Exception as e: - exc_str = parse_error(e) - logger.error(f"controllers.flight.update_env: {exc_str}") - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail=f"Failed to update environment: {exc_str}", - ) from e - else: - return FlightUpdated(flight_id=flight_id) - finally: - logger.info( - f"Call to controllers.flight.update_env completed for Flight {flight_id}" - ) + flight = await self.get_flight_by_id(flight_id) + flight.environment = env + self.update_flight_by_id(flight_id, flight) + return FlightUpdated(flight_id=flight_id) - @classmethod + @controller_exception_handler async def update_rocket_by_flight_id( - cls, flight_id: str, *, rocket: Rocket - ) -> Union[FlightUpdated, HTTPException]: + self, flight_id: str, *, rocket: RocketModel + ) -> FlightUpdated: """ Update a models.Flight.rocket in the database. @@ -286,81 +60,37 @@ async def update_rocket_by_flight_id( Raises: HTTP 404 Not Found: If the flight is not found in the database. """ - try: - flight = await cls.get_flight_by_id(flight_id) - flight.rocket = rocket - async with FlightRepository(flight) as flight_repo: - await flight_repo.update_rocket_by_flight_id(flight_id) - except PyMongoError as e: - logger.error( - f"controllers.flight.update_rocket_by_flight_id: PyMongoError {e}" - ) - raise HTTPException( - status_code=status.HTTP_503_SERVICE_UNAVAILABLE, - detail="Failed to update rocket from db", - ) from e - except HTTPException as e: - raise e from e - except Exception as e: - exc_str = parse_error(e) - logger.error(f"controllers.flight.update_rocket: {exc_str}") - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail=f"Failed to update rocket: {exc_str}", - ) from e - else: - return FlightUpdated(flight_id=flight_id) - finally: - logger.info( - f"Call to controllers.flight.update_rocket completed for Flight {flight_id}" - ) + flight = await self.get_flight_by_id(flight_id) + flight.rocket = rocket + self.update_flight_by_id(flight_id, flight) + return FlightUpdated(flight_id=flight_id) - @staticmethod - async def delete_flight_by_id( + @controller_exception_handler + async def get_rocketpy_flight_binary( + self, flight_id: str, - ) -> Union[FlightDeleted, HTTPException]: + ) -> bytes: """ - Delete a models.Flight from the database. + Get rocketpy.flight as dill binary. Args: flight_id: str Returns: - views.FlightDeleted + bytes Raises: HTTP 404 Not Found: If the flight is not found in the database. """ - try: - async with FlightRepository() as flight_repo: - await flight_repo.delete_flight_by_id(flight_id) - except PyMongoError as e: - logger.error(f"controllers.flight.delete_flight: PyMongoError {e}") - raise HTTPException( - status_code=status.HTTP_503_SERVICE_UNAVAILABLE, - detail="Failed to delete flight from db", - ) from e - except HTTPException as e: - raise e from e - except Exception as e: - exc_str = parse_error(e) - logger.error(f"controllers.flight.delete_flight: {exc_str}") - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail=f"Failed to delete flight: {exc_str}", - ) from e - else: - return FlightDeleted(flight_id=flight_id) - finally: - logger.info( - f"Call to controllers.flight.delete_flight completed for Flight {flight_id}" - ) + flight = await self.get_flight_by_id(flight_id) + flight_service = FlightService.from_flight_model(flight) + return flight_service.get_flight_binary() - @classmethod + @controller_exception_handler async def simulate_flight( - cls, + self, flight_id: str, - ) -> Union[FlightSummary, HTTPException]: + ) -> FlightSummary: """ Simulate a rocket flight. @@ -373,22 +103,6 @@ async def simulate_flight( Raises: HTTP 404 Not Found: If the flight does not exist in the database. """ - try: - flight = await cls.get_flight_by_id(flight_id=flight_id) - flight_service = FlightService.from_flight_model(flight) - flight_summary = flight_service.get_flight_summary() - except HTTPException as e: - raise e from e - except Exception as e: - exc_str = parse_error(e) - logger.error(f"controllers.flight.simulate_flight: {exc_str}") - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail=f"Failed to simulate flight, parameters may contain data that is not physically coherent: {exc_str}", - ) from e - else: - return flight_summary - finally: - logger.info( - f"Call to controllers.flight.simulate_flight completed for Flight {flight_id}" - ) + flight = await self.get_flight_by_id(flight_id=flight_id) + flight_service = FlightService.from_flight_model(flight) + return flight_service.get_flight_summary() diff --git a/lib/controllers/interface.py b/lib/controllers/interface.py new file mode 100644 index 0000000..acd8105 --- /dev/null +++ b/lib/controllers/interface.py @@ -0,0 +1,124 @@ +import functools +from typing import List +from pymongo.errors import PyMongoError +from fastapi import HTTPException, status + +from lib import logger +from lib.models.interface import ApiBaseModel +from lib.views.interface import ApiBaseView +from lib.repositories.interface import RepositoryInterface + + +def controller_exception_handler(method): + @functools.wraps(method) + async def wrapper(self, *args, **kwargs): + try: + return await method(self, *args, **kwargs) + except PyMongoError: + logger.error(f"{method.__name__}: PyMongoError") + raise HTTPException( + status_code=status.HTTP_503_SERVICE_UNAVAILABLE, + detail="Operation failed, please try again later", + ) + except HTTPException as e: + raise e from e + except Exception as e: + logger.exception(f"{method.__name__}: Unexpected error {e}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Unexpected error", + ) + finally: + logger.info( + f"Call to {method.__name__} completed for {self.__class__.__name__}" + ) + + return wrapper + + +class ControllerInterface: + + def __init__(self, models: List[ApiBaseModel]): + self._initialized_models = {} + self._load_models(models) + + def _load_models(self, models: List[ApiBaseModel]): + for model in models: + self._initialized_models[model.NAME] = model + + for action in model.METHODS: + method_name = ( + f"{action.lower()}_{model.NAME}" + if action == "POST" + else f"{action.lower()}_{model.NAME}_by_id" + ) + method = self._generate_method(action.lower(), model) + setattr(self, method_name, method) + + def _generate_method(self, action: str, model: ApiBaseModel): + async def method(*args, **kwargs): + handler = getattr(self, f"_{action}_model", None) + if handler: + model_repo = RepositoryInterface.get_model_repo(model) + return await handler( # pylint: disable=not-callable + model, model_repo, *args, **kwargs + ) + raise NotImplementedError(f"_{action}_model is not implemented") + + return method + + @controller_exception_handler + async def _post_model( + self, + model: ApiBaseModel, + model_repo: RepositoryInterface, + model_instance: ApiBaseModel, + ) -> ApiBaseView: + async with model_repo() as repo: + inserted_id = await getattr(repo, f'create_{model.NAME}')( + model_instance + ) + return model.CREATED(inserted_id) + + @controller_exception_handler + async def _get_model( + self, + model: ApiBaseModel, + model_repo: RepositoryInterface, + model_id: str, + ) -> ApiBaseView: + async with model_repo() as repo: + model_instance = await getattr(repo, f'read_{model.NAME}_by_id')( + model_id + ) + if model_instance: + return model.RETRIEVED(model_instance) + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"{model.NAME} not found", + ) + + @controller_exception_handler + async def _put_model( + self, + model: ApiBaseModel, + model_repo: RepositoryInterface, + model_id: str, + model_instance: ApiBaseModel, + ) -> ApiBaseView: + async with model_repo() as repo: + await getattr(repo, f'update_{model.NAME}_by_id')( + model_id, model_instance + ) + return model.UPDATED() + + @controller_exception_handler + async def _delete_model( + self, + model: ApiBaseModel, + model_repo: RepositoryInterface, + model_id: str, + ) -> ApiBaseView: + async with model_repo() as repo: + await getattr(repo, f'delete_{model.NAME}_by_id')(model_id) + return model.DELETED() diff --git a/lib/controllers/motor.py b/lib/controllers/motor.py index 81304fe..c73590c 100644 --- a/lib/controllers/motor.py +++ b/lib/controllers/motor.py @@ -1,141 +1,29 @@ -from typing import Union -from fastapi import HTTPException, status -from pymongo.errors import PyMongoError - -from lib import logger, parse_error -from lib.models.motor import Motor, MotorKinds -from lib.services.motor import MotorService -from lib.repositories.motor import MotorRepository -from lib.views.motor import ( - MotorSummary, - MotorCreated, - MotorUpdated, - MotorDeleted, - MotorView, +from lib.controllers.interface import ( + ControllerInterface, + controller_exception_handler, ) +from lib.views.motor import MotorSummary +from lib.models.motor import MotorModel +from lib.services.motor import MotorService -class MotorController: +class MotorController(ControllerInterface): """ Controller for the motor model. Enables: - - Create a rocketpy.Motor object from a Motor model object. + - Simulation of a RocketPy Motor. + - CRUD for Motor BaseApiModel. """ - @staticmethod - def guard(motor: Motor): - if ( - motor.motor_kind not in (MotorKinds.SOLID, MotorKinds.GENERIC) - and motor.tanks is None - ): - raise HTTPException( - status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, - detail="Tanks must be provided for liquid and hybrid motors.", - ) - - # TODO: extend guard to check motor kinds and tank kinds specifics - - @classmethod - async def create_motor( - cls, motor: Motor - ) -> Union[MotorCreated, HTTPException]: - """ - Create a models.Motor in the database. - - Returns: - views.MotorCreated - """ - motor_repo = None - try: - cls.guard(motor) - async with MotorRepository(motor) as motor_repo: - await motor_repo.create_motor() - except PyMongoError as e: - logger.error(f"controllers.motor.create_motor: PyMongoError {e}") - raise HTTPException( - status_code=status.HTTP_503_SERVICE_UNAVAILABLE, - detail="Failed to create motor in db", - ) from e - except HTTPException as e: - raise e from e - except Exception as e: - exc_str = parse_error(e) - logger.error(f"controllers.motor.create_motor: {exc_str}") - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail=f"Failed to create motor: {exc_str}", - ) from e - else: - return MotorCreated(motor_id=motor_repo.motor_id) - finally: - motor_id = ( - getattr(motor_repo, 'motor_id', 'unknown') - if motor_repo - else 'unknown' - ) - if motor_repo: - logger.info( - f"Call to controllers.motor.create_motor completed for Motor {motor_id}" - ) - - @staticmethod - async def get_motor_by_id( - motor_id: str, - ) -> Union[MotorView, HTTPException]: - """ - Get a models.Motor from the database. - - Args: - motor_id: str - - Returns: - models.Motor + def __init__(self): + super().__init__(models=[MotorModel]) - Raises: - HTTP 404 Not Found: If the motor is not found in the database. - """ - try: - async with MotorRepository() as motor_repo: - await motor_repo.get_motor_by_id(motor_id) - motor = motor_repo.motor - except PyMongoError as e: - logger.error( - f"controllers.motor.get_motor_by_id: PyMongoError {e}" - ) - raise HTTPException( - status_code=status.HTTP_503_SERVICE_UNAVAILABLE, - detail="Failed to read motor from db", - ) from e - except HTTPException as e: - raise e from e - except Exception as e: - exc_str = parse_error(e) - logger.error(f"controllers.motor.get_motor_by_id: {exc_str}") - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail=f"Failed to read motor: {exc_str}", - ) from e - else: - if motor: - motor_view = MotorView( - **motor.dict(), selected_motor_kind=motor.motor_kind - ) - return motor_view - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail="Motor not found", - ) - finally: - logger.info( - f"Call to controllers.motor.get_motor_by_id completed for Motor {motor_id}" - ) - - @classmethod + @controller_exception_handler async def get_rocketpy_motor_binary( - cls, + self, motor_id: str, - ) -> Union[bytes, HTTPException]: + ) -> bytes: """ Get a rocketpy.Motor object as a dill binary. @@ -148,114 +36,14 @@ async def get_rocketpy_motor_binary( Raises: HTTP 404 Not Found: If the motor is not found in the database. """ - try: - motor = await cls.get_motor_by_id(motor_id) - motor_service = MotorService.from_motor_model(motor) - except HTTPException as e: - raise e from e - except Exception as e: - exc_str = parse_error(e) - logger.error( - f"controllers.motor.get_rocketpy_motor_binary: {exc_str}" - ) - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail=f"Failed to read motor: {exc_str}", - ) from e - else: - return motor_service.get_motor_binary() - finally: - logger.info( - f"Call to controllers.motor.get_rocketpy_motor_binary completed for Motor {motor_id}" - ) - - @classmethod - async def update_motor_by_id( - cls, motor_id: str, motor: Motor - ) -> Union[MotorUpdated, HTTPException]: - """ - Update a motor in the database. - - Args: - motor_id: str - - Returns: - views.MotorUpdated - - Raises: - HTTP 404 Not Found: If the motor is not found in the database. - """ - try: - cls.guard(motor) - async with MotorRepository(motor) as motor_repo: - await motor_repo.update_motor_by_id(motor_id) - except PyMongoError as e: - logger.error(f"controllers.motor.update_motor: PyMongoError {e}") - raise HTTPException( - status_code=status.HTTP_503_SERVICE_UNAVAILABLE, - detail="Failed to update motor in db", - ) from e - except HTTPException as e: - raise e from e - except Exception as e: - exc_str = parse_error(e) - logger.error(f"controllers.motor.update_motor: {exc_str}") - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail=f"Failed to update motor: {exc_str}", - ) from e - else: - return MotorUpdated(motor_id=motor_id) - finally: - logger.info( - f"Call to controllers.motor.update_motor completed for Motor {motor_id}" - ) - - @staticmethod - async def delete_motor_by_id( - motor_id: str, - ) -> Union[MotorDeleted, HTTPException]: - """ - Delete a models.Motor from the database. - - Args: - motor_id: str - - Returns: - views.MotorDeleted - - Raises: - HTTP 404 Not Found: If the motor is not found in the database. - """ - try: - async with MotorRepository() as motor_repo: - await motor_repo.delete_motor_by_id(motor_id) - except PyMongoError as e: - logger.error(f"controllers.motor.delete_motor: PyMongoError {e}") - raise HTTPException( - status_code=status.HTTP_503_SERVICE_UNAVAILABLE, - detail="Failed to delete motor from db", - ) from e - except HTTPException as e: - raise e from e - except Exception as e: - exc_str = parse_error(e) - logger.error(f"controllers.motor.delete_motor: {exc_str}") - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail=f"Failed to delete motor: {exc_str}", - ) from e - else: - return MotorDeleted(motor_id=motor_id) - finally: - logger.info( - f"Call to controllers.motor.delete_motor completed for Motor {motor_id}" - ) + motor = await self.get_motor_by_id(motor_id) + motor_service = MotorService.from_motor_model(motor) + return motor_service.get_motor_binary() - @classmethod + @controller_exception_handler async def simulate_motor( - cls, motor_id: str - ) -> Union[MotorSummary, HTTPException]: + self, motor_id: str + ) -> MotorSummary: """ Simulate a rocketpy motor. @@ -268,22 +56,6 @@ async def simulate_motor( Raises: HTTP 404 Not Found: If the motor does not exist in the database. """ - try: - motor = await cls.get_motor_by_id(motor_id) - motor_service = MotorService.from_motor_model(motor) - motor_summary = motor_service.get_motor_summary() - except HTTPException as e: - raise e from e - except Exception as e: - exc_str = parse_error(e) - logger.error(f"controllers.motor.simulate_motor: {exc_str}") - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail=f"Failed to simulate motor, parameters may contain data that is not physically coherent: {exc_str}", - ) from e - else: - return motor_summary - finally: - logger.info( - f"Call to controllers.motor.simulate_motor completed for Motor {motor_id}" - ) + motor = await self.get_motor_by_id(motor_id) + motor_service = MotorService.from_motor_model(motor) + return motor_service.get_motor_summary() diff --git a/lib/controllers/rocket.py b/lib/controllers/rocket.py index 0bfea11..8cbedca 100644 --- a/lib/controllers/rocket.py +++ b/lib/controllers/rocket.py @@ -1,140 +1,28 @@ -from typing import Union - -from fastapi import HTTPException, status -from pymongo.errors import PyMongoError - -from lib import logger, parse_error -from lib.services.rocket import RocketService -from lib.models.rocket import Rocket -from lib.controllers.motor import MotorController -from lib.repositories.rocket import RocketRepository -from lib.views.motor import MotorView -from lib.views.rocket import ( - RocketSummary, - RocketCreated, - RocketUpdated, - RocketDeleted, - RocketView, +from lib.controllers.interface import ( + ControllerInterface, + controller_exception_handler, ) +from lib.views.rocket import RocketSummary +from lib.models.rocket import RocketModel +from lib.services.rocket import RocketService -class RocketController: +class RocketController(ControllerInterface): """ Controller for the Rocket model. Enables: - - CRUD operations over models.Rocket on the database. + - Simulation of a RocketPy Rocket. + - CRUD for Rocket BaseApiModel. """ - @staticmethod - def guard(rocket: Rocket): - MotorController.guard(rocket.motor) - - @classmethod - async def create_rocket( - cls, rocket: Rocket - ) -> Union[RocketCreated, HTTPException]: - """ - Create a models.Rocket in the database. - - Returns: - views.RocketCreated - """ - rocket_repo = None - try: - cls.guard(rocket) - async with RocketRepository(rocket) as rocket_repo: - await rocket_repo.create_rocket() - except PyMongoError as e: - logger.error(f"controllers.rocket.create_rocket: PyMongoError {e}") - raise HTTPException( - status_code=status.HTTP_503_SERVICE_UNAVAILABLE, - detail="Failed to create rocket in the db", - ) from e - except HTTPException as e: - raise e from e - except Exception as e: - exc_str = parse_error(e) - logger.error(f"controllers.rocket.create_rocket: {exc_str}") - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail=f"Failed to create rocket: {exc_str}", - ) from e - else: - return RocketCreated(rocket_id=rocket_repo.rocket_id) - finally: - rocket_id = ( - getattr(rocket_repo, 'rocket_id', 'unknown') - if rocket_repo - else 'unknown' - ) - if rocket_repo: - logger.info( - f"Call to controllers.rocket.create_rocket completed for Rocket {rocket_id}" - ) - - @staticmethod - async def get_rocket_by_id( - rocket_id: str, - ) -> Union[RocketView, HTTPException]: - """ - Get a rocket from the database. - - Args: - rocket_id: str - - Returns: - models.Rocket + def __init__(self): + super().__init__(models=[RocketModel]) - Raises: - HTTP 404 Not Found: If the rocket is not found in the database. - """ - try: - async with RocketRepository() as rocket_repo: - await rocket_repo.get_rocket_by_id(rocket_id) - rocket = rocket_repo.rocket - except PyMongoError as e: - logger.error( - f"controllers.rocket.get_rocket_by_id: PyMongoError {e}" - ) - raise HTTPException( - status_code=status.HTTP_503_SERVICE_UNAVAILABLE, - detail="Failed to read rocket from db", - ) from e - except HTTPException as e: - raise e from e - except Exception as e: - exc_str = parse_error(e) - logger.error(f"controllers.rocket.get_rocket_by_id: {exc_str}") - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail=f"Failed to read rocket: {exc_str}", - ) from e - else: - if rocket: - motor_view = MotorView( - **rocket.motor.dict(), - selected_motor_kind=rocket.motor.motor_kind, - ) - updated_rocket = rocket.dict() - updated_rocket.update(motor=motor_view) - rocket_view = RocketView( - **updated_rocket, - ) - return rocket_view - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail="Rocket not found", - ) - finally: - logger.info( - f"Call to controllers.rocket.get_rocket_by_id completed for Rocket {rocket_id}" - ) - - @classmethod + @controller_exception_handler async def get_rocketpy_rocket_binary( - cls, rocket_id: str - ) -> Union[bytes, HTTPException]: + self, rocket_id: str + ) -> bytes: """ Get a rocketpy.Rocket object as dill binary. @@ -147,115 +35,15 @@ async def get_rocketpy_rocket_binary( Raises: HTTP 404 Not Found: If the rocket is not found in the database. """ - try: - rocket = await cls.get_rocket_by_id(rocket_id) - rocket_service = RocketService.from_rocket_model(rocket) - except HTTPException as e: - raise e from e - except Exception as e: - exc_str = parse_error(e) - logger.error( - f"controllers.rocket.get_rocketpy_rocket_binary: {exc_str}" - ) - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail=f"Failed to read rocket: {exc_str}", - ) from e - else: - return rocket_service.get_rocket_binary() - finally: - logger.info( - f"Call to controllers.rocket.get_rocketpy_rocket_binary completed for Rocket {rocket_id}" - ) - - @classmethod - async def update_rocket_by_id( - cls, rocket_id: str, rocket: Rocket - ) -> Union[RocketUpdated, HTTPException]: - """ - Update a models.Rocket in the database. - - Args: - rocket_id: str - - Returns: - views.RocketUpdated - - Raises: - HTTP 404 Not Found: If the rocket is not found in the database. - """ - try: - cls.guard(rocket) - async with RocketRepository(rocket) as rocket_repo: - await rocket_repo.update_rocket_by_id(rocket_id) - except PyMongoError as e: - logger.error(f"controllers.rocket.update_rocket: PyMongoError {e}") - raise HTTPException( - status_code=status.HTTP_503_SERVICE_UNAVAILABLE, - detail="Failed to update rocket in the db", - ) from e - except HTTPException as e: - raise e from e - except Exception as e: - exc_str = parse_error(e) - logger.error(f"controllers.rocket.update_rocket: {exc_str}") - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail=f"Failed to update rocket: {exc_str}", - ) from e - else: - return RocketUpdated(rocket_id=rocket_id) - finally: - logger.info( - f"Call to controllers.rocket.update_rocket completed for Rocket {rocket_id}" - ) - - @staticmethod - async def delete_rocket_by_id( - rocket_id: str, - ) -> Union[RocketDeleted, HTTPException]: - """ - Delete a models.Rocket from the database. - - Args: - rocket_id: str - - Returns: - views.RocketDeleted - - Raises: - HTTP 404 Not Found: If the rocket is not found in the database. - """ - try: - async with RocketRepository() as rocket_repo: - await rocket_repo.delete_rocket_by_id(rocket_id) - except PyMongoError as e: - logger.error(f"controllers.rocket.delete_rocket: PyMongoError {e}") - raise HTTPException( - status_code=status.HTTP_503_SERVICE_UNAVAILABLE, - detail="Failed to delete rocket from db", - ) from e - except HTTPException as e: - raise e from e - except Exception as e: - exc_str = parse_error(e) - logger.error(f"controllers.rocket.delete_rocket: {exc_str}") - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail=f"Failed to delete rocket: {exc_str}", - ) from e - else: - return RocketDeleted(rocket_id=rocket_id) - finally: - logger.info( - f"Call to controllers.rocket.delete_rocket completed for Rocket {rocket_id}" - ) + rocket = await self.get_rocket_by_id(rocket_id) + rocket_service = RocketService.from_rocket_model(rocket) + return rocket_service.get_rocket_binary() - @classmethod + @controller_exception_handler async def simulate_rocket( - cls, + self, rocket_id: str, - ) -> Union[RocketSummary, HTTPException]: + ) -> RocketSummary: """ Simulate a rocketpy rocket. @@ -268,22 +56,6 @@ async def simulate_rocket( Raises: HTTP 404 Not Found: If the rocket does not exist in the database. """ - try: - rocket = await cls.get_rocket_by_id(rocket_id) - rocket_service = RocketService.from_rocket_model(rocket) - rocket_summary = rocket_service.get_rocket_summary() - except HTTPException as e: - raise e from e - except Exception as e: - exc_str = parse_error(e) - logger.error(f"controllers.rocket.simulate: {exc_str}") - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail=f"Failed to simulate rocket, parameters may contain data that is not physically coherent: {exc_str}", - ) from e - else: - return rocket_summary - finally: - logger.info( - f"Call to controllers.rocket.simulate completed for Rocket {rocket_id}" - ) + rocket = await self.get_rocket_by_id(rocket_id) + rocket_service = RocketService.from_rocket_model(rocket) + return rocket_service.get_rocket_summary() diff --git a/lib/models/environment.py b/lib/models/environment.py index 3b62940..be5c359 100644 --- a/lib/models/environment.py +++ b/lib/models/environment.py @@ -1,7 +1,7 @@ import datetime from enum import Enum -from typing import Optional -from pydantic import BaseModel +from typing import Optional, ClassVar, Self +from lib.models.interface import ApiBaseModel class AtmosphericModelTypes(str, Enum): @@ -13,7 +13,9 @@ class AtmosphericModelTypes(str, Enum): ENSEMBLE: str = "ENSEMBLE" -class Env(BaseModel): +class EnvironmentModel(ApiBaseModel): + NAME: ClassVar = 'environment' + METHODS: ClassVar = ('POST', 'GET', 'PUT', 'DELETE') latitude: float longitude: float elevation: Optional[int] = 1 @@ -26,3 +28,32 @@ class Env(BaseModel): date: Optional[datetime.datetime] = ( datetime.datetime.today() + datetime.timedelta(days=1) ) + + @staticmethod + def UPDATED(): + from lib.views.environment import EnvironmentUpdated + + return EnvironmentUpdated() + + @staticmethod + def DELETED(): + from lib.views.environment import EnvironmentDeleted + + return EnvironmentDeleted() + + @staticmethod + def CREATED(model_id: str): + from lib.views.environment import EnvironmentCreated + + return EnvironmentCreated(environment_id=model_id) + + @staticmethod + def RETRIEVED(model_instance: type(Self)): + from lib.views.environment import EnvironmentRetrieved, EnvironmentView + + return EnvironmentRetrieved( + environment=EnvironmentView( + environment_id=model_instance.get_id(), + **model_instance.model_dump(), + ) + ) diff --git a/lib/models/flight.py b/lib/models/flight.py index 7616475..e227f00 100644 --- a/lib/models/flight.py +++ b/lib/models/flight.py @@ -1,8 +1,8 @@ from enum import Enum -from typing import Optional -from pydantic import BaseModel -from lib.models.rocket import Rocket -from lib.models.environment import Env +from typing import Optional, Self, ClassVar +from lib.models.interface import ApiBaseModel +from lib.models.rocket import RocketModel +from lib.models.environment import EnvironmentModel class EquationsOfMotion(str, Enum): @@ -10,10 +10,13 @@ class EquationsOfMotion(str, Enum): SOLID_PROPULSION: str = "SOLID_PROPULSION" -class Flight(BaseModel): - name: str = "Flight" - environment: Env - rocket: Rocket +class FlightModel(ApiBaseModel): + NAME: ClassVar = "flight" + METHODS: ClassVar = ("POST", "GET", "PUT", "DELETE") + + name: str = "flight" + environment: EnvironmentModel + rocket: RocketModel rail_length: float = 1 time_overshoot: bool = True terminate_on_apogee: bool = True @@ -46,3 +49,32 @@ def get_additional_parameters(self): "equations_of_motion", ] } + + @staticmethod + def UPDATED(): + from lib.views.flight import FlightUpdated + + return FlightUpdated() + + @staticmethod + def DELETED(): + from lib.views.flight import FlightDeleted + + return FlightDeleted() + + @staticmethod + def CREATED(model_id: str): + from lib.views.flight import FlightCreated + + return FlightCreated(flight_id=model_id) + + @staticmethod + def RETRIEVED(model_instance: type(Self)): + from lib.views.flight import FlightRetrieved, FlightView + + return FlightRetrieved( + flight=FlightView( + flight_id=model_instance.get_id(), + **model_instance.model_dump(), + ) + ) diff --git a/lib/models/interface.py b/lib/models/interface.py new file mode 100644 index 0000000..1d7a085 --- /dev/null +++ b/lib/models/interface.py @@ -0,0 +1,51 @@ +from typing import Self, Optional +from abc import abstractmethod, ABC +from pydantic import BaseModel, PrivateAttr, ConfigDict +from bson import ObjectId + + +class ApiBaseModel(BaseModel, ABC): + _id: Optional[ObjectId] = PrivateAttr(default=None) + model_config = ConfigDict( + json_encoders={ObjectId: str}, + use_enum_values=True, + validate_default=True, + validate_all_in_root=True, + validate_assignment=True, + ) + + def set_id(self, value): + self._id = value + + def get_id(self): + return self._id + + @property + @abstractmethod + def NAME(): # pylint: disable=invalid-name, no-method-argument + pass + + @property + @abstractmethod + def METHODS(): # pylint: disable=invalid-name, no-method-argument + pass + + @staticmethod + @abstractmethod + def UPDATED(): # pylint: disable=invalid-name + pass + + @staticmethod + @abstractmethod + def DELETED(): # pylint: disable=invalid-name + pass + + @staticmethod + @abstractmethod + def CREATED(model_id: str): # pylint: disable=invalid-name + pass + + @staticmethod + @abstractmethod + def RETRIEVED(model_instance: type(Self)): # pylint: disable=invalid-name + pass diff --git a/lib/models/motor.py b/lib/models/motor.py index c8121bf..cb18603 100644 --- a/lib/models/motor.py +++ b/lib/models/motor.py @@ -1,76 +1,14 @@ -from enum import Enum -from typing import Optional, Tuple, List, Union -from pydantic import BaseModel, PrivateAttr +from typing import Optional, Tuple, List, Union, Self, ClassVar +from pydantic import PrivateAttr, model_validator +from lib.models.interface import ApiBaseModel +from lib.models.sub.tanks import MotorTank, TankFluids, TankKinds, InterpolationMethods, TankCoordinateSystemOrientation, MotorKinds, MotorTank -class MotorKinds(str, Enum): - HYBRID: str = "HYBRID" - SOLID: str = "SOLID" - GENERIC: str = "GENERIC" - LIQUID: str = "LIQUID" +class MotorModel(ApiBaseModel): + NAME: ClassVar = 'motor' + METHODS: ClassVar = ('POST', 'GET', 'PUT', 'DELETE') -class TankKinds(str, Enum): - LEVEL: str = "LEVEL" - MASS: str = "MASS" - MASS_FLOW: str = "MASSFLOW" - ULLAGE: str = "ULLAGE" - - -class CoordinateSystemOrientation(str, Enum): - NOZZLE_TO_COMBUSTION_CHAMBER: str = "NOZZLE_TO_COMBUSTION_CHAMBER" - COMBUSTION_CHAMBER_TO_NOZZLE: str = "COMBUSTION_CHAMBER_TO_NOZZLE" - - -class TankFluids(BaseModel): - name: str - density: float - - -class InterpolationMethods(str, Enum): - LINEAR: str = "LINEAR" - SPLINE: str = "SPLINE" - AKIMA: str = "AKIMA" - POLYNOMIAL: str = "POLYNOMIAL" - SHEPARD: str = "SHEPARD" - RBF: str = "RBF" - - -class MotorTank(BaseModel): - # Required parameters - geometry: List[Tuple[Tuple[float, float], float]] - gas: TankFluids - liquid: TankFluids - flux_time: Tuple[float, float] - position: float - discretize: int - - # Level based tank parameters - liquid_height: Optional[float] = None - - # Mass based tank parameters - liquid_mass: Optional[float] = None - gas_mass: Optional[float] = None - - # Mass flow based tank parameters - gas_mass_flow_rate_in: Optional[float] = None - gas_mass_flow_rate_out: Optional[float] = None - liquid_mass_flow_rate_in: Optional[float] = None - liquid_mass_flow_rate_out: Optional[float] = None - initial_liquid_mass: Optional[float] = None - initial_gas_mass: Optional[float] = None - - # Ullage based tank parameters - ullage: Optional[float] = None - - # Optional parameters - name: Optional[str] = None - - # Computed parameters - tank_kind: TankKinds = TankKinds.MASS_FLOW - - -class Motor(BaseModel): # Required parameters thrust_source: List[List[float]] burn_time: float @@ -103,17 +41,57 @@ class Motor(BaseModel): # Optional parameters interpolation_method: InterpolationMethods = InterpolationMethods.LINEAR - coordinate_system_orientation: CoordinateSystemOrientation = ( - CoordinateSystemOrientation.NOZZLE_TO_COMBUSTION_CHAMBER + coordinate_system_orientation: TankCoordinateSystemOrientation = ( + TankCoordinateSystemOrientation.NOZZLE_TO_COMBUSTION_CHAMBER ) reshape_thrust_curve: Union[bool, tuple] = False # Computed parameters _motor_kind: MotorKinds = PrivateAttr(default=MotorKinds.SOLID) + @model_validator(mode='after') + # TODO: extend guard to check motor kinds and tank kinds specifics + def validate_motor_kind(self): + if ( + self._motor_kind not in (MotorKinds.SOLID, MotorKinds.GENERIC) + and self.tanks is None + ): + raise ValueError("Tanks must be provided for liquid and hybrid motors.") + return self + @property def motor_kind(self) -> MotorKinds: return self._motor_kind def set_motor_kind(self, motor_kind: MotorKinds): self._motor_kind = motor_kind + return self + + @staticmethod + def UPDATED(): + from lib.views.motor import MotorUpdated + + return MotorUpdated() + + @staticmethod + def DELETED(): + from lib.views.motor import MotorDeleted + + return MotorDeleted() + + @staticmethod + def CREATED(model_id: str): + from lib.views.motor import MotorCreated + + return MotorCreated(motor_id=model_id) + + @staticmethod + def RETRIEVED(model_instance: type(Self)): + from lib.views.motor import MotorRetrieved, MotorView + + return MotorRetrieved( + motor=MotorView( + motor_id=model_instance.get_id(), + **model_instance.model_dump(), + ) + ) diff --git a/lib/models/rocket.py b/lib/models/rocket.py index ab45419..f8932d1 100644 --- a/lib/models/rocket.py +++ b/lib/models/rocket.py @@ -1,8 +1,8 @@ from enum import Enum -from typing import Optional, Tuple, List, Union -from pydantic import BaseModel -from lib.models.motor import Motor -from lib.models.aerosurfaces import ( +from typing import Optional, Tuple, List, Union, Self, ClassVar +from lib.models.interface import ApiBaseModel +from lib.models.motor import MotorModel +from lib.models.sub.aerosurfaces import ( Fins, NoseCone, Tail, @@ -11,15 +11,17 @@ ) -class CoordinateSystemOrientation(str, Enum): +class RocketCoordinateSystemOrientation(str, Enum): TAIL_TO_NOSE: str = "TAIL_TO_NOSE" NOSE_TO_TAIL: str = "NOSE_TO_TAIL" -class Rocket(BaseModel): +class RocketModel(ApiBaseModel): + NAME: ClassVar = "rocket" + METHODS: ClassVar = ("POST", "GET", "PUT", "DELETE") # Required parameters - motor: Motor + motor: MotorModel radius: float mass: float motor_position: float @@ -30,8 +32,8 @@ class Rocket(BaseModel): ] = (0, 0, 0) power_off_drag: List[Tuple[float, float]] = [(0, 0)] power_on_drag: List[Tuple[float, float]] = [(0, 0)] - coordinate_system_orientation: CoordinateSystemOrientation = ( - CoordinateSystemOrientation.TAIL_TO_NOSE + coordinate_system_orientation: RocketCoordinateSystemOrientation = ( + RocketCoordinateSystemOrientation.TAIL_TO_NOSE ) nose: NoseCone fins: List[Fins] @@ -40,3 +42,32 @@ class Rocket(BaseModel): parachutes: Optional[List[Parachute]] = None rail_buttons: Optional[RailButtons] = None tail: Optional[Tail] = None + + @staticmethod + def UPDATED(): + from lib.views.rocket import RocketUpdated + + return RocketUpdated() + + @staticmethod + def DELETED(): + from lib.views.rocket import RocketDeleted + + return RocketDeleted() + + @staticmethod + def CREATED(model_id: str): + from lib.views.rocket import RocketCreated + + return RocketCreated(rocket_id=model_id) + + @staticmethod + def RETRIEVED(model_instance: type(Self)): + from lib.views.rocket import RocketRetrieved, RocketView + + return RocketRetrieved( + rocket=RocketView( + rocket_id=model_instance.get_id(), + **model_instance.model_dump(), + ) + ) diff --git a/lib/models/aerosurfaces.py b/lib/models/sub/aerosurfaces.py similarity index 100% rename from lib/models/aerosurfaces.py rename to lib/models/sub/aerosurfaces.py diff --git a/lib/models/sub/tanks.py b/lib/models/sub/tanks.py new file mode 100644 index 0000000..abe8f48 --- /dev/null +++ b/lib/models/sub/tanks.py @@ -0,0 +1,70 @@ +from enum import Enum +from typing import Optional, Tuple, List +from pydantic import BaseModel + + +class MotorKinds(str, Enum): + HYBRID: str = "HYBRID" + SOLID: str = "SOLID" + GENERIC: str = "GENERIC" + LIQUID: str = "LIQUID" + + +class TankKinds(str, Enum): + LEVEL: str = "LEVEL" + MASS: str = "MASS" + MASS_FLOW: str = "MASSFLOW" + ULLAGE: str = "ULLAGE" + + +class TankCoordinateSystemOrientation(str, Enum): + NOZZLE_TO_COMBUSTION_CHAMBER: str = "NOZZLE_TO_COMBUSTION_CHAMBER" + COMBUSTION_CHAMBER_TO_NOZZLE: str = "COMBUSTION_CHAMBER_TO_NOZZLE" + + +class TankFluids(BaseModel): + name: str + density: float + + +class InterpolationMethods(str, Enum): + LINEAR: str = "LINEAR" + SPLINE: str = "SPLINE" + AKIMA: str = "AKIMA" + POLYNOMIAL: str = "POLYNOMIAL" + SHEPARD: str = "SHEPARD" + RBF: str = "RBF" + + +class MotorTank(BaseModel): + # Required parameters + geometry: List[Tuple[Tuple[float, float], float]] + gas: TankFluids + liquid: TankFluids + flux_time: Tuple[float, float] + position: float + discretize: int + + # Level based tank parameters + liquid_height: Optional[float] = None + + # Mass based tank parameters + liquid_mass: Optional[float] = None + gas_mass: Optional[float] = None + + # Mass flow based tank parameters + gas_mass_flow_rate_in: Optional[float] = None + gas_mass_flow_rate_out: Optional[float] = None + liquid_mass_flow_rate_in: Optional[float] = None + liquid_mass_flow_rate_out: Optional[float] = None + initial_liquid_mass: Optional[float] = None + initial_gas_mass: Optional[float] = None + + # Ullage based tank parameters + ullage: Optional[float] = None + + # Optional parameters + name: Optional[str] = None + + # Computed parameters + tank_kind: TankKinds = TankKinds.MASS_FLOW diff --git a/lib/repositories/environment.py b/lib/repositories/environment.py index 56dac18..2f37632 100644 --- a/lib/repositories/environment.py +++ b/lib/repositories/environment.py @@ -1,142 +1,34 @@ -from typing import Self -from bson import ObjectId -from pymongo.errors import PyMongoError -from lib.models.environment import Env -from lib import logger -from lib.repositories.repo import Repository, RepositoryNotInitializedException +from typing import Optional +from lib.models.environment import EnvironmentModel +from lib.repositories.interface import ( + RepositoryInterface, + repository_exception_handler, +) -class EnvRepository(Repository): +class EnvironmentRepository(RepositoryInterface): """ - Enables database CRUD operations with models.Env + Enables database CRUD operations with models.Environment Init Attributes: - environment: models.Env + environment: models.EnvironmentModel """ - def __init__(self, environment: Env = None): - super().__init__("environments") - self._env = environment - self._env_id = None + def __init__(self): + super().__init__(EnvironmentModel) - @property - def env(self) -> Env: - return self._env + @repository_exception_handler + async def create_environment(self, environment: EnvironmentModel) -> str: + return await self.insert(environment.model_dump()) - @env.setter - def env(self, environment: "Env"): - self._env = environment + @repository_exception_handler + async def read_environment_by_id(self, environment_id: str) -> Optional[EnvironmentModel]: + return await self.find_by_id(data_id=environment_id) - @property - def env_id(self) -> str: - return str(self._env_id) + @repository_exception_handler + async def update_environment_by_id(self, environment_id: str, environment: EnvironmentModel): + await self.update_by_id(environment.model_dump(), data_id=environment_id) - @env_id.setter - def env_id(self, env_id: "str"): - self._env_id = env_id - - async def insert_env(self, env_data: dict): - collection = self.get_collection() - result = await collection.insert_one(env_data) - self.env_id = result.inserted_id - return self - - async def update_env(self, env_data: dict, env_id: str): - collection = self.get_collection() - await collection.update_one( - {"_id": ObjectId(env_id)}, {"$set": env_data} - ) - return self - - async def find_env(self, env_id: str): - collection = self.get_collection() - return await collection.find_one({"_id": ObjectId(env_id)}) - - async def delete_env(self, env_id: str): - collection = self.get_collection() - await collection.delete_one({"_id": ObjectId(env_id)}) - return self - - async def create_env(self): - """ - Creates a models.Env in the database - - Returns: - self - """ - try: - environment_to_dict = self.env.dict() - await self.insert_env(environment_to_dict) - except PyMongoError as e: - raise e from e - except RepositoryNotInitializedException as e: - raise e from e - else: - return self - finally: - logger.info( - f"Call to repositories.environment.create_env completed for Env {self.env_id}" - ) - - async def get_env_by_id(self, env_id: str) -> Self: - """ - Gets a models.Env from the database - - Returns: - self - """ - try: - read_env = await self.find_env(env_id) - parsed_env = Env.parse_obj(read_env) if read_env else None - self.env = parsed_env - except PyMongoError as e: - raise e from e - except RepositoryNotInitializedException as e: - raise e from e - else: - return self - finally: - logger.info( - f"Call to repositories.environment.get_env completed for Env {env_id}" - ) - - async def delete_env_by_id(self, env_id: str): - """ - Deletes a models.Env from the database - - Returns: - self - """ - try: - await self.delete_env(env_id) - except PyMongoError as e: - raise e from e - except RepositoryNotInitializedException as e: - raise e from e - else: - return self - finally: - logger.info( - f"Call to repositories.environment.delete_env completed for Env {env_id}" - ) - - async def update_env_by_id(self, env_id: str): - """ - Updates a models.Env in the database - - Returns: - self - """ - try: - environment_to_dict = self.env.dict() - await self.update_env(environment_to_dict, env_id) - except PyMongoError as e: - raise e from e - except RepositoryNotInitializedException as e: - raise e from e - else: - return self - finally: - logger.info( - f"Call to repositories.environment.update_env_by_id completed for Env {env_id}" - ) + @repository_exception_handler + async def delete_environment_by_id(self, environment_id: str): + await self.delete_by_id(data_id=environment_id) diff --git a/lib/repositories/flight.py b/lib/repositories/flight.py index 0c5f1a9..fbe6fc5 100644 --- a/lib/repositories/flight.py +++ b/lib/repositories/flight.py @@ -1,212 +1,34 @@ -from typing import Self -from bson import ObjectId -from pymongo.errors import PyMongoError -from lib import logger -from lib.models.flight import Flight -from lib.models.motor import MotorKinds -from lib.repositories.repo import Repository, RepositoryNotInitializedException +from typing import Optional +from lib.models.flight import FlightModel +from lib.repositories.interface import ( + RepositoryInterface, + repository_exception_handler, +) -class FlightRepository(Repository): +class FlightRepository(RepositoryInterface): """ Enables database CRUD operations with models.Flight Init Attributes: - flight: models.Flight + flight: models.FlightModel """ - def __init__(self, flight: Flight = None): - super().__init__("flights") - self._flight = flight - self._flight_id = None + def __init__(self): + super().__init__(FlightModel) - @property - def flight(self) -> Flight: - return self._flight + @repository_exception_handler + async def create_flight(self, flight: FlightModel) -> str: + return await self.insert(flight.model_dump()) - @flight.setter - def flight(self, flight: "Flight"): - self._flight = flight + @repository_exception_handler + async def read_flight_by_id(self, flight_id: str) -> Optional[FlightModel]: + return await self.find_by_id(data_id=flight_id) - @property - def flight_id(self) -> str: - return str(self._flight_id) - - @flight_id.setter - def flight_id(self, flight_id: "str"): - self._flight_id = flight_id - - async def insert_flight(self, flight_data: dict): - collection = self.get_collection() - result = await collection.insert_one(flight_data) - self.flight_id = result.inserted_id - return self - - async def update_flight(self, flight_data: dict, flight_id: str): - collection = self.get_collection() - await collection.update_one( - {"_id": ObjectId(flight_id)}, {"$set": flight_data} - ) - return self - - async def update_env(self, env_data: dict, flight_id: str): - collection = self.get_collection() - await collection.update_one( - {"_id": ObjectId(flight_id)}, {"$set": {"environment": env_data}} - ) - return self - - async def update_rocket(self, rocket_data: dict, flight_id: str): - collection = self.get_collection() - await collection.update_one( - {"_id": ObjectId(flight_id)}, {"$set": {"rocket": rocket_data}} - ) - return self - - async def find_flight(self, flight_id: str): - collection = self.get_collection() - return await collection.find_one({"_id": ObjectId(flight_id)}) - - async def delete_flight(self, flight_id: str): - collection = self.get_collection() - await collection.delete_one({"_id": ObjectId(flight_id)}) - return self - - async def create_flight(self): - """ - Creates a models.Flight in the database - - Returns: - self - """ - try: - flight_to_dict = self.flight.dict() - flight_to_dict["rocket"]["motor"][ - "motor_kind" - ] = self.flight.rocket.motor.motor_kind.value - await self.insert_flight(flight_to_dict) - except PyMongoError as e: - raise e from e - except RepositoryNotInitializedException as e: - raise e from e - else: - return self - finally: - logger.info( - f"Call to repositories.flight.create_flight completed for Flight {self.flight_id}" - ) - - async def get_flight_by_id(self, flight_id: str) -> Self: - """ - Gets a models.Flight from the database - - Returns: - self - """ - try: - read_flight = await self.find_flight(flight_id) - if read_flight: - parsed_flight = Flight.parse_obj(read_flight) - parsed_flight.rocket.motor.set_motor_kind( - MotorKinds(read_flight["rocket"]["motor"]["motor_kind"]) - ) - self.flight = parsed_flight - except PyMongoError as e: - raise e from e - except RepositoryNotInitializedException as e: - raise e from e - else: - return self - finally: - logger.info( - f"Call to repositories.flight.get_flight completed for Flight {flight_id}" - ) + @repository_exception_handler + async def update_flight_by_id(self, flight_id: str, flight: FlightModel): + await self.update_by_id(flight.model_dump(), data_id=flight_id) + @repository_exception_handler async def delete_flight_by_id(self, flight_id: str): - """ - Deletes a models.Flight from the database - - Returns: - self - """ - try: - await self.delete_flight(flight_id) - except PyMongoError as e: - raise e from e - except RepositoryNotInitializedException as e: - raise e from e - else: - return self - finally: - logger.info( - f"Call to repositories.flight.delete_flight completed for Flight {flight_id}" - ) - - async def update_flight_by_id(self, flight_id: str): - """ - Updates a models.Flight in the database - - Returns: - self - """ - try: - flight_to_dict = self.flight.dict() - flight_to_dict["rocket"]["motor"][ - "motor_kind" - ] = self.flight.rocket.motor.motor_kind.value - await self.update_flight(flight_to_dict, flight_id) - except PyMongoError as e: - raise e from e - except RepositoryNotInitializedException as e: - raise e from e - else: - return self - finally: - logger.info( - f"Call to repositories.flight.update_flight_by_id completed for Flight {flight_id}" - ) - - async def update_env_by_flight_id(self, flight_id: str): - """ - Updates a models.Flight.Env in the database - - Returns: - self - """ - try: - env_to_dict = self.flight.environment.dict() - await self.update_env(env_to_dict, flight_id) - except PyMongoError as e: - raise e from e - except RepositoryNotInitializedException as e: - raise e from e - else: - return self - finally: - logger.info( - f"Call to repositories.flight.update_env_by_flight_id completed for Flight {flight_id}" - ) - - async def update_rocket_by_flight_id(self, flight_id: str): - """ - Updates a models.Flight.Rocket in the database - - Returns: - self - """ - try: - rocket_to_dict = self.flight.rocket.dict() - rocket_to_dict["motor"][ - "motor_kind" - ] = self.flight.rocket.motor.motor_kind.value - await self.update_rocket(rocket_to_dict, flight_id) - except PyMongoError as e: - raise e from e - except RepositoryNotInitializedException as e: - raise e from e - else: - return self - finally: - logger.info( - f"Call to repositories.flight.update_rocket_by_flight_id completed for Flight {flight_id}" - ) + await self.delete_by_id(data_id=flight_id) diff --git a/lib/repositories/repo.py b/lib/repositories/interface.py similarity index 59% rename from lib/repositories/repo.py rename to lib/repositories/interface.py index 9a9f7ae..9b97595 100644 --- a/lib/repositories/repo.py +++ b/lib/repositories/interface.py @@ -1,18 +1,28 @@ +import importlib import asyncio import threading +import functools +from typing import Self from tenacity import ( stop_after_attempt, wait_fixed, retry, ) from pydantic import BaseModel +from pymongo.errors import PyMongoError from pymongo.server_api import ServerApi from motor.motor_asyncio import AsyncIOMotorClient from fastapi import HTTPException, status +from bson import ObjectId from lib import logger from lib.secrets import Secrets +from lib.models.interface import ApiBaseModel + + +def not_implemented(*args, **kwargs): + raise NotImplementedError("Method not implemented.") class RepositoryNotInitializedException(HTTPException): @@ -23,6 +33,32 @@ def __init__(self): ) +def repository_exception_handler(method): + @functools.wraps(method) + async def wrapper(self, *args, **kwargs): + try: + return await method(self, *args, **kwargs) + except PyMongoError as e: + logger.exception(f"{method.__name__} - caught PyMongoError: {e}") + raise e from e + except RepositoryNotInitializedException as e: + raise e from e + except Exception as e: + logger.exception( + f"{method.__name__} - caught unexpected error: {e}" + ) + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail='Unexpected error ocurred', + ) from e + finally: + logger.info( + f"Call to repositories.{self.model.NAME}.{method.__name__} completed for {kwargs}" + ) + + return wrapper + + class RepoInstances(BaseModel): instance: object prospecting: int = 0 @@ -40,9 +76,9 @@ def get_instance(self): return self.instance -class Repository: +class RepositoryInterface: """ - Base class for all repositories (singleton) + Interface class for all repositories (singleton) """ _global_instances = {} @@ -54,7 +90,7 @@ def __new__(cls, *args, **kwargs): cls._global_thread_lock ): # Ensure thread safety for singleton instance creation if cls not in cls._global_instances: - instance = super(Repository, cls).__new__(cls) + instance = super(RepositoryInterface, cls).__new__(cls) cls._global_instances[cls] = RepoInstances(instance=instance) else: cls._global_instances[cls].add_prospecting() @@ -71,10 +107,10 @@ def _get_instance_prospecting(cls): return cls._global_instances[cls].get_prospecting() return 0 - def __init__(self, collection_name: str, *, max_pool_size: int = 10): + def __init__(self, model: ApiBaseModel, *, max_pool_size: int = 3): if not getattr(self, '_initialized', False): + self.model = model self._max_pool_size = max_pool_size - self._collection_name = collection_name self._initialized_event = asyncio.Event() self._initialize() @@ -128,11 +164,11 @@ def _initialize_connection(self): self._connection_string, server_api=ServerApi("1"), maxIdleTimeMS=30000, - minPoolSize=10, + minPoolSize=1, maxPoolSize=self._max_pool_size, serverSelectionTimeoutMS=60000, ) - self._collection = self._client.rocketpy[self._collection_name] + self._collection = self._client.rocketpy[self.model.NAME] logger.info("MongoDB client initialized for %s", self.__class__) except Exception as e: logger.error( @@ -164,3 +200,51 @@ def get_collection(self): if not getattr(self, '_initialized', False): raise RepositoryNotInitializedException() return self._collection + + @classmethod + def get_model_repo(cls, model: ApiBaseModel) -> Self: + repo_path = cls.__module__.replace('interface', f'{model.NAME}') + return getattr( + importlib.import_module(repo_path), + f"{model.NAME.capitalize()}Repository", + ) + + @repository_exception_handler + async def insert(self, data: dict): + collection = self.get_collection() + assert self.model.model_validate(data) + result = await collection.insert_one(data) + return str(result.inserted_id) + + @repository_exception_handler + async def update_by_id(self, data: dict, *, data_id: str): + collection = self.get_collection() + assert self.model.model_validate(data) + await collection.update_one({"_id": ObjectId(data_id)}, {"$set": data}) + return self + + @repository_exception_handler + async def find_by_id(self, *, data_id: str): + collection = self.get_collection() + read_data = await collection.find_one({"_id": ObjectId(data_id)}) + if read_data: + parsed_model = self.model.model_validate(read_data) + parsed_model.set_id(str(read_data["_id"])) + return parsed_model + return None + + @repository_exception_handler + async def delete_by_id(self, *, data_id: str): + collection = self.get_collection() + await collection.delete_one({"_id": ObjectId(data_id)}) + return self + + @repository_exception_handler + async def find_by_query(self, query: dict): + collection = self.get_collection() + parsed_models = [] + async for read_data in collection.find(query): + parsed_model = self.model.model_validate(read_data) + parsed_model.set_id(str(read_data["_id"])) + parsed_models.append(parsed_model) + return parsed_models diff --git a/lib/repositories/motor.py b/lib/repositories/motor.py index f63be78..452940d 100644 --- a/lib/repositories/motor.py +++ b/lib/repositories/motor.py @@ -1,148 +1,34 @@ -from typing import Self -from bson import ObjectId -from pymongo.errors import PyMongoError -from lib import logger -from lib.models.motor import Motor, MotorKinds -from lib.repositories.repo import Repository, RepositoryNotInitializedException +from typing import Optional +from lib.models.motor import MotorModel +from lib.repositories.interface import ( + RepositoryInterface, + repository_exception_handler, +) -class MotorRepository(Repository): +class MotorRepository(RepositoryInterface): """ Enables database CRUD operations with models.Motor Init Attributes: - motor: models.Motor + motor: models.MotorModel """ - def __init__(self, motor: Motor = None): - super().__init__("motors") - self._motor = motor - self._motor_id = None + def __init__(self): + super().__init__(MotorModel) - @property - def motor(self) -> Motor: - return self._motor + @repository_exception_handler + async def create_motor(self, motor: MotorModel) -> str: + return await self.insert(motor.model_dump()) - @motor.setter - def motor(self, motor: "Motor"): - self._motor = motor + @repository_exception_handler + async def read_motor_by_id(self, motor_id: str) -> Optional[MotorModel]: + return await self.find_by_id(data_id=motor_id) - @property - def motor_id(self) -> str: - return str(self._motor_id) - - @motor_id.setter - def motor_id(self, motor_id: "str"): - self._motor_id = motor_id - - async def insert_motor(self, motor_data: dict): - collection = self.get_collection() - result = await collection.insert_one(motor_data) - self.motor_id = result.inserted_id - return self - - async def update_motor(self, motor_data: dict, motor_id: str): - collection = self.get_collection() - await collection.update_one( - {"_id": ObjectId(motor_id)}, {"$set": motor_data} - ) - return self - - async def find_motor(self, motor_id: str): - collection = self.get_collection() - return await collection.find_one({"_id": ObjectId(motor_id)}) - - async def delete_motor(self, motor_id: str): - collection = self.get_collection() - await collection.delete_one({"_id": ObjectId(motor_id)}) - return self - - async def create_motor(self): - """ - Creates a models.Motor in the database - - Returns: - self - """ - try: - motor_to_dict = self.motor.dict() - motor_to_dict["motor_kind"] = self.motor.motor_kind.value - await self.insert_motor(motor_to_dict) - except PyMongoError as e: - raise e from e - except RepositoryNotInitializedException as e: - raise e from e - else: - return self - finally: - logger.info( - f"Call to repositories.motor.create_motor completed for Motor {self.motor_id}" - ) - - async def get_motor_by_id(self, motor_id: str) -> Self: - """ - Gets a models.Motor from the database - - Returns: - self - """ - try: - read_motor = await self.find_motor(motor_id) - if read_motor: - parsed_motor = Motor.parse_obj(read_motor) - parsed_motor.set_motor_kind( - MotorKinds(read_motor["motor_kind"]) - ) - self.motor = parsed_motor - except PyMongoError as e: - raise e from e - except RepositoryNotInitializedException as e: - raise e from e - else: - return self - finally: - logger.info( - f"Call to repositories.motor.get_motor completed for Motor {motor_id}" - ) + @repository_exception_handler + async def update_motor_by_id(self, motor_id: str, motor: MotorModel): + await self.update_by_id(motor.model_dump(), data_id=motor_id) + @repository_exception_handler async def delete_motor_by_id(self, motor_id: str): - """ - Deletes a models.Motor from the database - - Returns: - self - """ - try: - await self.delete_motor(motor_id) - except PyMongoError as e: - raise e from e - except RepositoryNotInitializedException as e: - raise e from e - else: - return self - finally: - logger.info( - f"Call to repositories.motor.delete_motor completed for Motor {motor_id}" - ) - - async def update_motor_by_id(self, motor_id: str): - """ - Updates a models.Motor in the database - - Returns: - self - """ - try: - motor_to_dict = self.motor.dict() - motor_to_dict["motor_kind"] = self.motor.motor_kind.value - await self.update_motor(motor_to_dict, motor_id) - except PyMongoError as e: - raise e from e - except RepositoryNotInitializedException as e: - raise e from e - else: - return self - finally: - logger.info( - f"Call to repositories.motor.update_motor completed for Motor {motor_id}" - ) + await self.delete_by_id(data_id=motor_id) diff --git a/lib/repositories/rocket.py b/lib/repositories/rocket.py index 951f9f1..0b7923c 100644 --- a/lib/repositories/rocket.py +++ b/lib/repositories/rocket.py @@ -1,153 +1,34 @@ -from typing import Self -from bson import ObjectId -from pymongo.errors import PyMongoError -from lib import logger -from lib.models.rocket import Rocket -from lib.models.motor import MotorKinds -from lib.repositories.repo import Repository, RepositoryNotInitializedException +from typing import Optional +from lib.models.rocket import RocketModel +from lib.repositories.interface import ( + RepositoryInterface, + repository_exception_handler, +) -class RocketRepository(Repository): +class RocketRepository(RepositoryInterface): """ Enables database CRUD operations with models.Rocket Init Attributes: - rocket: models.Rocket + rocket: models.RocketModel """ - def __init__(self, rocket: Rocket = None): - super().__init__("rockets") - self._rocket_id = None - self._rocket = rocket + def __init__(self): + super().__init__(RocketModel) - @property - def rocket(self) -> Rocket: - return self._rocket + @repository_exception_handler + async def create_rocket(self, rocket: RocketModel) -> str: + return await self.insert(rocket.model_dump()) - @rocket.setter - def rocket(self, rocket: "Rocket"): - self._rocket = rocket + @repository_exception_handler + async def read_rocket_by_id(self, rocket_id: str) -> Optional[RocketModel]: + return await self.find_by_id(data_id=rocket_id) - @property - def rocket_id(self) -> str: - return str(self._rocket_id) - - @rocket_id.setter - def rocket_id(self, rocket_id: "str"): - self._rocket_id = rocket_id - - async def insert_rocket(self, rocket_data: dict): - collection = self.get_collection() - result = await collection.insert_one(rocket_data) - self.rocket_id = result.inserted_id - return self - - async def update_rocket(self, rocket_data: dict, rocket_id: str): - collection = self.get_collection() - await collection.update_one( - {"_id": ObjectId(rocket_id)}, {"$set": rocket_data} - ) - return self - - async def find_rocket(self, rocket_id: str): - collection = self.get_collection() - return await collection.find_one({"_id": ObjectId(rocket_id)}) - - async def delete_rocket(self, rocket_id: str): - collection = self.get_collection() - await collection.delete_one({"_id": ObjectId(rocket_id)}) - return self - - async def create_rocket(self): - """ - Creates a models.Rocket in the database - - Returns: - self - """ - try: - rocket_to_dict = self.rocket.dict() - rocket_to_dict["motor"][ - "motor_kind" - ] = self.rocket.motor.motor_kind.value - await self.insert_rocket(rocket_to_dict) - except PyMongoError as e: - raise e from e - except RepositoryNotInitializedException as e: - raise e from e - else: - return self - finally: - logger.info( - f"Call to repositories.rocket.create_rocket completed for Rocket {self.rocket_id}" - ) - - async def get_rocket_by_id(self, rocket_id: str) -> Self: - """ - Gets a models.Rocket from the database - - Returns: - self - """ - try: - read_rocket = await self.find_rocket(rocket_id) - if read_rocket: - parsed_rocket = Rocket.parse_obj(read_rocket) - parsed_rocket.motor.set_motor_kind( - MotorKinds(read_rocket["motor"]["motor_kind"]) - ) - self.rocket = parsed_rocket - except PyMongoError as e: - raise e from e - except RepositoryNotInitializedException as e: - raise e from e - else: - return self - finally: - logger.info( - f"Call to repositories.rocket.get_rocket completed for Rocket {rocket_id}" - ) + @repository_exception_handler + async def update_rocket_by_id(self, rocket_id: str, rocket: RocketModel): + await self.update_by_id(rocket.model_dump(), data_id=rocket_id) + @repository_exception_handler async def delete_rocket_by_id(self, rocket_id: str): - """ - Deletes a models.Rocket from the database - - Returns: - self - """ - try: - await self.delete_rocket(rocket_id) - except PyMongoError as e: - raise e from e - except RepositoryNotInitializedException as e: - raise e from e - else: - return self - finally: - logger.info( - f"Call to repositories.rocket.delete_rocket completed for Rocket {rocket_id}" - ) - - async def update_rocket_by_id(self, rocket_id: str): - """ - Updates a models.Rocket in the database - - Returns: - self - """ - try: - rocket_to_dict = self.rocket.dict() - rocket_to_dict["motor"][ - "motor_kind" - ] = self.rocket.motor.motor_kind.value - await self.update_rocket(rocket_to_dict, rocket_id) - except PyMongoError as e: - raise e from e - except RepositoryNotInitializedException as e: - raise e from e - else: - return self - finally: - logger.info( - f"Call to repositories.rocket.update_rocket_by_id completed for Rocket {rocket_id}" - ) + await self.delete_by_id(data_id=rocket_id) diff --git a/lib/routes/environment.py b/lib/routes/environment.py index 6834ed3..e007131 100644 --- a/lib/routes/environment.py +++ b/lib/routes/environment.py @@ -6,13 +6,14 @@ from opentelemetry import trace from lib.views.environment import ( - EnvSummary, - EnvCreated, - EnvUpdated, - EnvDeleted, + EnvironmentSummary, + EnvironmentCreated, + EnvironmentRetrieved, + EnvironmentUpdated, + EnvironmentDeleted, ) -from lib.models.environment import Env -from lib.controllers.environment import EnvController +from lib.models.environment import EnvironmentModel +from lib.controllers.environment import EnvironmentController router = APIRouter( prefix="/environments", @@ -28,46 +29,62 @@ @router.post("/") -async def create_env(env: Env) -> EnvCreated: +async def create_environment(environment: EnvironmentModel) -> EnvironmentCreated: """ Creates a new environment ## Args - ``` models.Env JSON ``` + ``` models.Environment JSON ``` """ - with tracer.start_as_current_span("create_env"): - return await EnvController.create_env(env) + with tracer.start_as_current_span("create_environment"): + controller = EnvironmentController() + return await controller.post_environment(environment) -@router.get("/{env_id}") -async def read_env(env_id: str) -> Env: +@router.get("/{environment_id}") +async def read_environment(environment_id: str) -> EnvironmentRetrieved: """ - Reads an environment + Reads an existing environment ## Args - ``` env_id: str ``` + ``` environment_id: str ``` """ - with tracer.start_as_current_span("read_env"): - return await EnvController.get_env_by_id(env_id) + with tracer.start_as_current_span("read_environment"): + controller = EnvironmentController() + return await controller.get_environment_by_id(environment_id) -@router.put("/{env_id}") -async def update_env(env_id: str, env: Env) -> EnvUpdated: +@router.put("/{environment_id}") +async def update_environment(environment_id: str, environment: EnvironmentModel) -> EnvironmentUpdated: """ - Updates an environment + Updates an existing environment ## Args ``` - env_id: str - env: models.Env JSON + environment_id: str + becho: models.Becho JSON ``` """ - with tracer.start_as_current_span("update_env"): - return await EnvController.update_env_by_id(env_id, env) + with tracer.start_as_current_span("update_becho"): + controller = EnvironmentController() + return await controller.put_environment_by_id(environment_id, environment) + + +@router.delete("/{environment_id}") +async def delete_environment(environment_id: str) -> EnvironmentDeleted: + """ + Deletes an existing environment + + ## Args + ``` environment_id: str ``` + """ + with tracer.start_as_current_span("delete_becho"): + controller = EnvironmentController() + return await controller.delete_environment_by_id(environment_id) @router.get( - "/{env_id}/rocketpy", + "/{environment_id}/rocketpy", responses={ 203: { "description": "Binary file download", @@ -77,18 +94,19 @@ async def update_env(env_id: str, env: Env) -> EnvUpdated: status_code=203, response_class=Response, ) -async def read_rocketpy_env(env_id: str): +async def read_rocketpy_env(environment_id: str): """ Loads rocketpy.environment as a dill binary ## Args - ``` env_id: str ``` + ``` environment_id: str ``` """ with tracer.start_as_current_span("read_rocketpy_env"): headers = { - 'Content-Disposition': f'attachment; filename="rocketpy_environment_{env_id}.dill"' + 'Content-Disposition': f'attachment; filename="rocketpy_environment_{environment_id}.dill"' } - binary = await EnvController.get_rocketpy_env_binary(env_id) + controller = EnvironmentController() + binary = await controller.get_rocketpy_environment_binary(environment_id) return Response( content=binary, headers=headers, @@ -97,25 +115,14 @@ async def read_rocketpy_env(env_id: str): ) -@router.get("/{env_id}/summary") -async def simulate_env(env_id: str) -> EnvSummary: +@router.get("/{environment_id}/summary") +async def simulate_env(environment_id: str) -> EnvironmentSummary: """ Loads rocketpy.environment simulation ## Args - ``` env_id: str ``` + ``` environment_id: str ``` """ with tracer.start_as_current_span("simulate_env"): - return await EnvController.simulate_env(env_id) - - -@router.delete("/{env_id}") -async def delete_env(env_id: str) -> EnvDeleted: - """ - Deletes an environment - - ## Args - ``` env_id: str ``` - """ - with tracer.start_as_current_span("delete_env"): - return await EnvController.delete_env_by_id(env_id) + controller = EnvironmentController() + return await controller.get_environment_summary(environment_id) diff --git a/lib/routes/flight.py b/lib/routes/flight.py index e88b87a..1598be1 100644 --- a/lib/routes/flight.py +++ b/lib/routes/flight.py @@ -8,14 +8,14 @@ from lib.views.flight import ( FlightSummary, FlightCreated, + FlightRetrieved, FlightUpdated, FlightDeleted, ) from lib.models.environment import Env -from lib.models.flight import Flight +from lib.models.flight import FlightModel from lib.models.rocket import Rocket from lib.models.motor import MotorKinds -from lib.views.flight import FlightView from lib.controllers.flight import FlightController router = APIRouter( @@ -32,30 +32,60 @@ @router.post("/") -async def create_flight( - flight: Flight, motor_kind: MotorKinds -) -> FlightCreated: +async def create_flight(flight: FlightModel, motor_kind: MotorKinds) -> FlightCreated: """ Creates a new flight ## Args - ``` Flight object as JSON ``` + ``` models.Flight JSON ``` """ with tracer.start_as_current_span("create_flight"): + controller = FlightController() flight.rocket.motor.set_motor_kind(motor_kind) - return await FlightController.create_flight(flight) + return await controller.post_flight(flight) @router.get("/{flight_id}") -async def read_flight(flight_id: str) -> FlightView: +async def read_flight(flight_id: str) -> FlightRetrieved: """ - Reads a flight + Reads an existing flight ## Args - ``` flight_id: Flight ID ``` + ``` flight_id: str ``` """ with tracer.start_as_current_span("read_flight"): - return await FlightController.get_flight_by_id(flight_id) + controller = FlightController() + return await controller.get_flight_by_id(flight_id) + + +@router.put("/{flight_id}") +async def update_flight(flight_id: str, flight: FlightModel, motor_kind: MotorKinds) -> FlightUpdated: + """ + Updates an existing flight + + ## Args + ``` + flight_id: str + flight: models.flight JSON + ``` + """ + with tracer.start_as_current_span("update_flight"): + controller = FlightController() + flight.rocket.motor.set_motor_kind(motor_kind) + return await controller.put_flight_by_id(flight_id, flight) + + +@router.delete("/{flight_id}") +async def delete_flight(flight_id: str) -> FlightDeleted: + """ + Deletes an existing flight + + ## Args + ``` flight_id: str ``` + """ + with tracer.start_as_current_span("delete_flight"): + controller = FlightController() + return await controller.delete_flight_by_id(flight_id) @router.get( @@ -77,10 +107,11 @@ async def read_rocketpy_flight(flight_id: str): ``` flight_id: str ``` """ with tracer.start_as_current_span("read_rocketpy_flight"): + controller = FlightController() headers = { 'Content-Disposition': f'attachment; filename="rocketpy_flight_{flight_id}.dill"' } - binary = await FlightController.get_rocketpy_flight_binary(flight_id) + binary = await controller.get_rocketpy_flight_binary(flight_id) return Response( content=binary, headers=headers, @@ -101,7 +132,8 @@ async def update_flight_env(flight_id: str, env: Env) -> FlightUpdated: ``` """ with tracer.start_as_current_span("update_flight_env"): - return await FlightController.update_env_by_flight_id( + controller = FlightController() + return await controller.update_env_by_flight_id( flight_id, env=env ) @@ -122,33 +154,14 @@ async def update_flight_rocket( ``` """ with tracer.start_as_current_span("update_flight_rocket"): + controller = FlightController() rocket.motor.set_motor_kind(motor_kind) - return await FlightController.update_rocket_by_flight_id( + return await controller.update_rocket_by_flight_id( flight_id, rocket=rocket, ) -@router.put("/{flight_id}") -async def update_flight( - flight_id: str, - flight: Flight, - motor_kind: MotorKinds, -) -> FlightUpdated: - """ - Updates Flight object - - ## Args - ``` - flight_id: Flight ID - flight: Flight object as JSON - ``` - """ - with tracer.start_as_current_span("update_flight"): - flight.rocket.motor.set_motor_kind(motor_kind) - return await FlightController.update_flight_by_id(flight_id, flight) - - @router.get("/{flight_id}/summary") async def simulate_flight(flight_id: str) -> FlightSummary: """ @@ -158,16 +171,5 @@ async def simulate_flight(flight_id: str) -> FlightSummary: ``` flight_id: Flight ID ``` """ with tracer.start_as_current_span("simulate_flight"): - return await FlightController.simulate_flight(flight_id) - - -@router.delete("/{flight_id}") -async def delete_flight(flight_id: str) -> FlightDeleted: - """ - Deletes a flight - - ## Args - ``` flight_id: Flight ID ``` - """ - with tracer.start_as_current_span("delete_flight"): - return await FlightController.delete_flight_by_id(flight_id) + controller = FlightController() + return await controller.simulate_flight(flight_id) diff --git a/lib/routes/motor.py b/lib/routes/motor.py index 5a89da7..ced0169 100644 --- a/lib/routes/motor.py +++ b/lib/routes/motor.py @@ -8,12 +8,12 @@ from lib.views.motor import ( MotorSummary, MotorCreated, + MotorRetrieved, MotorUpdated, MotorDeleted, ) -from lib.models.motor import Motor, MotorKinds +from lib.models.motor import MotorModel, MotorKinds from lib.controllers.motor import MotorController -from lib.views.motor import MotorView router = APIRouter( prefix="/motors", @@ -29,46 +29,60 @@ @router.post("/") -async def create_motor(motor: Motor, motor_kind: MotorKinds) -> MotorCreated: +async def create_motor(motor: MotorModel, motor_kind: MotorKinds) -> MotorCreated: """ Creates a new motor ## Args - ``` Motor object as a JSON ``` + ``` models.Motor JSON ``` """ with tracer.start_as_current_span("create_motor"): + controller = MotorController() motor.set_motor_kind(motor_kind) - return await MotorController.create_motor(motor) + return await controller.post_motor(motor) @router.get("/{motor_id}") -async def read_motor(motor_id: str) -> MotorView: +async def read_motor(motor_id: str) -> MotorRetrieved: """ - Reads a motor + Reads an existing motor ## Args - ``` motor_id: Motor ID ``` + ``` motor_id: str ``` """ with tracer.start_as_current_span("read_motor"): - return await MotorController.get_motor_by_id(motor_id) + controller = MotorController() + return await controller.get_motor_by_id(motor_id) @router.put("/{motor_id}") -async def update_motor( - motor_id: str, motor: Motor, motor_kind: MotorKinds -) -> MotorUpdated: +async def update_motor(motor_id: str, motor: MotorModel, motor_kind: MotorKinds) -> MotorUpdated: """ - Updates a motor + Updates an existing motor ## Args ``` - motor_id: Motor ID - motor: Motor object as JSON + motor_id: str + motor: models.motor JSON ``` """ with tracer.start_as_current_span("update_motor"): + controller = MotorController() motor.set_motor_kind(motor_kind) - return await MotorController.update_motor_by_id(motor_id, motor) + return await controller.put_motor_by_id(motor_id, motor) + + +@router.delete("/{motor_id}") +async def delete_motor(motor_id: str) -> MotorDeleted: + """ + Deletes an existing motor + + ## Args + ``` motor_id: str ``` + """ + with tracer.start_as_current_span("delete_motor"): + controller = MotorController() + return await controller.delete_motor_by_id(motor_id) @router.get( @@ -93,7 +107,8 @@ async def read_rocketpy_motor(motor_id: str): headers = { 'Content-Disposition': f'attachment; filename="rocketpy_motor_{motor_id}.dill"' } - binary = await MotorController.get_rocketpy_motor_binary(motor_id) + controller = MotorController() + binary = await controller.get_rocketpy_motor_binary(motor_id) return Response( content=binary, headers=headers, @@ -111,16 +126,5 @@ async def simulate_motor(motor_id: str) -> MotorSummary: ``` motor_id: Motor ID ``` """ with tracer.start_as_current_span("simulate_motor"): - return await MotorController.simulate_motor(motor_id) - - -@router.delete("/{motor_id}") -async def delete_motor(motor_id: str) -> MotorDeleted: - """ - Deletes a motor - - ## Args - ``` motor_id: Motor ID ``` - """ - with tracer.start_as_current_span("delete_motor"): - return await MotorController.delete_motor_by_id(motor_id) + controller = MotorController() + return await controller.simulate_motor(motor_id) diff --git a/lib/routes/rocket.py b/lib/routes/rocket.py index f847754..711582b 100644 --- a/lib/routes/rocket.py +++ b/lib/routes/rocket.py @@ -8,12 +8,12 @@ from lib.views.rocket import ( RocketSummary, RocketCreated, + RocketRetrieved, RocketUpdated, RocketDeleted, ) -from lib.models.rocket import Rocket +from lib.models.rocket import RocketModel from lib.models.motor import MotorKinds -from lib.views.rocket import RocketView from lib.controllers.rocket import RocketController router = APIRouter( @@ -30,50 +30,60 @@ @router.post("/") -async def create_rocket( - rocket: Rocket, motor_kind: MotorKinds -) -> RocketCreated: +async def create_rocket(rocket: RocketModel, motor_kind: MotorKinds) -> RocketCreated: """ Creates a new rocket ## Args - ``` Rocket object as a JSON ``` + ``` models.Rocket JSON ``` """ with tracer.start_as_current_span("create_rocket"): + controller = RocketController() rocket.motor.set_motor_kind(motor_kind) - return await RocketController.create_rocket(rocket) + return await controller.post_rocket(rocket) @router.get("/{rocket_id}") -async def read_rocket(rocket_id: str) -> RocketView: +async def read_rocket(rocket_id: str) -> RocketRetrieved: """ - Reads a rocket + Reads an existing rocket ## Args - ``` rocket_id: Rocket ID ``` + ``` rocket_id: str ``` """ with tracer.start_as_current_span("read_rocket"): - return await RocketController.get_rocket_by_id(rocket_id) + controller = RocketController() + return await controller.get_rocket_by_id(rocket_id) @router.put("/{rocket_id}") -async def update_rocket( - rocket_id: str, - rocket: Rocket, - motor_kind: MotorKinds, -) -> RocketUpdated: +async def update_rocket(rocket_id: str, rocket: RocketModel, motor_kind: MotorKinds) -> RocketUpdated: """ - Updates a rocket + Updates an existing rocket ## Args ``` - rocket_id: Rocket ID - rocket: Rocket object as JSON + rocket_id: str + rocket: models.rocket JSON ``` """ with tracer.start_as_current_span("update_rocket"): + controller = RocketController() rocket.motor.set_motor_kind(motor_kind) - return await RocketController.update_rocket_by_id(rocket_id, rocket) + return await controller.put_rocket_by_id(rocket_id, rocket) + + +@router.delete("/{rocket_id}") +async def delete_rocket(rocket_id: str) -> RocketDeleted: + """ + Deletes an existing rocket + + ## Args + ``` rocket_id: str ``` + """ + with tracer.start_as_current_span("delete_rocket"): + controller = RocketController() + return await controller.delete_rocket_by_id(rocket_id) @router.get( @@ -98,7 +108,8 @@ async def read_rocketpy_rocket(rocket_id: str): headers = { 'Content-Disposition': f'attachment; filename="rocketpy_rocket_{rocket_id}.dill"' } - binary = await RocketController.get_rocketpy_rocket_binary(rocket_id) + controller = RocketController() + binary = await controller.get_rocketpy_rocket_binary(rocket_id) return Response( content=binary, headers=headers, @@ -116,16 +127,5 @@ async def simulate_rocket(rocket_id: str) -> RocketSummary: ``` rocket_id: Rocket ID ``` """ with tracer.start_as_current_span("simulate_rocket"): - return await RocketController.simulate_rocket(rocket_id) - - -@router.delete("/{rocket_id}") -async def delete_rocket(rocket_id: str) -> RocketDeleted: - """ - Deletes a rocket - - ## Args - ``` rocket_id: Rocket ID ``` - """ - with tracer.start_as_current_span("delete_rocket"): - return await RocketController.delete_rocket_by_id(rocket_id) + controller = RocketController() + return await controller.simulate_rocket(rocket_id) diff --git a/lib/services/rocket.py b/lib/services/rocket.py index 85db0e8..d26812d 100644 --- a/lib/services/rocket.py +++ b/lib/services/rocket.py @@ -15,7 +15,7 @@ from lib import logger from lib.models.rocket import Parachute -from lib.models.aerosurfaces import NoseCone, Tail, Fins +from lib.models.sub.aerosurfaces import NoseCone, Tail, Fins from lib.services.motor import MotorService from lib.views.rocket import RocketView, RocketSummary diff --git a/lib/views/environment.py b/lib/views/environment.py index e3fcd31..4e9ffd5 100644 --- a/lib/views/environment.py +++ b/lib/views/environment.py @@ -1,11 +1,11 @@ from typing import Optional, Any from datetime import datetime, timedelta -from pydantic import BaseModel, ConfigDict -from lib.models.environment import AtmosphericModelTypes +from pydantic import ApiBaseView, ConfigDict +from lib.models.environment import AtmosphericModelTypes, EnvironmentModel from lib.utils import to_python_primitive -class EnvSummary(BaseModel): +class EnvironmentSummary(ApiBaseView): latitude: Optional[float] = None longitude: Optional[float] = None elevation: Optional[float] = 1 @@ -52,16 +52,23 @@ class EnvSummary(BaseModel): model_config = ConfigDict(json_encoders={Any: to_python_primitive}) -class EnvCreated(BaseModel): - env_id: str +class EnvironmentView(EnvironmentModel): + environment_id: Optional[str] = None + + +class EnvironmentCreated(ApiBaseView): message: str = "Environment successfully created" + environment_id: str + + +class EnvironmentRetrieved(ApiBaseView): + message: str = "Environment successfully retrieved" + environment: EnvironmentView -class EnvUpdated(BaseModel): - env_id: str +class EnvironmentUpdated(ApiBaseView): message: str = "Environment successfully updated" -class EnvDeleted(BaseModel): - env_id: str +class EnvironmentDeleted(ApiBaseView): message: str = "Environment successfully deleted" diff --git a/lib/views/flight.py b/lib/views/flight.py index f06cae4..3df3cba 100644 --- a/lib/views/flight.py +++ b/lib/views/flight.py @@ -1,6 +1,7 @@ from typing import Optional, Any -from pydantic import BaseModel, ConfigDict -from lib.models.flight import Flight +from pydantic import ConfigDict +from lib.models.flight import FlightModel +from lib.views.interface import ApiBaseView from lib.views.rocket import RocketView, RocketSummary from lib.views.environment import EnvSummary from lib.utils import to_python_primitive @@ -154,20 +155,24 @@ class FlightSummary(RocketSummary, EnvSummary): model_config = ConfigDict(json_encoders={Any: to_python_primitive}) -class FlightCreated(BaseModel): +class FlightView(FlightModel): flight_id: str - message: str = "Flight successfully created" + rocket: RocketView -class FlightUpdated(BaseModel): +class FlightCreated(ApiBaseView): + message: str = "Flight successfully created" flight_id: str - message: str = "Flight successfully updated" -class FlightDeleted(BaseModel): - flight_id: str - message: str = "Flight successfully deleted" +class FlightRetrieved(ApiBaseView): + message: str = "Flight successfully retrieved" + flight: FlightView -class FlightView(Flight): - rocket: RocketView +class FlightUpdated(ApiBaseView): + message: str = "Flight successfully updated" + + +class FlightDeleted(ApiBaseView): + message: str = "Flight successfully deleted" diff --git a/lib/views/interface.py b/lib/views/interface.py new file mode 100644 index 0000000..ef58c5a --- /dev/null +++ b/lib/views/interface.py @@ -0,0 +1,5 @@ +from pydantic import BaseModel + + +class ApiBaseView(BaseModel): + message: str = 'View not implemented' diff --git a/lib/views/motor.py b/lib/views/motor.py index 858bc0e..dfc88aa 100644 --- a/lib/views/motor.py +++ b/lib/views/motor.py @@ -1,6 +1,7 @@ from typing import List, Any, Optional from pydantic import BaseModel, ConfigDict -from lib.models.motor import Motor, MotorKinds, CoordinateSystemOrientation +from lib.views.interface import ApiBaseView +from lib.models.sub.motor import MotorModel, MotorKinds, MotorCoordinateSystemOrientation from lib.utils import to_python_primitive @@ -11,7 +12,7 @@ class MotorSummary(BaseModel): burn_start_time: Optional[float] = None center_of_dry_mass_position: Optional[float] = None coordinate_system_orientation: str = ( - CoordinateSystemOrientation.NOZZLE_TO_COMBUSTION_CHAMBER.value + MotorCoordinateSystemOrientation.NOZZLE_TO_COMBUSTION_CHAMBER.value ) dry_I_11: Optional[float] = None dry_I_12: Optional[float] = None @@ -72,20 +73,24 @@ class MotorSummary(BaseModel): model_config = ConfigDict(json_encoders={Any: to_python_primitive}) -class MotorCreated(BaseModel): - motor_id: str - message: str = "Motor successfully created" +class MotorView(MotorModel): + motor_id: Optional[str] = None + selected_motor_kind: MotorKinds -class MotorUpdated(BaseModel): +class MotorCreated(ApiBaseView): + message: str = "Motor successfully created" motor_id: str - message: str = "Motor successfully updated" -class MotorDeleted(BaseModel): - motor_id: str - message: str = "Motor successfully deleted" +class MotorRetrieved(ApiBaseView): + message: str = "Motor successfully retrieved" + motor: MotorView -class MotorView(Motor): - selected_motor_kind: MotorKinds +class MotorUpdated(ApiBaseView): + message: str = "Motor successfully updated" + + +class MotorDeleted(ApiBaseView): + message: str = "Motor successfully deleted" diff --git a/lib/views/rocket.py b/lib/views/rocket.py index 45664ad..bb85104 100644 --- a/lib/views/rocket.py +++ b/lib/views/rocket.py @@ -1,6 +1,7 @@ from typing import Any, Optional -from pydantic import BaseModel, ConfigDict -from lib.models.rocket import Rocket, CoordinateSystemOrientation +from pydantic import ConfigDict +from lib.models.rocket import RocketModel, RocketCoordinateSystemOrientation +from lib.views.interface import ApiBaseView from lib.views.motor import MotorView, MotorSummary from lib.utils import to_python_primitive @@ -8,7 +9,7 @@ class RocketSummary(MotorSummary): area: Optional[float] = None coordinate_system_orientation: str = ( - CoordinateSystemOrientation.TAIL_TO_NOSE.value + RocketCoordinateSystemOrientation.TAIL_TO_NOSE.value ) center_of_mass_without_motor: Optional[float] = None motor_center_of_dry_mass_position: Optional[float] = None @@ -41,20 +42,24 @@ class RocketSummary(MotorSummary): model_config = ConfigDict(json_encoders={Any: to_python_primitive}) -class RocketCreated(BaseModel): - rocket_id: str - message: str = "Rocket successfully created" +class RocketView(RocketModel): + rocket_id: Optional[str] = None + motor: MotorView -class RocketUpdated(BaseModel): +class RocketCreated(ApiBaseView): + message: str = "Rocket successfully created" rocket_id: str - message: str = "Rocket successfully updated" -class RocketDeleted(BaseModel): - rocket_id: str - message: str = "Rocket successfully deleted" +class RocketRetrieved(ApiBaseView): + message: str = "Rocket successfully retrieved" + rocket: RocketView -class RocketView(Rocket): - motor: MotorView +class RocketUpdated(ApiBaseView): + message: str = "Rocket successfully updated" + + +class RocketDeleted(ApiBaseView): + message: str = "Rocket successfully deleted" diff --git a/tests/test_controllers/test_controller_interface.py b/tests/test_controllers/test_controller_interface.py new file mode 100644 index 0000000..e430e04 --- /dev/null +++ b/tests/test_controllers/test_controller_interface.py @@ -0,0 +1,225 @@ +from unittest.mock import patch, Mock +import pytest +from pymongo.errors import PyMongoError +from fastapi import HTTPException, status +from lib.controllers.interface import ( + ControllerInterface, + controller_exception_handler, +) + + +@pytest.fixture +def stub_model(): + model = Mock() + model.NAME = 'test_model' + model.METHODS = ('GET', 'POST', 'PUT', 'DELETE') + model.UPDATED = lambda: 'Updated' + model.DELETED = lambda: 'Deleted' + model.CREATED = lambda arg: 'Created' + model.RETRIEVED = lambda arg: 'Retrieved' + return model + + +@pytest.fixture +def stub_controller(stub_model): + return ControllerInterface([stub_model]) + + +@pytest.mark.asyncio +async def test_controller_exception_handler_no_exception(stub_model): + async def method(self, model, *args, **kwargs): + return stub_model, args, kwargs + + test_controller = Mock(method=method) + method = test_controller.method + mock_kwargs = {'foo': 'bar'} + mock_args = ('foo', 'bar') + wrapped_method = controller_exception_handler(method) + with patch('lib.controllers.interface.logger') as mock_logger: + assert await wrapped_method( + test_controller, stub_model, *mock_args, **mock_kwargs + ) == (stub_model, mock_args, mock_kwargs) + mock_logger.error.assert_not_called() + mock_logger.info.assert_called_once_with( + "Call to method completed for Mock" + ) + + +@pytest.mark.asyncio +async def test_controller_exception_handler_db_exception(stub_model): + async def method(self, model, *args, **kwargs): + raise PyMongoError + + wrapped_method = controller_exception_handler(method) + with patch('lib.controllers.interface.logger') as mock_logger: + with pytest.raises(HTTPException) as exc: + await wrapped_method(None, stub_model) + assert exc.value.status_code == status.HTTP_503_SERVICE_UNAVAILABLE + assert exc.value.detail == "Operation failed, please try again later" + mock_logger.error.assert_called_once_with( + f"{method.__name__}: PyMongoError" + ) + + +@pytest.mark.asyncio +async def test_controller_exception_handler_http_exception(stub_model): + async def method(self, model, *args, **kwargs): + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail='Not Found' + ) + + wrapped_method = controller_exception_handler(method) + with patch('lib.controllers.interface.logger') as mock_logger: + with pytest.raises(HTTPException) as exc: + await wrapped_method(None, stub_model) + assert exc.value.status_code == status.HTTP_404_NOT_FOUND + assert exc.value.detail == 'Not Found' + mock_logger.error.assert_not_called() + + +@pytest.mark.asyncio +async def test_controller_exception_handler_unexpected_exception(stub_model): + async def method(self, model, *args, **kwargs): + raise ValueError('Test Error') + + wrapped_method = controller_exception_handler(method) + with patch('lib.controllers.interface.logger') as mock_logger: + with pytest.raises(HTTPException) as exc: + await wrapped_method(None, stub_model) + assert exc.value.status_code == status.HTTP_500_INTERNAL_SERVER_ERROR + assert exc.value.detail == 'Unexpected error' + mock_logger.exception.assert_called_once_with( + f"{method.__name__}: Unexpected error Test Error" + ) + + +def test_controller_interface_init(stub_model): + with patch( + 'lib.controllers.interface.ControllerInterface._generate_method' + ) as mock_gen: + mock_gen.return_value = lambda *args, **kwargs: True + stub_controller = ControllerInterface([stub_model]) + assert stub_controller._initialized_models == { + 'test_model': stub_model + } + assert hasattr(stub_controller, 'post_test_model') + assert hasattr(stub_controller, 'get_test_model_by_id') + assert hasattr(stub_controller, 'put_test_model_by_id') + assert hasattr(stub_controller, 'delete_test_model_by_id') + assert getattr(stub_controller, 'post_test_model')() is True + assert getattr(stub_controller, 'get_test_model_by_id')() is True + assert getattr(stub_controller, 'put_test_model_by_id')() is True + assert getattr(stub_controller, 'delete_test_model_by_id')() is True + + +@pytest.mark.asyncio +async def test_controller_interface_generate_available_method( + stub_controller, stub_model +): + with patch('lib.controllers.interface.RepositoryInterface') as mock_repo: + with patch( + 'lib.controllers.interface.ControllerInterface._get_model' + ) as mock_get: + mock_get.return_value = stub_model + method = stub_controller._generate_method('get', stub_model) + assert ( + await method(None, stub_model, mock_repo, 'arg', key='bar') + == stub_model + ) + + +@pytest.mark.asyncio +async def test_controller_interface_generate_unavailable_method( + stub_controller, stub_model +): + with patch('lib.controllers.interface.RepositoryInterface') as mock_repo: + with pytest.raises(NotImplementedError): + method = stub_controller._generate_method('foo', stub_model) + await method(None, stub_model, mock_repo, 'arg', key='bar') + + +@pytest.mark.asyncio +async def test_controller_interface_post_model(stub_controller, stub_model): + with patch('lib.controllers.interface.RepositoryInterface') as mock_repo: + with patch( + 'lib.controllers.interface.RepositoryInterface.get_model_repo' + ) as mock_get_repo: + mock_get_repo.return_value = mock_repo + assert ( + await stub_controller._post_model( + stub_model, mock_repo, stub_model + ) + == 'Created' + ) + + +@pytest.mark.asyncio +async def test_controller_interface_get_model_found( + stub_controller, stub_model +): + with patch('lib.controllers.interface.RepositoryInterface') as mock_repo: + with patch( + 'lib.controllers.interface.RepositoryInterface.get_model_repo' + ) as mock_get_repo: + mock_get_repo.return_value = mock_repo + mock_repo.return_value.__aenter__.return_value.read_test_model_by_id.return_value = ( + stub_model + ) + assert ( + await stub_controller._get_model(stub_model, mock_repo, '123') + == 'Retrieved' + ) + + +@pytest.mark.asyncio +async def test_controller_interface_get_model_not_found( + stub_controller, stub_model +): + with patch('lib.controllers.interface.RepositoryInterface') as mock_repo: + with patch( + 'lib.controllers.interface.RepositoryInterface.get_model_repo' + ) as mock_get_repo: + mock_get_repo.return_value = mock_repo + mock_repo.return_value.__aenter__.return_value.read_test_model_by_id.return_value = ( + None + ) + with pytest.raises(HTTPException) as exc: + await stub_controller._get_model(stub_model, mock_repo, '123') + assert exc.value.status_code == status.HTTP_404_NOT_FOUND + assert exc.value.detail == 'test_model not found' + + +@pytest.mark.asyncio +async def test_controller_interface_update_model(stub_controller, stub_model): + with patch('lib.controllers.interface.RepositoryInterface') as mock_repo: + with patch( + 'lib.controllers.interface.RepositoryInterface.get_model_repo' + ) as mock_get_repo: + mock_get_repo.return_value = mock_repo + mock_repo.return_value.__aenter__.return_value.update_test_model_by_id.return_value = ( + None + ) + assert ( + await stub_controller._put_model( + stub_model, mock_repo, '123', stub_model + ) + == 'Updated' + ) + + +@pytest.mark.asyncio +async def test_controller_interface_delete_model(stub_controller, stub_model): + with patch('lib.controllers.interface.RepositoryInterface') as mock_repo: + with patch( + 'lib.controllers.interface.RepositoryInterface.get_model_repo' + ) as mock_get_repo: + mock_get_repo.return_value = mock_repo + mock_repo.return_value.__aenter__.return_value.delete_test_model_by_id.return_value = ( + None + ) + assert ( + await stub_controller._delete_model( + stub_model, mock_repo, '123' + ) + == 'Deleted' + ) diff --git a/tests/test_repositories/test_repository_interface.py b/tests/test_repositories/test_repository_interface.py new file mode 100644 index 0000000..af936b5 --- /dev/null +++ b/tests/test_repositories/test_repository_interface.py @@ -0,0 +1,286 @@ +from unittest.mock import patch, Mock, AsyncMock +import pytest +import pytest_asyncio +from pymongo.errors import PyMongoError +from fastapi import HTTPException, status +from lib.repositories.interface import ( + RepositoryInterface, + repository_exception_handler, + RepositoryNotInitializedException, +) + + +@pytest.fixture +def stub_loaded_model(): + return {'_id': 'mock_id'} + + +@pytest.fixture +def mock_model(): + return Mock(set_id=Mock()) + + +@pytest_asyncio.fixture +def mock_db_interface(stub_loaded_model): + async def async_gen(*args, **kwargs): + yield stub_loaded_model + yield stub_loaded_model + + mock_db_interface = AsyncMock() + mock_db_interface.find = Mock(return_value=async_gen()) + mock_db_interface.insert_one = AsyncMock( + return_value=Mock(inserted_id='mock_id') + ) + mock_db_interface.update_one = AsyncMock(return_value=None) + mock_db_interface.find_one = AsyncMock(return_value=stub_loaded_model) + mock_db_interface.delete_one = AsyncMock(return_value=None) + return mock_db_interface + + +@pytest_asyncio.fixture +def mock_db_interface_empty_find(): + async def async_gen(*args, **kwargs): + if False: # pylint: disable=using-constant-test + yield + + mock_db_interface = AsyncMock() + mock_db_interface.find = Mock(return_value=async_gen()) + return mock_db_interface + + +@pytest_asyncio.fixture +def stub_repository(mock_model): + with patch.object(RepositoryInterface, "_initialize", return_value=None): + stub_model = Mock( + NAME='mock_model', + model_validate=mock_model, + ) + repo = RepositoryInterface(stub_model) + repo._initialized = True + yield repo + + +@pytest_asyncio.fixture +def stub_repository_invalid_model(): + with patch.object(RepositoryInterface, "_initialize", return_value=None): + stub_model = Mock( + NAME='mock_model', + model_validate=Mock(return_value=False), + ) + repo = RepositoryInterface(stub_model) + repo._initialized = True + yield repo + + +@pytest.mark.asyncio +async def test_repository_exception_handler_no_exception(): + async def method(self, *args, **kwargs): + return args, kwargs + + mock_kwargs = {'foo': 'bar'} + mock_args = ('foo', 'bar') + mock_repo = Mock(model=Mock(NAME='mock_model')) + wrapped_method = repository_exception_handler(method) + with patch('lib.repositories.interface.logger') as mock_logger: + assert await wrapped_method(mock_repo, *mock_args, **mock_kwargs) == ( + mock_args, + mock_kwargs, + ) + mock_logger.error.assert_not_called() + mock_logger.info.assert_called_once_with( + f"Call to repositories.{mock_repo.model.NAME}.{method.__name__} completed for {mock_kwargs}" + ) + + +@pytest.mark.asyncio +async def test_repository_exception_handler_db_exception(): + async def method(self, *args, **kwargs): + raise PyMongoError + + wrapped_method = repository_exception_handler(method) + with pytest.raises(PyMongoError): + await wrapped_method(Mock(model=Mock(NAME='mock_model'))) + + +@pytest.mark.asyncio +async def test_repository_exception_not_initialized_exception(): + async def method(self, *args, **kwargs): + raise RepositoryNotInitializedException + + wrapped_method = repository_exception_handler(method) + with pytest.raises(HTTPException) as exc: + await wrapped_method(Mock(model=Mock(NAME='mock_model'))) + assert exc.value.status_code == status.HTTP_503_SERVICE_UNAVAILABLE + assert ( + exc.value.detail + == "Repository not initialized. Please try again later." + ) + + +@pytest.mark.asyncio +async def test_repository_exception_handler_unexpected_exception(): + async def method(self, *args, **kwargs): + raise Exception # pylint: disable=broad-exception-raised + + wrapped_method = repository_exception_handler(method) + with pytest.raises(HTTPException) as exc: + await wrapped_method(Mock(model=Mock(NAME='mock_model'))) + assert exc.value.status_code == status.HTTP_500_INTERNAL_SERVER_ERROR + assert exc.value.detail == 'Unexpected error ocurred' + + +@pytest.mark.asyncio +async def test_repository_interface_init(stub_repository): + assert stub_repository.model.NAME == 'mock_model' + + +def test_get_model_repo(stub_repository): + with patch( + 'lib.repositories.interface.importlib.import_module' + ) as mock_import_module: + mock_import_module.return_value = Mock(MockmodelRepository='mock_repo') + assert ( + stub_repository.get_model_repo(Mock(NAME='mockmodel')) + == 'mock_repo' + ) + + +@pytest.mark.asyncio +async def test_repository_insert_data(stub_repository, mock_db_interface): + with patch( + 'lib.repositories.interface.RepositoryInterface.get_collection', + return_value=mock_db_interface, + ): + assert await stub_repository.insert('mock_data') == 'mock_id' + mock_db_interface.insert_one.assert_called_once_with('mock_data') + + +@pytest.mark.asyncio +async def test_repository_insert_invalid_data(stub_repository_invalid_model): + with patch( + 'lib.repositories.interface.RepositoryInterface.get_collection', + return_value=mock_db_interface, + ): + with pytest.raises(HTTPException): + await stub_repository_invalid_model.insert('invalid_model_data') + + +@pytest.mark.asyncio +async def test_repository_update_data(stub_repository, mock_db_interface): + with patch( + 'lib.repositories.interface.RepositoryInterface.get_collection', + return_value=mock_db_interface, + ): + with patch( + 'lib.repositories.interface.ObjectId', side_effect=lambda x: x + ): + assert ( + await stub_repository.update_by_id( + 'mock_data', data_id='mock_id' + ) + is stub_repository + ) + mock_db_interface.update_one.assert_called_once_with( + {'_id': 'mock_id'}, {'$set': 'mock_data'} + ) + + +@pytest.mark.asyncio +async def test_repository_update_invalid_data(stub_repository_invalid_model): + with patch( + 'lib.repositories.interface.RepositoryInterface.get_collection', + return_value=mock_db_interface, + ): + with pytest.raises(HTTPException): + await stub_repository_invalid_model.update_by_id( + data_id='mock_id', data='invalid_model_data' + ) + + +@pytest.mark.asyncio +async def test_repository_find_data_found( + stub_repository, mock_db_interface, stub_loaded_model +): + with patch( + 'lib.repositories.interface.RepositoryInterface.get_collection', + return_value=mock_db_interface, + ): + with patch( + 'lib.repositories.interface.ObjectId', side_effect=lambda x: x + ): + assert ( + await stub_repository.find_by_id(data_id='mock_id') + == stub_repository.model.model_validate.return_value + ) + mock_db_interface.find_one.assert_called_once_with( + {'_id': 'mock_id'} + ) + stub_repository.model.model_validate.return_value.set_id.assert_called_once_with( + stub_loaded_model['_id'] + ) + + +@pytest.mark.asyncio +async def test_repository_find_data_not_found( + stub_repository, mock_db_interface +): + mock_db_interface.find_one.return_value = None + with patch( + 'lib.repositories.interface.RepositoryInterface.get_collection', + return_value=mock_db_interface, + ): + with patch( + 'lib.repositories.interface.ObjectId', side_effect=lambda x: x + ): + assert await stub_repository.find_by_id(data_id='mock_id') is None + mock_db_interface.find_one.assert_called_once_with( + {'_id': 'mock_id'} + ) + + +@pytest.mark.asyncio +async def test_repository_delete_data(stub_repository, mock_db_interface): + with patch( + 'lib.repositories.interface.RepositoryInterface.get_collection', + return_value=mock_db_interface, + ): + with patch( + 'lib.repositories.interface.ObjectId', side_effect=lambda x: x + ): + assert ( + await stub_repository.delete_by_id(data_id='mock_id') + is stub_repository + ) + mock_db_interface.delete_one.assert_called_once_with( + {'_id': 'mock_id'} + ) + + +@pytest.mark.asyncio +async def test_repository_find_by_query_found( + stub_repository, mock_db_interface, stub_loaded_model +): + with patch( + 'lib.repositories.interface.RepositoryInterface.get_collection', + return_value=mock_db_interface, + ): + assert await stub_repository.find_by_query('mock_query') == [ + stub_repository.model.model_validate.return_value, + stub_repository.model.model_validate.return_value, + ] + mock_db_interface.find.assert_called_once_with('mock_query') + stub_repository.model.model_validate.return_value.set_id.assert_called_with( + stub_loaded_model['_id'] + ) + + +@pytest.mark.asyncio +async def test_repository_find_by_query_not_found( + stub_repository, mock_db_interface_empty_find +): + with patch( + 'lib.repositories.interface.RepositoryInterface.get_collection', + return_value=mock_db_interface_empty_find, + ): + assert await stub_repository.find_by_query('mock_query') == [] + mock_db_interface_empty_find.find.assert_called_once_with('mock_query') From da1e16dbb3a81353a43d91ffa6f4b323b65a791e Mon Sep 17 00:00:00 2001 From: Gabriel Barberini Date: Sun, 9 Feb 2025 13:52:30 -0300 Subject: [PATCH 02/34] Fixes runtime errors --- lib/controllers/environment.py | 6 +++--- lib/models/motor.py | 10 +++++++++- lib/models/sub/tanks.py | 7 ------- lib/routes/flight.py | 10 +++++----- lib/services/environment.py | 12 ++++++------ lib/views/environment.py | 3 ++- lib/views/flight.py | 4 ++-- lib/views/motor.py | 5 +++-- tests/test_routes/test_environments_route.py | 6 +++--- 9 files changed, 33 insertions(+), 30 deletions(-) diff --git a/lib/controllers/environment.py b/lib/controllers/environment.py index 7169d53..dcf56d6 100644 --- a/lib/controllers/environment.py +++ b/lib/controllers/environment.py @@ -2,7 +2,7 @@ ControllerInterface, controller_exception_handler, ) -from lib.views.environment import EnvSummary +from lib.views.environment import EnvironmentSummary from lib.models.environment import EnvironmentModel from lib.services.environment import EnvironmentService @@ -43,7 +43,7 @@ async def get_rocketpy_env_binary( @controller_exception_handler async def simulate_env( self, env_id: str - ) -> EnvSummary: + ) -> EnvironmentSummary: """ Simulate a rocket environment. @@ -51,7 +51,7 @@ async def simulate_env( env_id: str. Returns: - EnvSummary + EnvironmentSummary Raises: HTTP 404 Not Found: If the env does not exist in the database. diff --git a/lib/models/motor.py b/lib/models/motor.py index cb18603..c4d156e 100644 --- a/lib/models/motor.py +++ b/lib/models/motor.py @@ -1,8 +1,16 @@ +from enum import Enum from typing import Optional, Tuple, List, Union, Self, ClassVar from pydantic import PrivateAttr, model_validator from lib.models.interface import ApiBaseModel -from lib.models.sub.tanks import MotorTank, TankFluids, TankKinds, InterpolationMethods, TankCoordinateSystemOrientation, MotorKinds, MotorTank +from lib.models.sub.tanks import MotorTank, TankFluids, TankKinds, InterpolationMethods, TankCoordinateSystemOrientation, MotorTank + + +class MotorKinds(str, Enum): + HYBRID: str = "HYBRID" + SOLID: str = "SOLID" + GENERIC: str = "GENERIC" + LIQUID: str = "LIQUID" class MotorModel(ApiBaseModel): diff --git a/lib/models/sub/tanks.py b/lib/models/sub/tanks.py index abe8f48..8f0c137 100644 --- a/lib/models/sub/tanks.py +++ b/lib/models/sub/tanks.py @@ -3,13 +3,6 @@ from pydantic import BaseModel -class MotorKinds(str, Enum): - HYBRID: str = "HYBRID" - SOLID: str = "SOLID" - GENERIC: str = "GENERIC" - LIQUID: str = "LIQUID" - - class TankKinds(str, Enum): LEVEL: str = "LEVEL" MASS: str = "MASS" diff --git a/lib/routes/flight.py b/lib/routes/flight.py index 1598be1..2e71499 100644 --- a/lib/routes/flight.py +++ b/lib/routes/flight.py @@ -12,9 +12,9 @@ FlightUpdated, FlightDeleted, ) -from lib.models.environment import Env +from lib.models.environment import EnvironmentModel from lib.models.flight import FlightModel -from lib.models.rocket import Rocket +from lib.models.rocket import RocketModel from lib.models.motor import MotorKinds from lib.controllers.flight import FlightController @@ -121,7 +121,7 @@ async def read_rocketpy_flight(flight_id: str): @router.put("/{flight_id}/env") -async def update_flight_env(flight_id: str, env: Env) -> FlightUpdated: +async def update_flight_env(flight_id: str, env: EnvironmentModel) -> FlightUpdated: """ Updates flight environment @@ -141,7 +141,7 @@ async def update_flight_env(flight_id: str, env: Env) -> FlightUpdated: @router.put("/{flight_id}/rocket") async def update_flight_rocket( flight_id: str, - rocket: Rocket, + rocket: RocketModel, motor_kind: MotorKinds, ) -> FlightUpdated: """ @@ -150,7 +150,7 @@ async def update_flight_rocket( ## Args ``` flight_id: Flight ID - rocket: Rocket object as JSON + rocket: RocketModel object as JSON ``` """ with tracer.start_as_current_span("update_flight_rocket"): diff --git a/lib/services/environment.py b/lib/services/environment.py index f98ea84..e6ec778 100644 --- a/lib/services/environment.py +++ b/lib/services/environment.py @@ -4,8 +4,8 @@ from rocketpy.environment.environment import Environment as RocketPyEnvironment from rocketpy.utilities import get_instance_attributes -from lib.models.environment import Env -from lib.views.environment import EnvSummary +from lib.models.environment import EnvironmentModel +from lib.views.environment import EnvironmentSummary class EnvironmentService: @@ -15,7 +15,7 @@ def __init__(self, environment: RocketPyEnvironment = None): self._environment = environment @classmethod - def from_env_model(cls, env: Env) -> Self: + def from_env_model(cls, env: EnvironmentModel) -> Self: """ Get the rocketpy env object. @@ -42,16 +42,16 @@ def environment(self) -> RocketPyEnvironment: def environment(self, environment: RocketPyEnvironment): self._environment = environment - def get_env_summary(self) -> EnvSummary: + def get_env_summary(self) -> EnvironmentSummary: """ Get the summary of the environment. Returns: - EnvSummary + EnvironmentSummary """ attributes = get_instance_attributes(self.environment) - env_summary = EnvSummary(**attributes) + env_summary = EnvironmentSummary(**attributes) return env_summary def get_env_binary(self) -> bytes: diff --git a/lib/views/environment.py b/lib/views/environment.py index 4e9ffd5..60af42f 100644 --- a/lib/views/environment.py +++ b/lib/views/environment.py @@ -1,6 +1,7 @@ from typing import Optional, Any from datetime import datetime, timedelta -from pydantic import ApiBaseView, ConfigDict +from pydantic import ConfigDict +from lib.views.interface import ApiBaseView from lib.models.environment import AtmosphericModelTypes, EnvironmentModel from lib.utils import to_python_primitive diff --git a/lib/views/flight.py b/lib/views/flight.py index 3df3cba..203545c 100644 --- a/lib/views/flight.py +++ b/lib/views/flight.py @@ -3,11 +3,11 @@ from lib.models.flight import FlightModel from lib.views.interface import ApiBaseView from lib.views.rocket import RocketView, RocketSummary -from lib.views.environment import EnvSummary +from lib.views.environment import EnvironmentSummary from lib.utils import to_python_primitive -class FlightSummary(RocketSummary, EnvSummary): +class FlightSummary(RocketSummary, EnvironmentSummary): name: Optional[str] = None max_time: Optional[int] = None min_time_step: Optional[int] = None diff --git a/lib/views/motor.py b/lib/views/motor.py index dfc88aa..1c663ef 100644 --- a/lib/views/motor.py +++ b/lib/views/motor.py @@ -1,7 +1,8 @@ from typing import List, Any, Optional from pydantic import BaseModel, ConfigDict from lib.views.interface import ApiBaseView -from lib.models.sub.motor import MotorModel, MotorKinds, MotorCoordinateSystemOrientation +from lib.models.sub.tanks import TankCoordinateSystemOrientation +from lib.models.motor import MotorModel, MotorKinds from lib.utils import to_python_primitive @@ -12,7 +13,7 @@ class MotorSummary(BaseModel): burn_start_time: Optional[float] = None center_of_dry_mass_position: Optional[float] = None coordinate_system_orientation: str = ( - MotorCoordinateSystemOrientation.NOZZLE_TO_COMBUSTION_CHAMBER.value + TankCoordinateSystemOrientation.NOZZLE_TO_COMBUSTION_CHAMBER.value ) dry_I_11: Optional[float] = None dry_I_12: Optional[float] = None diff --git a/tests/test_routes/test_environments_route.py b/tests/test_routes/test_environments_route.py index ceea53a..179b907 100644 --- a/tests/test_routes/test_environments_route.py +++ b/tests/test_routes/test_environments_route.py @@ -9,7 +9,7 @@ EnvCreated, EnvUpdated, EnvDeleted, - EnvSummary, + EnvironmentSummary, ) from lib import app @@ -18,7 +18,7 @@ @pytest.fixture def stub_env_summary(): - env_summary = EnvSummary() + env_summary = EnvironmentSummary() env_summary_json = env_summary.model_dump_json() return json.loads(env_summary_json) @@ -190,7 +190,7 @@ def test_simulate_env(stub_env_summary): with patch.object( EnvController, 'simulate_env', - return_value=EnvSummary(**stub_env_summary), + return_value=EnvironmentSummary(**stub_env_summary), ) as mock_simulate_env: response = client.get('/environments/123/summary') assert response.status_code == 200 From b51ecaffadcacfe399416d0b87ebfee8d5a3b664 Mon Sep 17 00:00:00 2001 From: Gabriel Barberini Date: Sun, 9 Feb 2025 19:49:23 -0300 Subject: [PATCH 03/34] format and lint --- lib/models/motor.py | 2 +- pyproject.toml | 4 +++- tests/test_controllers/test_controller_interface.py | 2 +- tests/test_repositories/test_repository_interface.py | 6 +++--- 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/lib/models/motor.py b/lib/models/motor.py index c4d156e..18eb8bb 100644 --- a/lib/models/motor.py +++ b/lib/models/motor.py @@ -3,7 +3,7 @@ from pydantic import PrivateAttr, model_validator from lib.models.interface import ApiBaseModel -from lib.models.sub.tanks import MotorTank, TankFluids, TankKinds, InterpolationMethods, TankCoordinateSystemOrientation, MotorTank +from lib.models.sub.tanks import MotorTank, InterpolationMethods, TankCoordinateSystemOrientation class MotorKinds(str, Enum): diff --git a/pyproject.toml b/pyproject.toml index eb8954a..3a6271a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,6 +47,8 @@ disable = """ too-many-arguments, redefined-outer-name, too-many-positional-arguments, + no-member, + protected-access, """ [tool.ruff] @@ -55,7 +57,7 @@ target-version = "py312" [tool.ruff.lint] select = ["E", "F", "N", "Q"] -ignore = ["N815", "E501", "Q000", "E402"] +ignore = ["N815", "E501", "Q000", "E402", "N802"] fixable = [ "F401", ] diff --git a/tests/test_controllers/test_controller_interface.py b/tests/test_controllers/test_controller_interface.py index e430e04..4672e54 100644 --- a/tests/test_controllers/test_controller_interface.py +++ b/tests/test_controllers/test_controller_interface.py @@ -27,7 +27,7 @@ def stub_controller(stub_model): @pytest.mark.asyncio async def test_controller_exception_handler_no_exception(stub_model): - async def method(self, model, *args, **kwargs): + async def method(self, model, *args, **kwargs): # pylint: disable=unused-argument return stub_model, args, kwargs test_controller = Mock(method=method) diff --git a/tests/test_repositories/test_repository_interface.py b/tests/test_repositories/test_repository_interface.py index af936b5..6ea6001 100644 --- a/tests/test_repositories/test_repository_interface.py +++ b/tests/test_repositories/test_repository_interface.py @@ -22,7 +22,7 @@ def mock_model(): @pytest_asyncio.fixture def mock_db_interface(stub_loaded_model): - async def async_gen(*args, **kwargs): + async def async_gen(*args, **kwargs): # pylint: disable=unused-argument yield stub_loaded_model yield stub_loaded_model @@ -39,7 +39,7 @@ async def async_gen(*args, **kwargs): @pytest_asyncio.fixture def mock_db_interface_empty_find(): - async def async_gen(*args, **kwargs): + async def async_gen(*args, **kwargs): # pylint: disable=unused-argument if False: # pylint: disable=using-constant-test yield @@ -74,7 +74,7 @@ def stub_repository_invalid_model(): @pytest.mark.asyncio async def test_repository_exception_handler_no_exception(): - async def method(self, *args, **kwargs): + async def method(self, *args, **kwargs): # pylint: disable=unused-argument return args, kwargs mock_kwargs = {'foo': 'bar'} From 55d884ddb9aec5a8430d0928891f8780eee0b83c Mon Sep 17 00:00:00 2001 From: Gabriel Barberini Date: Sun, 9 Feb 2025 20:05:41 -0300 Subject: [PATCH 04/34] fixes imports --- lib/services/motor.py | 3 +- requirements-dev.txt | 1 + tests/test_routes/conftest.py | 15 +++--- tests/test_routes/test_environments_route.py | 54 ++++++++++---------- tests/test_routes/test_flights_route.py | 24 ++++----- tests/test_routes/test_motors_route.py | 22 ++++---- tests/test_routes/test_rockets_route.py | 46 ++++++++--------- 7 files changed, 84 insertions(+), 81 deletions(-) diff --git a/lib/services/motor.py b/lib/services/motor.py index 4bf4b35..70c6543 100644 --- a/lib/services/motor.py +++ b/lib/services/motor.py @@ -15,7 +15,8 @@ TankGeometry, ) -from lib.models.motor import MotorKinds, TankKinds +from lib.models.sub.tanks import TankKinds +from lib.models.motor import MotorKinds from lib.views.motor import MotorSummary, MotorView diff --git a/requirements-dev.txt b/requirements-dev.txt index 9b83bee..811bdd1 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,3 +1,4 @@ +pytest_asyncio flake8 pylint ruff diff --git a/tests/test_routes/conftest.py b/tests/test_routes/conftest.py index e8f6fb1..7e0a6f5 100644 --- a/tests/test_routes/conftest.py +++ b/tests/test_routes/conftest.py @@ -1,22 +1,23 @@ import json import pytest -from lib.models.rocket import Rocket -from lib.models.motor import Motor, MotorTank, TankFluids, TankKinds -from lib.models.environment import Env -from lib.models.aerosurfaces import Fins, NoseCone +from lib.models.rocket import RocketModel +from lib.models.sub.tanks import MotorTank, TankFluids, TankKinds +from lib.models.motor import MotorModel +from lib.models.environment import EnvironmentModel +from lib.models.sub.aerosurfaces import Fins, NoseCone @pytest.fixture def stub_env(): - env = Env(latitude=0, longitude=0) + env = EnvironmentModel(latitude=0, longitude=0) env_json = env.model_dump_json() return json.loads(env_json) @pytest.fixture def stub_motor(): - motor = Motor( + motor = MotorModel( thrust_source=[[0, 0]], burn_time=0, nozzle_radius=0, @@ -109,7 +110,7 @@ def stub_fins(): @pytest.fixture def stub_rocket(stub_motor, stub_nose_cone, stub_fins): - rocket = Rocket( + rocket = RocketModel( motor=stub_motor, radius=0, mass=0, diff --git a/tests/test_routes/test_environments_route.py b/tests/test_routes/test_environments_route.py index 179b907..15ace75 100644 --- a/tests/test_routes/test_environments_route.py +++ b/tests/test_routes/test_environments_route.py @@ -3,12 +3,12 @@ import pytest from fastapi.testclient import TestClient from fastapi import HTTPException, status -from lib.models.environment import Env -from lib.controllers.environment import EnvController +from lib.models.environment import EnvironmentModel +from lib.controllers.environment import EnvironmentController from lib.views.environment import ( - EnvCreated, - EnvUpdated, - EnvDeleted, + EnvironmentCreated, + EnvironmentUpdated, + EnvironmentDeleted, EnvironmentSummary, ) from lib import app @@ -25,7 +25,7 @@ def stub_env_summary(): def test_create_env(stub_env): with patch.object( - EnvController, 'create_env', return_value=EnvCreated(env_id='123') + EnvironmentController, 'create_env', return_value=EnvironmentCreated(env_id='123') ) as mock_create_env: response = client.post('/environments/', json=stub_env) assert response.status_code == 200 @@ -33,7 +33,7 @@ def test_create_env(stub_env): 'env_id': '123', 'message': 'Environment successfully created', } - mock_create_env.assert_called_once_with(Env(**stub_env)) + mock_create_env.assert_called_once_with(EnvironmentModel(**stub_env)) def test_create_env_optional_params(): @@ -46,7 +46,7 @@ def test_create_env_optional_params(): 'date': '2021-01-01T00:00:00', } with patch.object( - EnvController, 'create_env', return_value=EnvCreated(env_id='123') + EnvironmentController, 'create_env', return_value=EnvironmentCreated(env_id='123') ) as mock_create_env: response = client.post('/environments/', json=test_object) assert response.status_code == 200 @@ -54,7 +54,7 @@ def test_create_env_optional_params(): 'env_id': '123', 'message': 'Environment successfully created', } - mock_create_env.assert_called_once_with(Env(**test_object)) + mock_create_env.assert_called_once_with(EnvironmentModel(**test_object)) def test_create_env_invalid_input(): @@ -66,7 +66,7 @@ def test_create_env_invalid_input(): def test_create_env_server_error(stub_env): with patch.object( - EnvController, + EnvironmentController, 'create_env', side_effect=HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR @@ -79,7 +79,7 @@ def test_create_env_server_error(stub_env): def test_read_env(stub_env): with patch.object( - EnvController, 'get_env_by_id', return_value=Env(**stub_env) + EnvironmentController, 'get_env_by_id', return_value=EnvironmentModel(**stub_env) ) as mock_read_env: response = client.get('/environments/123') assert response.status_code == 200 @@ -89,7 +89,7 @@ def test_read_env(stub_env): def test_read_env_not_found(): with patch.object( - EnvController, + EnvironmentController, 'get_env_by_id', side_effect=HTTPException(status_code=status.HTTP_404_NOT_FOUND), ) as mock_read_env: @@ -101,7 +101,7 @@ def test_read_env_not_found(): def test_read_env_server_error(): with patch.object( - EnvController, + EnvironmentController, 'get_env_by_id', side_effect=HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR @@ -114,9 +114,9 @@ def test_read_env_server_error(): def test_update_env(stub_env): with patch.object( - EnvController, + EnvironmentController, 'update_env_by_id', - return_value=EnvUpdated(env_id='123'), + return_value=EnvironmentUpdated(env_id='123'), ) as mock_update_env: response = client.put('/environments/123', json=stub_env) assert response.status_code == 200 @@ -124,7 +124,7 @@ def test_update_env(stub_env): 'env_id': '123', 'message': 'Environment successfully updated', } - mock_update_env.assert_called_once_with('123', Env(**stub_env)) + mock_update_env.assert_called_once_with('123', EnvironmentModel(**stub_env)) def test_update_env_invalid_input(): @@ -136,7 +136,7 @@ def test_update_env_invalid_input(): def test_update_env_not_found(stub_env): with patch.object( - EnvController, + EnvironmentController, 'update_env_by_id', side_effect=HTTPException(status_code=status.HTTP_404_NOT_FOUND), ): @@ -147,7 +147,7 @@ def test_update_env_not_found(stub_env): def test_update_env_server_error(stub_env): with patch.object( - EnvController, + EnvironmentController, 'update_env_by_id', side_effect=HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR @@ -160,9 +160,9 @@ def test_update_env_server_error(stub_env): def test_delete_env(): with patch.object( - EnvController, + EnvironmentController, 'delete_env_by_id', - return_value=EnvDeleted(env_id='123'), + return_value=EnvironmentDeleted(env_id='123'), ) as mock_delete_env: response = client.delete('/environments/123') assert response.status_code == 200 @@ -175,7 +175,7 @@ def test_delete_env(): def test_delete_env_server_error(): with patch.object( - EnvController, + EnvironmentController, 'delete_env_by_id', side_effect=HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR @@ -188,7 +188,7 @@ def test_delete_env_server_error(): def test_simulate_env(stub_env_summary): with patch.object( - EnvController, + EnvironmentController, 'simulate_env', return_value=EnvironmentSummary(**stub_env_summary), ) as mock_simulate_env: @@ -200,7 +200,7 @@ def test_simulate_env(stub_env_summary): def test_simulate_env_not_found(): with patch.object( - EnvController, + EnvironmentController, 'simulate_env', side_effect=HTTPException(status_code=status.HTTP_404_NOT_FOUND), ) as mock_simulate_env: @@ -212,7 +212,7 @@ def test_simulate_env_not_found(): def test_simulate_env_server_error(): with patch.object( - EnvController, + EnvironmentController, 'simulate_env', side_effect=HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR @@ -225,7 +225,7 @@ def test_simulate_env_server_error(): def test_read_rocketpy_env(): with patch.object( - EnvController, 'get_rocketpy_env_binary', return_value=b'rocketpy' + EnvironmentController, 'get_rocketpy_env_binary', return_value=b'rocketpy' ) as mock_read_rocketpy_env: response = client.get('/environments/123/rocketpy') assert response.status_code == 203 @@ -236,7 +236,7 @@ def test_read_rocketpy_env(): def test_read_rocketpy_env_not_found(): with patch.object( - EnvController, + EnvironmentController, 'get_rocketpy_env_binary', side_effect=HTTPException(status_code=status.HTTP_404_NOT_FOUND), ) as mock_read_rocketpy_env: @@ -248,7 +248,7 @@ def test_read_rocketpy_env_not_found(): def test_read_rocketpy_env_server_error(): with patch.object( - EnvController, + EnvironmentController, 'get_rocketpy_env_binary', side_effect=HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR diff --git a/tests/test_routes/test_flights_route.py b/tests/test_routes/test_flights_route.py index 695fb95..e049efe 100644 --- a/tests/test_routes/test_flights_route.py +++ b/tests/test_routes/test_flights_route.py @@ -3,10 +3,10 @@ import pytest from fastapi.testclient import TestClient from fastapi import HTTPException, status -from lib.models.environment import Env -from lib.models.flight import Flight -from lib.models.motor import Motor, MotorKinds -from lib.models.rocket import Rocket +from lib.models.environment import EnvironmentModel +from lib.models.flight import FlightModel +from lib.models.motor import MotorModel, MotorKinds +from lib.models.rocket import RocketModel from lib.controllers.flight import FlightController from lib.views.motor import MotorView from lib.views.rocket import RocketView @@ -50,7 +50,7 @@ def test_create_flight(stub_flight): return_value=FlightCreated(flight_id='123'), ) as mock_create_flight: with patch.object( - Motor, 'set_motor_kind', side_effect=None + MotorModel, 'set_motor_kind', side_effect=None ) as mock_set_motor_kind: response = client.post( '/flights/', json=stub_flight, params={'motor_kind': 'HYBRID'} @@ -61,7 +61,7 @@ def test_create_flight(stub_flight): 'message': 'Flight successfully created', } mock_set_motor_kind.assert_called_once_with(MotorKinds.HYBRID) - mock_create_flight.assert_called_once_with(Flight(**stub_flight)) + mock_create_flight.assert_called_once_with(FlightModel(**stub_flight)) def test_create_flight_optional_params(stub_flight): @@ -83,7 +83,7 @@ def test_create_flight_optional_params(stub_flight): return_value=FlightCreated(flight_id='123'), ) as mock_create_flight: with patch.object( - Motor, 'set_motor_kind', side_effect=None + MotorModel, 'set_motor_kind', side_effect=None ) as mock_set_motor_kind: response = client.post( '/flights/', json=stub_flight, params={'motor_kind': 'HYBRID'} @@ -94,7 +94,7 @@ def test_create_flight_optional_params(stub_flight): 'message': 'Flight successfully created', } mock_set_motor_kind.assert_called_once_with(MotorKinds.HYBRID) - mock_create_flight.assert_called_once_with(Flight(**stub_flight)) + mock_create_flight.assert_called_once_with(FlightModel(**stub_flight)) def test_create_flight_invalid_input(): @@ -168,7 +168,7 @@ def test_update_flight(stub_flight): return_value=FlightUpdated(flight_id='123'), ) as mock_update_flight: with patch.object( - Motor, 'set_motor_kind', side_effect=None + MotorModel, 'set_motor_kind', side_effect=None ) as mock_set_motor_kind: response = client.put( '/flights/123', @@ -181,7 +181,7 @@ def test_update_flight(stub_flight): 'message': 'Flight successfully updated', } mock_update_flight.assert_called_once_with( - '123', Flight(**stub_flight) + '123', FlightModel(**stub_flight) ) mock_set_motor_kind.assert_called_once_with(MotorKinds.GENERIC) @@ -198,7 +198,7 @@ def test_update_env_by_flight_id(stub_env): 'flight_id': '123', 'message': 'Flight successfully updated', } - mock_update_flight.assert_called_once_with('123', env=Env(**stub_env)) + mock_update_flight.assert_called_once_with('123', env=EnvironmentModel(**stub_env)) def test_update_rocket_by_flight_id(stub_rocket): @@ -219,7 +219,7 @@ def test_update_rocket_by_flight_id(stub_rocket): } assert mock_update_flight.call_count == 1 assert mock_update_flight.call_args[0][0] == '123' - assert mock_update_flight.call_args[1]['rocket'].model_dump() == Rocket(**stub_rocket).model_dump() + assert mock_update_flight.call_args[1]['rocket'].model_dump() == RocketModel(**stub_rocket).model_dump() def test_update_env_by_flight_id_invalid_input(): diff --git a/tests/test_routes/test_motors_route.py b/tests/test_routes/test_motors_route.py index 9f32a7b..9e61493 100644 --- a/tests/test_routes/test_motors_route.py +++ b/tests/test_routes/test_motors_route.py @@ -4,7 +4,7 @@ from fastapi.testclient import TestClient from fastapi import HTTPException, status from lib.models.motor import ( - Motor, + MotorModel, MotorKinds, ) from lib.controllers.motor import MotorController @@ -45,7 +45,7 @@ def test_create_motor(stub_motor): 'message': 'Motor successfully created', } mock_set_motor_kind.assert_called_once_with(MotorKinds.HYBRID) - mock_create_motor.assert_called_once_with(Motor(**stub_motor)) + mock_create_motor.assert_called_once_with(MotorModel(**stub_motor)) def test_create_motor_optional_params(stub_motor): @@ -73,7 +73,7 @@ def test_create_motor_optional_params(stub_motor): 'message': 'Motor successfully created', } mock_set_motor_kind.assert_called_once_with(MotorKinds.HYBRID) - mock_create_motor.assert_called_once_with(Motor(**stub_motor)) + mock_create_motor.assert_called_once_with(MotorModel(**stub_motor)) def test_create_generic_motor(stub_motor): @@ -103,7 +103,7 @@ def test_create_generic_motor(stub_motor): 'message': 'Motor successfully created', } mock_set_motor_kind.assert_called_once_with(MotorKinds.GENERIC) - mock_create_motor.assert_called_once_with(Motor(**stub_motor)) + mock_create_motor.assert_called_once_with(MotorModel(**stub_motor)) def test_create_liquid_motor_level_tank(stub_motor, stub_level_tank): @@ -125,7 +125,7 @@ def test_create_liquid_motor_level_tank(stub_motor, stub_level_tank): 'message': 'Motor successfully created', } mock_set_motor_kind.assert_called_once_with(MotorKinds.LIQUID) - mock_create_motor.assert_called_once_with(Motor(**stub_motor)) + mock_create_motor.assert_called_once_with(MotorModel(**stub_motor)) def test_create_liquid_motor_mass_flow_tank(stub_motor, stub_mass_flow_tank): @@ -147,7 +147,7 @@ def test_create_liquid_motor_mass_flow_tank(stub_motor, stub_mass_flow_tank): 'message': 'Motor successfully created', } mock_set_motor_kind.assert_called_once_with(MotorKinds.LIQUID) - mock_create_motor.assert_called_once_with(Motor(**stub_motor)) + mock_create_motor.assert_called_once_with(MotorModel(**stub_motor)) def test_create_liquid_motor_ullage_tank(stub_motor, stub_ullage_tank): @@ -169,7 +169,7 @@ def test_create_liquid_motor_ullage_tank(stub_motor, stub_ullage_tank): 'message': 'Motor successfully created', } mock_set_motor_kind.assert_called_once_with(MotorKinds.LIQUID) - mock_create_motor.assert_called_once_with(Motor(**stub_motor)) + mock_create_motor.assert_called_once_with(MotorModel(**stub_motor)) def test_create_liquid_motor_mass_tank(stub_motor, stub_mass_tank): @@ -191,7 +191,7 @@ def test_create_liquid_motor_mass_tank(stub_motor, stub_mass_tank): 'message': 'Motor successfully created', } mock_set_motor_kind.assert_called_once_with(MotorKinds.LIQUID) - mock_create_motor.assert_called_once_with(Motor(**stub_motor)) + mock_create_motor.assert_called_once_with(MotorModel(**stub_motor)) def test_create_hybrid_motor(stub_motor, stub_level_tank): @@ -225,7 +225,7 @@ def test_create_hybrid_motor(stub_motor, stub_level_tank): 'message': 'Motor successfully created', } mock_set_motor_kind.assert_called_once_with(MotorKinds.HYBRID) - mock_create_motor.assert_called_once_with(Motor(**stub_motor)) + mock_create_motor.assert_called_once_with(MotorModel(**stub_motor)) def test_create_solid_motor(stub_motor): @@ -257,7 +257,7 @@ def test_create_solid_motor(stub_motor): 'message': 'Motor successfully created', } mock_set_motor_kind.assert_called_once_with(MotorKinds.SOLID) - mock_create_motor.assert_called_once_with(Motor(**stub_motor)) + mock_create_motor.assert_called_once_with(MotorModel(**stub_motor)) def test_create_motor_invalid_input(): @@ -338,7 +338,7 @@ def test_update_motor(stub_motor): 'message': 'Motor successfully updated', } mock_update_motor.assert_called_once_with( - '123', Motor(**stub_motor) + '123', MotorModel(**stub_motor) ) mock_set_motor_kind.assert_called_once_with(MotorKinds.HYBRID) diff --git a/tests/test_routes/test_rockets_route.py b/tests/test_routes/test_rockets_route.py index f689b98..3e0e514 100644 --- a/tests/test_routes/test_rockets_route.py +++ b/tests/test_routes/test_rockets_route.py @@ -3,14 +3,14 @@ import pytest from fastapi.testclient import TestClient from fastapi import HTTPException, status -from lib.models.aerosurfaces import ( +from lib.models.sub.aerosurfaces import ( Tail, RailButtons, Parachute, ) -from lib.models.rocket import Rocket +from lib.models.rocket import RocketModel from lib.models.motor import ( - Motor, + MotorModel, MotorKinds, ) from lib.controllers.rocket import RocketController @@ -80,7 +80,7 @@ def test_create_rocket(stub_rocket): return_value=RocketCreated(rocket_id='123'), ) as mock_create_rocket: with patch.object( - Motor, 'set_motor_kind', side_effect=None + MotorModel, 'set_motor_kind', side_effect=None ) as mock_set_motor_kind: response = client.post( '/rockets/', json=stub_rocket, params={'motor_kind': 'HYBRID'} @@ -91,7 +91,7 @@ def test_create_rocket(stub_rocket): 'message': 'Rocket successfully created', } mock_set_motor_kind.assert_called_once_with(MotorKinds.HYBRID) - mock_create_rocket.assert_called_once_with(Rocket(**stub_rocket)) + mock_create_rocket.assert_called_once_with(RocketModel(**stub_rocket)) def test_create_rocket_optional_params( @@ -113,7 +113,7 @@ def test_create_rocket_optional_params( return_value=RocketCreated(rocket_id='123'), ) as mock_create_rocket: with patch.object( - Motor, 'set_motor_kind', side_effect=None + MotorModel, 'set_motor_kind', side_effect=None ) as mock_set_motor_kind: response = client.post( '/rockets/', json=stub_rocket, params={'motor_kind': 'HYBRID'} @@ -124,7 +124,7 @@ def test_create_rocket_optional_params( 'message': 'Rocket successfully created', } mock_set_motor_kind.assert_called_once_with(MotorKinds.HYBRID) - mock_create_rocket.assert_called_once_with(Rocket(**stub_rocket)) + mock_create_rocket.assert_called_once_with(RocketModel(**stub_rocket)) def test_create_generic_motor_rocket(stub_rocket, stub_motor): @@ -144,7 +144,7 @@ def test_create_generic_motor_rocket(stub_rocket, stub_motor): return_value=RocketCreated(rocket_id='123'), ) as mock_create_rocket: with patch.object( - Motor, 'set_motor_kind', side_effect=None + MotorModel, 'set_motor_kind', side_effect=None ) as mock_set_motor_kind: response = client.post( '/rockets/', json=stub_rocket, params={'motor_kind': 'GENERIC'} @@ -155,7 +155,7 @@ def test_create_generic_motor_rocket(stub_rocket, stub_motor): 'message': 'Rocket successfully created', } mock_set_motor_kind.assert_called_once_with(MotorKinds.GENERIC) - mock_create_rocket.assert_called_once_with(Rocket(**stub_rocket)) + mock_create_rocket.assert_called_once_with(RocketModel(**stub_rocket)) def test_create_liquid_motor_level_tank_rocket( @@ -169,7 +169,7 @@ def test_create_liquid_motor_level_tank_rocket( return_value=RocketCreated(rocket_id='123'), ) as mock_create_rocket: with patch.object( - Motor, 'set_motor_kind', side_effect=None + MotorModel, 'set_motor_kind', side_effect=None ) as mock_set_motor_kind: response = client.post( '/rockets/', json=stub_rocket, params={'motor_kind': 'LIQUID'} @@ -180,7 +180,7 @@ def test_create_liquid_motor_level_tank_rocket( 'message': 'Rocket successfully created', } mock_set_motor_kind.assert_called_once_with(MotorKinds.LIQUID) - mock_create_rocket.assert_called_once_with(Rocket(**stub_rocket)) + mock_create_rocket.assert_called_once_with(RocketModel(**stub_rocket)) def test_create_liquid_motor_mass_flow_tank_rocket( @@ -194,7 +194,7 @@ def test_create_liquid_motor_mass_flow_tank_rocket( return_value=RocketCreated(rocket_id='123'), ) as mock_create_rocket: with patch.object( - Motor, 'set_motor_kind', side_effect=None + MotorModel, 'set_motor_kind', side_effect=None ) as mock_set_motor_kind: response = client.post( '/rockets/', json=stub_rocket, params={'motor_kind': 'LIQUID'} @@ -205,7 +205,7 @@ def test_create_liquid_motor_mass_flow_tank_rocket( 'message': 'Rocket successfully created', } mock_set_motor_kind.assert_called_once_with(MotorKinds.LIQUID) - mock_create_rocket.assert_called_once_with(Rocket(**stub_rocket)) + mock_create_rocket.assert_called_once_with(RocketModel(**stub_rocket)) def test_create_liquid_motor_ullage_tank_rocket( @@ -219,7 +219,7 @@ def test_create_liquid_motor_ullage_tank_rocket( return_value=RocketCreated(rocket_id='123'), ) as mock_create_rocket: with patch.object( - Motor, 'set_motor_kind', side_effect=None + MotorModel, 'set_motor_kind', side_effect=None ) as mock_set_motor_kind: response = client.post( '/rockets/', json=stub_rocket, params={'motor_kind': 'LIQUID'} @@ -230,7 +230,7 @@ def test_create_liquid_motor_ullage_tank_rocket( 'message': 'Rocket successfully created', } mock_set_motor_kind.assert_called_once_with(MotorKinds.LIQUID) - mock_create_rocket.assert_called_once_with(Rocket(**stub_rocket)) + mock_create_rocket.assert_called_once_with(RocketModel(**stub_rocket)) def test_create_liquid_motor_mass_tank_rocket( @@ -244,7 +244,7 @@ def test_create_liquid_motor_mass_tank_rocket( return_value=RocketCreated(rocket_id='123'), ) as mock_create_rocket: with patch.object( - Motor, 'set_motor_kind', side_effect=None + MotorModel, 'set_motor_kind', side_effect=None ) as mock_set_motor_kind: response = client.post( '/rockets/', json=stub_rocket, params={'motor_kind': 'LIQUID'} @@ -255,7 +255,7 @@ def test_create_liquid_motor_mass_tank_rocket( 'message': 'Rocket successfully created', } mock_set_motor_kind.assert_called_once_with(MotorKinds.LIQUID) - mock_create_rocket.assert_called_once_with(Rocket(**stub_rocket)) + mock_create_rocket.assert_called_once_with(RocketModel(**stub_rocket)) def test_create_hybrid_motor_rocket(stub_rocket, stub_motor, stub_level_tank): @@ -279,7 +279,7 @@ def test_create_hybrid_motor_rocket(stub_rocket, stub_motor, stub_level_tank): return_value=RocketCreated(rocket_id='123'), ) as mock_create_rocket: with patch.object( - Motor, 'set_motor_kind', side_effect=None + MotorModel, 'set_motor_kind', side_effect=None ) as mock_set_motor_kind: response = client.post( '/rockets/', json=stub_rocket, params={'motor_kind': 'HYBRID'} @@ -290,7 +290,7 @@ def test_create_hybrid_motor_rocket(stub_rocket, stub_motor, stub_level_tank): 'message': 'Rocket successfully created', } mock_set_motor_kind.assert_called_once_with(MotorKinds.HYBRID) - mock_create_rocket.assert_called_once_with(Rocket(**stub_rocket)) + mock_create_rocket.assert_called_once_with(RocketModel(**stub_rocket)) def test_create_solid_motor_rocket(stub_rocket, stub_motor): @@ -312,7 +312,7 @@ def test_create_solid_motor_rocket(stub_rocket, stub_motor): return_value=RocketCreated(rocket_id='123'), ) as mock_create_rocket: with patch.object( - Motor, 'set_motor_kind', side_effect=None + MotorModel, 'set_motor_kind', side_effect=None ) as mock_set_motor_kind: response = client.post( '/rockets/', json=stub_rocket, params={'motor_kind': 'SOLID'} @@ -323,7 +323,7 @@ def test_create_solid_motor_rocket(stub_rocket, stub_motor): 'message': 'Rocket successfully created', } mock_set_motor_kind.assert_called_once_with(MotorKinds.SOLID) - mock_create_rocket.assert_called_once_with(Rocket(**stub_rocket)) + mock_create_rocket.assert_called_once_with(RocketModel(**stub_rocket)) def test_create_rocket_invalid_input(): @@ -393,7 +393,7 @@ def test_update_rocket(stub_rocket): return_value=RocketUpdated(rocket_id='123'), ) as mock_update_rocket: with patch.object( - Motor, 'set_motor_kind', side_effect=None + MotorModel, 'set_motor_kind', side_effect=None ) as mock_set_motor_kind: response = client.put( '/rockets/123', @@ -406,7 +406,7 @@ def test_update_rocket(stub_rocket): 'message': 'Rocket successfully updated', } mock_update_rocket.assert_called_once_with( - '123', Rocket(**stub_rocket) + '123', RocketModel(**stub_rocket) ) mock_set_motor_kind.assert_called_once_with(MotorKinds.GENERIC) From e8e2d509ef481e97744e00edf733d85f9bd370eb Mon Sep 17 00:00:00 2001 From: Gabriel Barberini Date: Fri, 14 Feb 2025 23:23:23 -0300 Subject: [PATCH 05/34] migrate Enum to Literal when possible --- lib/controllers/environment.py | 2 +- lib/models/environment.py | 16 +- lib/models/flight.py | 10 +- lib/models/motor.py | 8 +- lib/models/rocket.py | 12 +- lib/models/sub/aerosurfaces.py | 17 +- lib/models/sub/tanks.py | 14 - lib/routes/environment.py | 4 +- lib/services/environment.py | 2 +- lib/services/flight.py | 2 +- lib/services/motor.py | 4 +- lib/services/rocket.py | 6 +- lib/views/motor.py | 5 +- lib/views/rocket.py | 6 +- tests/test_routes/conftest.py | 16 +- tests/test_routes/test_environments_route.py | 383 +++++------ tests/test_routes/test_flights_route.py | 2 +- tests/test_routes/test_motors_route.py | 680 ++++++++----------- 18 files changed, 505 insertions(+), 684 deletions(-) diff --git a/lib/controllers/environment.py b/lib/controllers/environment.py index dcf56d6..42d3a6b 100644 --- a/lib/controllers/environment.py +++ b/lib/controllers/environment.py @@ -41,7 +41,7 @@ async def get_rocketpy_env_binary( return env_service.get_env_binary() @controller_exception_handler - async def simulate_env( + async def get_environment_simulation( self, env_id: str ) -> EnvironmentSummary: """ diff --git a/lib/models/environment.py b/lib/models/environment.py index be5c359..e922506 100644 --- a/lib/models/environment.py +++ b/lib/models/environment.py @@ -1,18 +1,8 @@ import datetime -from enum import Enum -from typing import Optional, ClassVar, Self +from typing import Optional, ClassVar, Self, Literal from lib.models.interface import ApiBaseModel -class AtmosphericModelTypes(str, Enum): - STANDARD_ATMOSPHERE: str = "STANDARD_ATMOSPHERE" - CUSTOM_ATMOSPHERE: str = "CUSTOM_ATMOSPHERE" - WYOMING_SOUNDING: str = "WYOMING_SOUNDING" - FORECAST: str = "FORECAST" - REANALYSIS: str = "REANALYSIS" - ENSEMBLE: str = "ENSEMBLE" - - class EnvironmentModel(ApiBaseModel): NAME: ClassVar = 'environment' METHODS: ClassVar = ('POST', 'GET', 'PUT', 'DELETE') @@ -21,9 +11,7 @@ class EnvironmentModel(ApiBaseModel): elevation: Optional[int] = 1 # Optional parameters - atmospheric_model_type: AtmosphericModelTypes = ( - AtmosphericModelTypes.STANDARD_ATMOSPHERE - ) + atmospheric_model_type: Literal['standard_atmosphere', 'custom_atmosphere', 'wyoming_sounding', 'forecast', 'reanalysis', 'ensemble'] = 'standard_atmosphere' atmospheric_model_file: Optional[str] = None date: Optional[datetime.datetime] = ( datetime.datetime.today() + datetime.timedelta(days=1) diff --git a/lib/models/flight.py b/lib/models/flight.py index e227f00..473fef2 100644 --- a/lib/models/flight.py +++ b/lib/models/flight.py @@ -1,15 +1,9 @@ -from enum import Enum -from typing import Optional, Self, ClassVar +from typing import Optional, Self, ClassVar, Literal from lib.models.interface import ApiBaseModel from lib.models.rocket import RocketModel from lib.models.environment import EnvironmentModel -class EquationsOfMotion(str, Enum): - STANDARD: str = "STANDARD" - SOLID_PROPULSION: str = "SOLID_PROPULSION" - - class FlightModel(ApiBaseModel): NAME: ClassVar = "flight" METHODS: ClassVar = ("POST", "GET", "PUT", "DELETE") @@ -20,7 +14,7 @@ class FlightModel(ApiBaseModel): rail_length: float = 1 time_overshoot: bool = True terminate_on_apogee: bool = True - equations_of_motion: EquationsOfMotion = EquationsOfMotion.STANDARD + equations_of_motion: Literal['standard', 'solid_propulsion'] = 'standard' # Optional parameters inclination: Optional[int] = None diff --git a/lib/models/motor.py b/lib/models/motor.py index 18eb8bb..1f658e9 100644 --- a/lib/models/motor.py +++ b/lib/models/motor.py @@ -3,7 +3,7 @@ from pydantic import PrivateAttr, model_validator from lib.models.interface import ApiBaseModel -from lib.models.sub.tanks import MotorTank, InterpolationMethods, TankCoordinateSystemOrientation +from lib.models.sub.tanks import MotorTank class MotorKinds(str, Enum): @@ -48,10 +48,8 @@ class MotorModel(ApiBaseModel): throat_radius: Optional[float] = None # Optional parameters - interpolation_method: InterpolationMethods = InterpolationMethods.LINEAR - coordinate_system_orientation: TankCoordinateSystemOrientation = ( - TankCoordinateSystemOrientation.NOZZLE_TO_COMBUSTION_CHAMBER - ) + interpolation_method: Literal['linear', 'spline', 'akima', 'polynomial', 'shepard', 'rbf'] = 'linear' + coordinate_system_orientation: Literal['nozzle_to_combustion_chamber', 'combustion_chamber_to_nozzle'] = 'nose_to_combustion_chamber' reshape_thrust_curve: Union[bool, tuple] = False # Computed parameters diff --git a/lib/models/rocket.py b/lib/models/rocket.py index f8932d1..39def1e 100644 --- a/lib/models/rocket.py +++ b/lib/models/rocket.py @@ -1,5 +1,4 @@ -from enum import Enum -from typing import Optional, Tuple, List, Union, Self, ClassVar +from typing import Optional, Tuple, List, Union, Self, ClassVar, Literal from lib.models.interface import ApiBaseModel from lib.models.motor import MotorModel from lib.models.sub.aerosurfaces import ( @@ -11,11 +10,6 @@ ) -class RocketCoordinateSystemOrientation(str, Enum): - TAIL_TO_NOSE: str = "TAIL_TO_NOSE" - NOSE_TO_TAIL: str = "NOSE_TO_TAIL" - - class RocketModel(ApiBaseModel): NAME: ClassVar = "rocket" METHODS: ClassVar = ("POST", "GET", "PUT", "DELETE") @@ -32,9 +26,7 @@ class RocketModel(ApiBaseModel): ] = (0, 0, 0) power_off_drag: List[Tuple[float, float]] = [(0, 0)] power_on_drag: List[Tuple[float, float]] = [(0, 0)] - coordinate_system_orientation: RocketCoordinateSystemOrientation = ( - RocketCoordinateSystemOrientation.TAIL_TO_NOSE - ) + coordinate_system_orientation: Literal['tail_to_nose', 'nose_to_tail'] = 'tail_to_nose' nose: NoseCone fins: List[Fins] diff --git a/lib/models/sub/aerosurfaces.py b/lib/models/sub/aerosurfaces.py index 68d8e41..6e29ee7 100644 --- a/lib/models/sub/aerosurfaces.py +++ b/lib/models/sub/aerosurfaces.py @@ -1,5 +1,4 @@ -from enum import Enum -from typing import Optional, Tuple, List, Union +from typing import Optional, Tuple, List, Union, Literal from pydantic import BaseModel, Field @@ -28,18 +27,8 @@ class NoseCone(BaseModel): rocket_radius: float -class FinsKinds(str, Enum): - TRAPEZOIDAL: str = "TRAPEZOIDAL" - ELLIPTICAL: str = "ELLIPTICAL" - - -class AngleUnit(str, Enum): - RADIANS: str = "RADIANS" - DEGREES: str = "DEGREES" - - class Fins(BaseModel): - fins_kind: FinsKinds + fins_kind: Literal['trapezoidal', 'elliptical'] name: str n: int root_chord: float @@ -50,7 +39,7 @@ class Fins(BaseModel): tip_chord: Optional[float] = None cant_angle: Optional[float] = None rocket_radius: Optional[float] = None - airfoil: Optional[Tuple[List[Tuple[float, float]], AngleUnit]] = None + airfoil: Optional[Tuple[List[Tuple[float, float]], Literal['radians', 'degrees']]] = None def get_additional_parameters(self): return { diff --git a/lib/models/sub/tanks.py b/lib/models/sub/tanks.py index 8f0c137..92f39b8 100644 --- a/lib/models/sub/tanks.py +++ b/lib/models/sub/tanks.py @@ -10,25 +10,11 @@ class TankKinds(str, Enum): ULLAGE: str = "ULLAGE" -class TankCoordinateSystemOrientation(str, Enum): - NOZZLE_TO_COMBUSTION_CHAMBER: str = "NOZZLE_TO_COMBUSTION_CHAMBER" - COMBUSTION_CHAMBER_TO_NOZZLE: str = "COMBUSTION_CHAMBER_TO_NOZZLE" - - class TankFluids(BaseModel): name: str density: float -class InterpolationMethods(str, Enum): - LINEAR: str = "LINEAR" - SPLINE: str = "SPLINE" - AKIMA: str = "AKIMA" - POLYNOMIAL: str = "POLYNOMIAL" - SHEPARD: str = "SHEPARD" - RBF: str = "RBF" - - class MotorTank(BaseModel): # Required parameters geometry: List[Tuple[Tuple[float, float], float]] diff --git a/lib/routes/environment.py b/lib/routes/environment.py index e007131..5525a1a 100644 --- a/lib/routes/environment.py +++ b/lib/routes/environment.py @@ -116,13 +116,13 @@ async def read_rocketpy_env(environment_id: str): @router.get("/{environment_id}/summary") -async def simulate_env(environment_id: str) -> EnvironmentSummary: +async def get_environment_simulation(environment_id: str) -> EnvironmentSummary: """ Loads rocketpy.environment simulation ## Args ``` environment_id: str ``` """ - with tracer.start_as_current_span("simulate_env"): + with tracer.start_as_current_span("get_environment_simulation"): controller = EnvironmentController() return await controller.get_environment_summary(environment_id) diff --git a/lib/services/environment.py b/lib/services/environment.py index e6ec778..0220ffc 100644 --- a/lib/services/environment.py +++ b/lib/services/environment.py @@ -29,7 +29,7 @@ def from_env_model(cls, env: EnvironmentModel) -> Self: date=env.date, ) rocketpy_env.set_atmospheric_model( - type=env.atmospheric_model_type.value.lower(), + type=env.atmospheric_model_type, file=env.atmospheric_model_file, ) return cls(environment=rocketpy_env) diff --git a/lib/services/flight.py b/lib/services/flight.py index 169b9fa..1c300b8 100644 --- a/lib/services/flight.py +++ b/lib/services/flight.py @@ -34,7 +34,7 @@ def from_flight_model(cls, flight: FlightView) -> Self: rail_length=flight.rail_length, terminate_on_apogee=flight.terminate_on_apogee, time_overshoot=flight.time_overshoot, - equations_of_motion=flight.equations_of_motion.value.lower(), + equations_of_motion=flight.equations_of_motion, **flight.get_additional_parameters(), ) return cls(flight=rocketpy_flight) diff --git a/lib/services/motor.py b/lib/services/motor.py index 70c6543..bfb75ec 100644 --- a/lib/services/motor.py +++ b/lib/services/motor.py @@ -42,8 +42,8 @@ def from_motor_model(cls, motor: MotorView) -> Self: "dry_mass": motor.dry_mass, "dry_inertia": motor.dry_inertia, "center_of_dry_mass_position": motor.center_of_dry_mass_position, - "coordinate_system_orientation": motor.coordinate_system_orientation.value.lower(), - "interpolation_method": motor.interpolation_method.value.lower(), + "coordinate_system_orientation": motor.coordinate_system_orientation, + "interpolation_method": motor.interpolation_method, "reshape_thrust_curve": False or motor.reshape_thrust_curve, } diff --git a/lib/services/rocket.py b/lib/services/rocket.py index d26812d..d282be5 100644 --- a/lib/services/rocket.py +++ b/lib/services/rocket.py @@ -43,7 +43,7 @@ def from_rocket_model(cls, rocket: RocketView) -> Self: power_off_drag=rocket.power_off_drag, power_on_drag=rocket.power_on_drag, center_of_mass_without_motor=rocket.center_of_mass_without_motor, - coordinate_system_orientation=rocket.coordinate_system_orientation.value.lower(), + coordinate_system_orientation=rocket.coordinate_system_orientation, ) rocketpy_rocket.add_motor( MotorService.from_motor_model(rocket.motor).motor, @@ -157,7 +157,7 @@ def get_rocketpy_finset(fins: Fins, kind: str) -> RocketPyFins: RocketPyEllipticalFins """ match kind: - case "TRAPEZOIDAL": + case "trapezoidal": rocketpy_finset = RocketPyTrapezoidalFins( n=fins.n, name=fins.name, @@ -165,7 +165,7 @@ def get_rocketpy_finset(fins: Fins, kind: str) -> RocketPyFins: span=fins.span, **fins.get_additional_parameters(), ) - case "ELLIPTICAL": + case "elliptical": rocketpy_finset = RocketPyEllipticalFins( n=fins.n, name=fins.name, diff --git a/lib/views/motor.py b/lib/views/motor.py index 1c663ef..0f213d1 100644 --- a/lib/views/motor.py +++ b/lib/views/motor.py @@ -1,7 +1,6 @@ from typing import List, Any, Optional from pydantic import BaseModel, ConfigDict from lib.views.interface import ApiBaseView -from lib.models.sub.tanks import TankCoordinateSystemOrientation from lib.models.motor import MotorModel, MotorKinds from lib.utils import to_python_primitive @@ -12,9 +11,7 @@ class MotorSummary(BaseModel): burn_out_time: Optional[float] = None burn_start_time: Optional[float] = None center_of_dry_mass_position: Optional[float] = None - coordinate_system_orientation: str = ( - TankCoordinateSystemOrientation.NOZZLE_TO_COMBUSTION_CHAMBER.value - ) + coordinate_system_orientation: str = 'nozzle_to_combustion_chamber' dry_I_11: Optional[float] = None dry_I_12: Optional[float] = None dry_I_13: Optional[float] = None diff --git a/lib/views/rocket.py b/lib/views/rocket.py index bb85104..97482bd 100644 --- a/lib/views/rocket.py +++ b/lib/views/rocket.py @@ -1,6 +1,6 @@ from typing import Any, Optional from pydantic import ConfigDict -from lib.models.rocket import RocketModel, RocketCoordinateSystemOrientation +from lib.models.rocket import RocketModel from lib.views.interface import ApiBaseView from lib.views.motor import MotorView, MotorSummary from lib.utils import to_python_primitive @@ -8,9 +8,7 @@ class RocketSummary(MotorSummary): area: Optional[float] = None - coordinate_system_orientation: str = ( - RocketCoordinateSystemOrientation.TAIL_TO_NOSE.value - ) + coordinate_system_orientation: str = 'tail_to_nose' center_of_mass_without_motor: Optional[float] = None motor_center_of_dry_mass_position: Optional[float] = None motor_position: Optional[float] = None diff --git a/tests/test_routes/conftest.py b/tests/test_routes/conftest.py index 7e0a6f5..e793714 100644 --- a/tests/test_routes/conftest.py +++ b/tests/test_routes/conftest.py @@ -9,14 +9,14 @@ @pytest.fixture -def stub_env(): +def stub_environment_dump(): env = EnvironmentModel(latitude=0, longitude=0) env_json = env.model_dump_json() return json.loads(env_json) @pytest.fixture -def stub_motor(): +def stub_motor_dump(): motor = MotorModel( thrust_source=[[0, 0]], burn_time=0, @@ -30,7 +30,7 @@ def stub_motor(): @pytest.fixture -def stub_tank(): +def stub_tank_dump(): tank = MotorTank( geometry=[[(0, 0), 0]], gas=TankFluids(name='gas', density=0), @@ -81,7 +81,7 @@ def stub_mass_tank(stub_tank): @pytest.fixture -def stub_nose_cone(): +def stub_nose_cone_dump(): nose_cone = NoseCone( name='nose', length=0, @@ -95,9 +95,9 @@ def stub_nose_cone(): @pytest.fixture -def stub_fins(): +def stub_fins_dump(): fins = Fins( - fins_kind='TRAPEZOIDAL', + fins_kind='trapezoidal', name='fins', n=0, root_chord=0, @@ -109,7 +109,7 @@ def stub_fins(): @pytest.fixture -def stub_rocket(stub_motor, stub_nose_cone, stub_fins): +def stub_rocket_dump(stub_motor, stub_nose_cone, stub_fins): rocket = RocketModel( motor=stub_motor, radius=0, @@ -121,7 +121,7 @@ def stub_rocket(stub_motor, stub_nose_cone, stub_fins): power_on_drag=[(0, 0)], nose=stub_nose_cone, fins=[stub_fins], - coordinate_system_orientation='TAIL_TO_NOSE', + coordinate_system_orientation='tail_to_nose', ) rocket_json = rocket.model_dump_json() return json.loads(rocket_json) diff --git a/tests/test_routes/test_environments_route.py b/tests/test_routes/test_environments_route.py index 15ace75..761a975 100644 --- a/tests/test_routes/test_environments_route.py +++ b/tests/test_routes/test_environments_route.py @@ -17,243 +17,200 @@ @pytest.fixture -def stub_env_summary(): +def stub_environment_summary_dump(): env_summary = EnvironmentSummary() env_summary_json = env_summary.model_dump_json() return json.loads(env_summary_json) +@pytest.fixture(autouse=True) +def mock_controller_instance(): + with patch( + "lib.routes.environment.EnvironmentController", autospec=True + ) as mock_controller: + mock_controller_instance = mock_controller.return_value + mock_controller_instance.post_environment = Mock() + mock_controller_instance.get_environment_by_id = Mock() + mock_controller_instance.put_environment_by_id = Mock() + mock_controller_instance.delete_environment_by_id = Mock() + yield mock_controller_instance + +def test_create_environment(stub_environment_dump, mock_controller_instance): + mock_response = AsyncMock(return_value=EnvironmentCreated(environment_id='123')) + mock_controller_instance.post_environment = mock_response + response = client.post('/environments/', json=stub_environment_dump) + assert response.status_code == 200 + assert response.json() == { + 'environment_id': '123', + 'message': 'environment successfully created', + } + mock_controller_instance.post_environment.assert_called_once_with( + EnvironmentModel(**stub_environment_dump) + ) -def test_create_env(stub_env): - with patch.object( - EnvironmentController, 'create_env', return_value=EnvironmentCreated(env_id='123') - ) as mock_create_env: - response = client.post('/environments/', json=stub_env) - assert response.status_code == 200 - assert response.json() == { - 'env_id': '123', - 'message': 'Environment successfully created', - } - mock_create_env.assert_called_once_with(EnvironmentModel(**stub_env)) - - -def test_create_env_optional_params(): - test_object = { +def test_create_environment_optional_params(stub_environment_dump, mock_controller_instance): + stub_environment_dump.update({ 'latitude': 0, 'longitude': 0, 'elevation': 1, 'atmospheric_model_type': 'STANDARD_ATMOSPHERE', 'atmospheric_model_file': None, 'date': '2021-01-01T00:00:00', + }) + mock_response = AsyncMock(return_value=EnvironmentCreated(environment_id='123')) + mock_controller_instance.post_environment = mock_response + response = client.post('/environments/', json=stub_environment_dump) + assert response.status_code == 200 + assert response.json() == { + 'environment_id': '123', + 'message': 'Environment successfully created', } - with patch.object( - EnvironmentController, 'create_env', return_value=EnvironmentCreated(env_id='123') - ) as mock_create_env: - response = client.post('/environments/', json=test_object) - assert response.status_code == 200 - assert response.json() == { - 'env_id': '123', - 'message': 'Environment successfully created', - } - mock_create_env.assert_called_once_with(EnvironmentModel(**test_object)) - -def test_create_env_invalid_input(): +def test_create_environment_invalid_input(): response = client.post( '/environments/', json={'latitude': 'foo', 'longitude': 'bar'} ) assert response.status_code == 422 +def test_create_environment_server_error( + stub_environment_dump, mock_controller_instance +): + mock_response = AsyncMock(side_effect=HTTPException(status_code=500)) + mock_controller_instance.post_environment = mock_response + response = client.post('/environments/', json=stub_environment_dump) + assert response.status_code == 500 + assert response.json() == {'detail': 'Internal Server Error'} + +def test_read_environment(stub_env): + stub_environment_out = EnvironmentModelOut(environment_id='123', **stub_environment_dump) + mock_response = AsyncMock( + return_value=EnvironmentRetrieved(environment=stub_environment_out) + ) + mock_controller_instance.get_environment_by_id = mock_response + response = client.get('/environments/123') + assert response.status_code == 200 + assert response.json() == { + 'message': 'Environment successfully retrieved', + 'environment': json.loads(stub_environment_out.model_dump_json()), + } -def test_create_env_server_error(stub_env): - with patch.object( - EnvironmentController, - 'create_env', - side_effect=HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR - ), - ): - response = client.post('/environments/', json=stub_env) - assert response.status_code == 500 - assert response.json() == {'detail': 'Internal Server Error'} - - -def test_read_env(stub_env): - with patch.object( - EnvironmentController, 'get_env_by_id', return_value=EnvironmentModel(**stub_env) - ) as mock_read_env: - response = client.get('/environments/123') - assert response.status_code == 200 - assert response.json() == stub_env - mock_read_env.assert_called_once_with('123') - - -def test_read_env_not_found(): - with patch.object( - EnvironmentController, - 'get_env_by_id', - side_effect=HTTPException(status_code=status.HTTP_404_NOT_FOUND), - ) as mock_read_env: - response = client.get('/environments/123') - assert response.status_code == 404 - assert response.json() == {'detail': 'Not Found'} - mock_read_env.assert_called_once_with('123') - - -def test_read_env_server_error(): - with patch.object( - EnvironmentController, - 'get_env_by_id', - side_effect=HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR - ), - ): - response = client.get('/environments/123') - assert response.status_code == 500 - assert response.json() == {'detail': 'Internal Server Error'} - - -def test_update_env(stub_env): - with patch.object( - EnvironmentController, - 'update_env_by_id', - return_value=EnvironmentUpdated(env_id='123'), - ) as mock_update_env: - response = client.put('/environments/123', json=stub_env) - assert response.status_code == 200 - assert response.json() == { - 'env_id': '123', - 'message': 'Environment successfully updated', - } - mock_update_env.assert_called_once_with('123', EnvironmentModel(**stub_env)) - +def test_read_environment_not_found(mock_controller_instance): + mock_response = AsyncMock(side_effect=HTTPException(status_code=404)) + mock_controller_instance.get_environment_by_id = mock_response + response = client.get('/environments/123') + assert response.status_code == 404 + assert response.json() == {'detail': 'Not Found'} + mock_controller_instance.get_environment_by_id.assert_called_once_with('123') + +def test_read_environment_server_error(mock_controller_instance): + mock_response = AsyncMock(side_effect=HTTPException(status_code=500)) + mock_controller_instance.get_environment_by_id = mock_response + response = client.get('/environments/123') + assert response.status_code == 500 + assert response.json() == {'detail': 'Internal Server Error'} + +def test_update_environment(stub_environment_dump, mock_controller_instance): + mock_reponse = AsyncMock(return_value=EnvironmentUpdated(environment_id='123')) + mock_controller_instance.put_environment_by_id = mock_reponse + response = client.put('/environments/123', json=stub_environment_dump) + assert response.status_code == 200 + assert response.json() == { + 'message': 'Environment successfully updated', + } + mock_controller_instance.put_environment_by_id.assert_called_once_with( + '123', EnvironmentModel(**stub_environment_dump) + ) -def test_update_env_invalid_input(): +def test_update_environment_invalid_input(): response = client.put( - '/environments/123', json={'latitude': 'foo', 'longitude': 'bar'} + '/environments/123', json={'consignment': 'foo', 'delivery': 'bar'} ) assert response.status_code == 422 +def test_update_environment_not_found(stub_environment_dump, mock_controller_instance): + mock_reponse = AsyncMock(side_effect=HTTPException(status_code=404)) + mock_controller_instance.put_environment_by_id = mock_reponse + response = client.put('/environments/123', json=stub_environment_dump) + assert response.status_code == 404 + assert response.json() == {'detail': 'Not Found'} + mock_controller_instance.put_environment_by_id.assert_called_once_with( + '123', EnvironmentModel(**stub_environment_dump) + ) -def test_update_env_not_found(stub_env): - with patch.object( - EnvironmentController, - 'update_env_by_id', - side_effect=HTTPException(status_code=status.HTTP_404_NOT_FOUND), - ): - response = client.put('/environments/123', json=stub_env) - assert response.status_code == 404 - assert response.json() == {'detail': 'Not Found'} - - -def test_update_env_server_error(stub_env): - with patch.object( - EnvironmentController, - 'update_env_by_id', - side_effect=HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR - ), - ): - response = client.put('/environments/123', json=stub_env) - assert response.status_code == 500 - assert response.json() == {'detail': 'Internal Server Error'} - - -def test_delete_env(): - with patch.object( - EnvironmentController, - 'delete_env_by_id', - return_value=EnvironmentDeleted(env_id='123'), - ) as mock_delete_env: - response = client.delete('/environments/123') - assert response.status_code == 200 - assert response.json() == { - 'env_id': '123', - 'message': 'Environment successfully deleted', - } - mock_delete_env.assert_called_once_with('123') - - -def test_delete_env_server_error(): - with patch.object( - EnvironmentController, - 'delete_env_by_id', - side_effect=HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR - ), - ): - response = client.delete('/environments/123') - assert response.status_code == 500 - assert response.json() == {'detail': 'Internal Server Error'} - - -def test_simulate_env(stub_env_summary): - with patch.object( - EnvironmentController, - 'simulate_env', - return_value=EnvironmentSummary(**stub_env_summary), - ) as mock_simulate_env: - response = client.get('/environments/123/summary') - assert response.status_code == 200 - assert response.json() == stub_env_summary - mock_simulate_env.assert_called_once_with('123') - - -def test_simulate_env_not_found(): - with patch.object( - EnvironmentController, - 'simulate_env', - side_effect=HTTPException(status_code=status.HTTP_404_NOT_FOUND), - ) as mock_simulate_env: - response = client.get('/environments/123/summary') - assert response.status_code == 404 - assert response.json() == {'detail': 'Not Found'} - mock_simulate_env.assert_called_once_with('123') - - -def test_simulate_env_server_error(): - with patch.object( - EnvironmentController, - 'simulate_env', - side_effect=HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR - ), - ): - response = client.get('/environments/123/summary') - assert response.status_code == 500 - assert response.json() == {'detail': 'Internal Server Error'} - - -def test_read_rocketpy_env(): - with patch.object( - EnvironmentController, 'get_rocketpy_env_binary', return_value=b'rocketpy' - ) as mock_read_rocketpy_env: - response = client.get('/environments/123/rocketpy') - assert response.status_code == 203 - assert response.content == b'rocketpy' - assert response.headers['content-type'] == 'application/octet-stream' - mock_read_rocketpy_env.assert_called_once_with('123') - - -def test_read_rocketpy_env_not_found(): - with patch.object( - EnvironmentController, - 'get_rocketpy_env_binary', - side_effect=HTTPException(status_code=status.HTTP_404_NOT_FOUND), - ) as mock_read_rocketpy_env: - response = client.get('/environments/123/rocketpy') - assert response.status_code == 404 - assert response.json() == {'detail': 'Not Found'} - mock_read_rocketpy_env.assert_called_once_with('123') +def test_update_environment_server_error( + stub_environment_dump, mock_controller_instance +): + mock_response = AsyncMock(side_effect=HTTPException(status_code=500)) + mock_controller_instance.put_environment_by_id = mock_response + response = client.put('/environments/123', json=stub_environment_dump) + assert response.status_code == 500 + assert response.json() == {'detail': 'Internal Server Error'} + +def test_delete_environment(mock_controller_instance): + mock_reponse = AsyncMock(return_value=EnvironmentDeleted(environment_id='123')) + mock_controller_instance.delete_environment_by_id = mock_reponse + response = client.delete('/environments/123') + assert response.status_code == 200 + assert response.json() == { + 'message': 'Environment successfully deleted', + } + mock_controller_instance.delete_environment_by_id.assert_called_once_with('123') + +def test_delete_environment_server_error(mock_controller_instance): + mock_response = AsyncMock(side_effect=HTTPException(status_code=500)) + mock_controller_instance.delete_environment_by_id = mock_response + response = client.delete('/environments/123') + assert response.status_code == 500 + assert response.json() == {'detail': 'Internal Server Error'} + +def test_simulate_environment_success( + stub_environment_summary_dump, mock_controller_instance +): + mock_reponse = AsyncMock(return_value=EnvironmentSummary(**stub_environment_summary_dump)) + mock_controller_instance.get_environment_simulation = mock_reponse + response = client.get('/environments/123/summary') + assert response.status_code == 200 + assert response.json() == stub_environment_summary_dump + mock_controller_instance.get_environment_simulation.assert_called_once_with('123') + +def test_simulate_environment_not_found(mock_controller_instance): + mock_response = AsyncMock(side_effect=HTTPException(status_code=404)) + mock_controller_instance.get_environment_simulation = mock_response + response = client.get('/environments/123/summary') + assert response.status_code == 404 + assert response.json() == {'detail': 'Not Found'} + mock_controller_instance.get_environment_simulation.assert_called_once_with('123') + +def test_simulate_environment_server_error(mock_controller_instance): + mock_response = AsyncMock(side_effect=HTTPException(status_code=500)) + mock_controller_instance.get_environment_simulation = mock_response + response = client.get('/environments/123/summary') + assert response.status_code == 500 + assert response.json() == {'detail': 'Internal Server Error'} + +def test_read_rocketpy_environment(mock_controller_instance): + mock_response = AsyncMock(return_value=b'rocketpy') + mock_controller_instance.get_rocketpy_environment_binary = mock_response + response = client.get('/environments/123/rocketpy') + assert response.status_code == 203 + assert response.content == b'rocketpy' + assert response.headers['content-type'] == 'application/octet-stream' + mock_controller_instance.get_rocketpy_environment_binary.assert_called_once_with( + '123' + ) +def test_read_rocketpy_environment_not_found(mock_controller_instance): + mock_response = AsyncMock(side_effect=HTTPException(status_code=404)) + mock_controller_instance.get_rocketpy_environment_binary = mock_response + response = client.get('/environments/123/rocketpy') + assert response.status_code == 404 + assert response.json() == {'detail': 'Not Found'} + mock_controller_instance.get_rocketpy_environment_binary.assert_called_once_with( + '123' + ) -def test_read_rocketpy_env_server_error(): - with patch.object( - EnvironmentController, - 'get_rocketpy_env_binary', - side_effect=HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR - ), - ): - response = client.get('/environments/123/rocketpy') - assert response.status_code == 500 - assert response.json() == {'detail': 'Internal Server Error'} +def test_read_rocketpy_environment_server_error(mock_controller_instance): + mock_response = AsyncMock(side_effect=HTTPException(status_code=500)) + mock_controller_instance.get_rocketpy_environment_binary = mock_response + response = client.get('/environments/123/rocketpy') + assert response.status_code == 500 + assert response.json() == {'detail': 'Internal Server Error'} diff --git a/tests/test_routes/test_flights_route.py b/tests/test_routes/test_flights_route.py index e049efe..2e483da 100644 --- a/tests/test_routes/test_flights_route.py +++ b/tests/test_routes/test_flights_route.py @@ -31,7 +31,7 @@ def stub_flight(stub_env, stub_rocket): 'rail_length': 1, 'time_overshoot': True, 'terminate_on_apogee': True, - 'equations_of_motion': 'STANDARD', + 'equations_of_motion': 'standard', } return flight diff --git a/tests/test_routes/test_motors_route.py b/tests/test_routes/test_motors_route.py index 9e61493..41857fe 100644 --- a/tests/test_routes/test_motors_route.py +++ b/tests/test_routes/test_motors_route.py @@ -21,63 +21,63 @@ @pytest.fixture -def stub_motor_summary(): +def stub_motor_dump_summary(): motor_summary = MotorSummary() motor_summary_json = motor_summary.model_dump_json() return json.loads(motor_summary_json) - -def test_create_motor(stub_motor): +@pytest.fixture(autouse=True) +def mock_controller_instance(): + with patch( + "lib.routes.motor.MotorController", autospec=True + ) as mock_controller: + mock_controller_instance = mock_controller.return_value + mock_controller_instance.post_motor = Mock() + mock_controller_instance.get_motor_by_id = Mock() + mock_controller_instance.put_motor_by_id = Mock() + mock_controller_instance.delete_motor_by_id = Mock() + yield mock_controller_instance + +def test_create_motor(stub_motor_dump, mock_controller_instance): + mock_response = AsyncMock(return_value=MotorCreated(motor_id='123')) + mock_controller_instance.post_motor = mock_response with patch.object( - MotorController, - 'create_motor', - return_value=MotorCreated(motor_id='123'), - ) as mock_create_motor: - with patch.object( Motor, 'set_motor_kind', side_effect=None ) as mock_set_motor_kind: - response = client.post( - '/motors/', json=stub_motor, params={'motor_kind': 'HYBRID'} - ) - assert response.status_code == 200 - assert response.json() == { - 'motor_id': '123', - 'message': 'Motor successfully created', - } - mock_set_motor_kind.assert_called_once_with(MotorKinds.HYBRID) - mock_create_motor.assert_called_once_with(MotorModel(**stub_motor)) - - -def test_create_motor_optional_params(stub_motor): - stub_motor.update( - { - 'interpolation_method': 'LINEAR', - 'coordinate_system_orientation': 'NOZZLE_TO_COMBUSTION_CHAMBER', - 'reshape_thrust_curve': False, + response = client.post('/motors/', json=stub_motor_dump, params={'motor_kind': 'HYBRID'}) + assert response.status_code == 200 + assert response.json() == { + 'motor_id': '123', + 'message': 'motor successfully created', } - ) + mock_set_motor_kind.assert_called_once_with(MotorKinds.HYBRID) + mock_controller_instance.post_motor.assert_called_once_with( + MotorModel(**stub_motor_dump)) + +def test_create_motor_optional_params(stub_motor_dump, mock_controller_instance): + stub_motor_dump.update({ + 'interpolation_method': 'linear', + 'coordinate_system_orientation': 'nozzle_to_combustion_chamber', + 'reshape_thrust_curve': False, + }) + mock_response = AsyncMock(return_value=MotorCreated(motor_id='123')) + mock_controller_instance.post_motor = mock_response with patch.object( - MotorController, - 'create_motor', - return_value=MotorCreated(motor_id='123'), - ) as mock_create_motor: - with patch.object( - Motor, 'set_motor_kind', side_effect=None - ) as mock_set_motor_kind: - response = client.post( - '/motors/', json=stub_motor, params={'motor_kind': 'HYBRID'} - ) - assert response.status_code == 200 - assert response.json() == { - 'motor_id': '123', - 'message': 'Motor successfully created', - } - mock_set_motor_kind.assert_called_once_with(MotorKinds.HYBRID) - mock_create_motor.assert_called_once_with(MotorModel(**stub_motor)) - - -def test_create_generic_motor(stub_motor): - stub_motor.update( + Motor, 'set_motor_kind', side_effect=None + ) as mock_set_motor_kind: + response = client.post( + '/motors/', json=stub_motor_dump, params={'motor_kind': 'HYBRID'} + ) + assert response.status_code == 200 + assert response.json() == { + 'motor_id': '123', + 'message': 'motor successfully created', + } + mock_set_motor_kind.assert_called_once_with(MotorKinds.HYBRID) + mock_controller_instance.post_motor.assert_called_once_with(MotorModel(**stub_motor_dump)) + +def test_create_generic_motor(stub_motor_dump, mock_controller_instance): + stub_motor_dump.update( { 'chamber_radius': 0, 'chamber_height': 0, @@ -86,116 +86,96 @@ def test_create_generic_motor(stub_motor): 'nozzle_position': 0, } ) + mock_response = AsyncMock(return_value=MotorCreated(motor_id='123')) + mock_controller_instance.post_motor = mock_response with patch.object( - MotorController, - 'create_motor', - return_value=MotorCreated(motor_id='123'), - ) as mock_create_motor: - with patch.object( - Motor, 'set_motor_kind', side_effect=None - ) as mock_set_motor_kind: - response = client.post( - '/motors/', json=stub_motor, params={'motor_kind': 'GENERIC'} - ) - assert response.status_code == 200 - assert response.json() == { - 'motor_id': '123', - 'message': 'Motor successfully created', - } - mock_set_motor_kind.assert_called_once_with(MotorKinds.GENERIC) - mock_create_motor.assert_called_once_with(MotorModel(**stub_motor)) - - -def test_create_liquid_motor_level_tank(stub_motor, stub_level_tank): - stub_motor.update({'tanks': [stub_level_tank]}) + Motor, 'set_motor_kind', side_effect=None + ) as mock_set_motor_kind: + response = client.post( + '/motors/', json=stub_motor_dump, params={'motor_kind': 'GENERIC'} + ) + assert response.status_code == 200 + assert response.json() == { + 'motor_id': '123', + 'message': 'motor successfully created', + } + mock_set_motor_kind.assert_called_once_with(MotorKinds.GENERIC) + mock_controller_instance.post_motor.assert_called_once_with(MotorModel(**stub_motor_dump)) + +def test_create_liquid_motor_level_tank(stub_motor_dump, stub_level_tank, mock_controller_instance): + stub_motor_dump.update({'tanks': [stub_level_tank]}) + mock_response = AsyncMock(return_value=MotorCreated(motor_id='123')) + mock_controller_instance.post_motor = mock_response with patch.object( - MotorController, - 'create_motor', - return_value=MotorCreated(motor_id='123'), - ) as mock_create_motor: - with patch.object( - Motor, 'set_motor_kind', side_effect=None - ) as mock_set_motor_kind: - response = client.post( - '/motors/', json=stub_motor, params={'motor_kind': 'LIQUID'} - ) - assert response.status_code == 200 - assert response.json() == { - 'motor_id': '123', - 'message': 'Motor successfully created', - } - mock_set_motor_kind.assert_called_once_with(MotorKinds.LIQUID) - mock_create_motor.assert_called_once_with(MotorModel(**stub_motor)) - - -def test_create_liquid_motor_mass_flow_tank(stub_motor, stub_mass_flow_tank): - stub_motor.update({'tanks': [stub_mass_flow_tank]}) + Motor, 'set_motor_kind', side_effect=None + ) as mock_set_motor_kind: + response = client.post( + '/motors/', json=stub_motor_dump, params={'motor_kind': 'LIQUID'} + ) + assert response.status_code == 200 + assert response.json() == { + 'motor_id': '123', + 'message': 'motor successfully created', + } + mock_set_motor_kind.assert_called_once_with(MotorKinds.LIQUID) + mock_controller_instance.post_motor.assert_called_once_with(MotorModel(**stub_motor_dump)) + +def test_create_liquid_motor_mass_flow_tank(stub_motor_dump, stub_mass_flow_tank, mock_controller_instance): + stub_motor_dump.update({'tanks': [stub_mass_flow_tank]}) + mock_response = AsyncMock(return_value=MotorCreated(motor_id='123')) + mock_controller_instance.post_motor = mock_response with patch.object( - MotorController, - 'create_motor', - return_value=MotorCreated(motor_id='123'), - ) as mock_create_motor: - with patch.object( - Motor, 'set_motor_kind', side_effect=None - ) as mock_set_motor_kind: - response = client.post( - '/motors/', json=stub_motor, params={'motor_kind': 'LIQUID'} - ) - assert response.status_code == 200 - assert response.json() == { - 'motor_id': '123', - 'message': 'Motor successfully created', - } - mock_set_motor_kind.assert_called_once_with(MotorKinds.LIQUID) - mock_create_motor.assert_called_once_with(MotorModel(**stub_motor)) - - -def test_create_liquid_motor_ullage_tank(stub_motor, stub_ullage_tank): - stub_motor.update({'tanks': [stub_ullage_tank]}) + Motor, 'set_motor_kind', side_effect=None + ) as mock_set_motor_kind: + response = client.post( + '/motors/', json=stub_motor_dump, params={'motor_kind': 'LIQUID'} + ) + assert response.status_code == 200 + assert response.json() == { + 'motor_id': '123', + 'message': 'motor successfully created', + } + mock_set_motor_kind.assert_called_once_with(MotorKinds.LIQUID) + mock_controller_instance.post_motor.assert_called_once_with(MotorModel(**stub_motor_dump)) + +def test_create_liquid_motor_ullage_tank(stub_motor_dump, stub_ullage_tank, mock_controller_instance): + stub_motor_dump.update({'tanks': [stub_ullage_tank]}) + mock_response = AsyncMock(return_value=MotorCreated(motor_id='123')) + mock_controller_instance.post_motor = mock_response with patch.object( - MotorController, - 'create_motor', - return_value=MotorCreated(motor_id='123'), - ) as mock_create_motor: - with patch.object( - Motor, 'set_motor_kind', side_effect=None - ) as mock_set_motor_kind: - response = client.post( - '/motors/', json=stub_motor, params={'motor_kind': 'LIQUID'} - ) - assert response.status_code == 200 - assert response.json() == { - 'motor_id': '123', - 'message': 'Motor successfully created', - } - mock_set_motor_kind.assert_called_once_with(MotorKinds.LIQUID) - mock_create_motor.assert_called_once_with(MotorModel(**stub_motor)) - - -def test_create_liquid_motor_mass_tank(stub_motor, stub_mass_tank): - stub_motor.update({'tanks': [stub_mass_tank]}) + Motor, 'set_motor_kind', side_effect=None + ) as mock_set_motor_kind: + response = client.post( + '/motors/', json=stub_motor_dump, params={'motor_kind': 'LIQUID'} + ) + assert response.status_code == 200 + assert response.json() == { + 'motor_id': '123', + 'message': 'motor successfully created', + } + mock_set_motor_kind.assert_called_once_with(MotorKinds.LIQUID) + mock_controller_instance.post_motor.assert_called_once_with(MotorModel(**stub_motor_dump)) + +def test_create_liquid_motor_mass_tank(stub_motor_dump, stub_mass_tank, mock_controller_instance): + stub_motor_dump.update({'tanks': [stub_mass_tank]}) + mock_response = AsyncMock(return_value=MotorCreated(motor_id='123')) + mock_controller_instance.post_motor = mock_response with patch.object( - MotorController, - 'create_motor', - return_value=MotorCreated(motor_id='123'), - ) as mock_create_motor: - with patch.object( - Motor, 'set_motor_kind', side_effect=None - ) as mock_set_motor_kind: - response = client.post( - '/motors/', json=stub_motor, params={'motor_kind': 'LIQUID'} - ) - assert response.status_code == 200 - assert response.json() == { - 'motor_id': '123', - 'message': 'Motor successfully created', - } - mock_set_motor_kind.assert_called_once_with(MotorKinds.LIQUID) - mock_create_motor.assert_called_once_with(MotorModel(**stub_motor)) - - -def test_create_hybrid_motor(stub_motor, stub_level_tank): - stub_motor.update( + Motor, 'set_motor_kind', side_effect=None + ) as mock_set_motor_kind: + response = client.post( + '/motors/', json=stub_motor_dump, params={'motor_kind': 'LIQUID'} + ) + assert response.status_code == 200 + assert response.json() == { + 'motor_id': '123', + 'message': 'motor successfully created', + } + mock_set_motor_kind.assert_called_once_with(MotorKinds.LIQUID) + mock_controller_instance.post_motor.assert_called_once_with(MotorModel(**stub_motor_dump)) + +def test_create_hybrid_motor(stub_motor_dump, stub_level_tank, mock_controller_instance): + stub_motor_dump.update( { 'grain_number': 0, 'grain_density': 0, @@ -208,28 +188,24 @@ def test_create_hybrid_motor(stub_motor, stub_level_tank): 'tanks': [stub_level_tank], } ) + mock_response = AsyncMock(return_value=MotorCreated(motor_id='123')) + mock_controller_instance.post_motor = mock_response with patch.object( - MotorController, - 'create_motor', - return_value=MotorCreated(motor_id='123'), - ) as mock_create_motor: - with patch.object( - Motor, 'set_motor_kind', side_effect=None - ) as mock_set_motor_kind: - response = client.post( - '/motors/', json=stub_motor, params={'motor_kind': 'HYBRID'} - ) - assert response.status_code == 200 - assert response.json() == { - 'motor_id': '123', - 'message': 'Motor successfully created', - } - mock_set_motor_kind.assert_called_once_with(MotorKinds.HYBRID) - mock_create_motor.assert_called_once_with(MotorModel(**stub_motor)) - - -def test_create_solid_motor(stub_motor): - stub_motor.update( + Motor, 'set_motor_kind', side_effect=None + ) as mock_set_motor_kind: + response = client.post( + '/motors/', json=stub_motor_dump, params={'motor_kind': 'HYBRID'} + ) + assert response.status_code == 200 + assert response.json() == { + 'motor_id': '123', + 'message': 'motor successfully created', + } + mock_set_motor_kind.assert_called_once_with(MotorKinds.HYBRID) + mock_controller_instance.post_motor.assert_called_once_with(MotorModel(**stub_motor_dump)) + +def test_create_solid_motor(stub_motor_dump, mock_controller_instance): + stub_motor_dump.update( { 'grain_number': 0, 'grain_density': 0, @@ -240,25 +216,21 @@ def test_create_solid_motor(stub_motor): 'grain_separation': 0, } ) + mock_response = AsyncMock(return_value=MotorCreated(motor_id='123')) + mock_controller_instance.post_motor = mock_response with patch.object( - MotorController, - 'create_motor', - return_value=MotorCreated(motor_id='123'), - ) as mock_create_motor: - with patch.object( - Motor, 'set_motor_kind', side_effect=None - ) as mock_set_motor_kind: - response = client.post( - '/motors/', json=stub_motor, params={'motor_kind': 'SOLID'} - ) - assert response.status_code == 200 - assert response.json() == { - 'motor_id': '123', - 'message': 'Motor successfully created', - } - mock_set_motor_kind.assert_called_once_with(MotorKinds.SOLID) - mock_create_motor.assert_called_once_with(MotorModel(**stub_motor)) - + Motor, 'set_motor_kind', side_effect=None + ) as mock_set_motor_kind: + response = client.post( + '/motors/', json=stub_motor_dump, params={'motor_kind': 'SOLID'} + ) + assert response.status_code == 200 + assert response.json() == { + 'motor_id': '123', + 'message': 'motor successfully created', + } + mock_set_motor_kind.assert_called_once_with(MotorKinds.SOLID) + mock_controller_instance.post_motor.assert_called_once_with(MotorModel(**stub_motor_dump)) def test_create_motor_invalid_input(): response = client.post( @@ -266,82 +238,59 @@ def test_create_motor_invalid_input(): ) assert response.status_code == 422 - -def test_create_motor_server_error(stub_motor): +def test_create_motor_server_error(stub_motor_dump, mock_controller_instance): + mock_response = AsyncMock(side_effect=HTTPException(status_code=500)) + mock_controller_instance.post_motor = mock_response with patch.object( - MotorController, - 'create_motor', - side_effect=HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR - ), - ): - response = client.post( - '/motors/', json=stub_motor, params={'motor_kind': 'HYBRID'} - ) + Motor, 'set_motor_kind', side_effect=None + ) as mock_set_motor_kind: + response = client.post('/motors/', json=stub_motor_dump, params={'motor_kind': 'HYBRID'}) assert response.status_code == 500 assert response.json() == {'detail': 'Internal Server Error'} - - -def test_read_motor(stub_motor): - stub_motor.update({'selected_motor_kind': 'HYBRID'}) + mock_set_motor_kind.assert_called_once_with(MotorKinds.HYBRID) + mock_controller_instance.post_motor.assert_called_once_with(MotorModel(**stub_motor_dump)) + +def test_read_motor(stub_motor_dump, mock_controller_instance): + mock_response = AsyncMock(return_value=MotorView(**stub_motor_dump)) + mock_controller_instance.get_motor_by_id = mock_response + response = client.get('/motors/123') + assert response.status_code == 200 + assert response.json() == stub_motor_dump + mock_controller_instance.get_motor_by_id.assert_called_once_with('123') + +def test_read_motor_not_found(mock_controller_instance): + mock_response = AsyncMock(side_effect=HTTPException(status_code=404)) + mock_controller_instance.get_motor_by_id = mock_response + response = client.get('/motors/123') + assert response.status_code == 404 + assert response.json() == {'detail': 'Not Found'} + mock_controller_instance.get_motor_by_id.assert_called_once_with('123') + +def test_read_motor_server_error(mock_controller_instance): + mock_response = AsyncMock(side_effect=HTTPException(status_code=500)) + mock_controller_instance.get_motor_by_id = mock_response + response = client.get('/motors/123') + assert response.status_code == 500 + assert response.json() == {'detail': 'Internal Server Error'} + +def test_update_motor(stub_motor_dump, mock_controller_instance): + mock_response = AsyncMock(return_value=MotorUpdated(motor_id='123')) + mock_controller_instance.put_motor_by_id = mock_response with patch.object( - MotorController, - 'get_motor_by_id', - return_value=MotorView(**stub_motor), - ) as mock_read_motor: - response = client.get('/motors/123') + Motor, 'set_motor_kind', side_effect=None + ) as mock_set_motor_kind: + response = client.put( + '/motors/123', json=stub_motor_dump, params={'motor_kind': 'HYBRID'} + ) assert response.status_code == 200 - assert response.json() == stub_motor - mock_read_motor.assert_called_once_with('123') - - -def test_read_motor_not_found(): - with patch.object( - MotorController, - 'get_motor_by_id', - side_effect=HTTPException(status_code=status.HTTP_404_NOT_FOUND), - ) as mock_read_motor: - response = client.get('/motors/123') - assert response.status_code == 404 - assert response.json() == {'detail': 'Not Found'} - mock_read_motor.assert_called_once_with('123') - - -def test_read_motor_server_error(): - with patch.object( - MotorController, - 'get_motor_by_id', - side_effect=HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR - ), - ): - response = client.get('/motors/123') - assert response.status_code == 500 - assert response.json() == {'detail': 'Internal Server Error'} - - -def test_update_motor(stub_motor): - with patch.object( - MotorController, - 'update_motor_by_id', - return_value=MotorUpdated(motor_id='123'), - ) as mock_update_motor: - with patch.object( - Motor, 'set_motor_kind', side_effect=None - ) as mock_set_motor_kind: - response = client.put( - '/motors/123', json=stub_motor, params={'motor_kind': 'HYBRID'} - ) - assert response.status_code == 200 - assert response.json() == { - 'motor_id': '123', - 'message': 'Motor successfully updated', - } - mock_update_motor.assert_called_once_with( - '123', MotorModel(**stub_motor) - ) - mock_set_motor_kind.assert_called_once_with(MotorKinds.HYBRID) - + assert response.json() == { + 'motor_id': '123', + 'message': 'motor successfully updated', + } + mock_set_motor_kind.assert_called_once_with(MotorKinds.HYBRID) + mock_controller_instance.put_motor_by_id.assert_called_once_with( + '123', MotorModel(**stub_motor_dump) + ) def test_update_motor_invalid_input(): response = client.put( @@ -351,131 +300,104 @@ def test_update_motor_invalid_input(): ) assert response.status_code == 422 - -def test_update_motor_not_found(stub_motor): +def test_update_motor_not_found(stub_motor_dump, mock_controller_instance): + mock_response = AsyncMock(side_effect=HTTPException(status_code=404)) + mock_controller_instance.put_motor_by_id = mock_response with patch.object( - MotorController, - 'update_motor_by_id', - side_effect=HTTPException(status_code=status.HTTP_404_NOT_FOUND), - ): + Motor, 'set_motor_kind', side_effect=None + ) as mock_set_motor_kind: response = client.put( - '/motors/123', json=stub_motor, params={'motor_kind': 'HYBRID'} + '/motors/123', json=stub_motor_dump, params={'motor_kind': 'HYBRID'} ) assert response.status_code == 404 assert response.json() == {'detail': 'Not Found'} + mock_set_motor_kind.assert_called_once_with(MotorKinds.HYBRID) + mock_controller_instance.put_motor_by_id.assert_called_once_with( + '123', MotorModel(**stub_motor_dump) + ) - -def test_update_motor_server_error(stub_motor): +def test_update_motor_server_error(stub_motor_dump, mock_controller_instance): + mock_response = AsyncMock(side_effect=HTTPException(status_code=500)) + mock_controller_instance.put_motor_by_id = mock_response with patch.object( - MotorController, - 'update_motor_by_id', - side_effect=HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR - ), - ): + Motor, 'set_motor_kind', side_effect=None + ) as mock_set_motor_kind: response = client.put( - '/motors/123', json=stub_motor, params={'motor_kind': 'HYBRID'} + '/motors/123', json=stub_motor_dump, params={'motor_kind': 'HYBRID'} ) assert response.status_code == 500 assert response.json() == {'detail': 'Internal Server Error'} + mock_set_motor_kind.assert_called_once_with(MotorKinds.HYBRID) + mock_controller_instance.put_motor_by_id.assert_called_once_with( + '123', MotorModel(**stub_motor_dump) + ) - -def test_delete_motor(): - with patch.object( - MotorController, - 'delete_motor_by_id', - return_value=MotorDeleted(motor_id='123'), - ) as mock_delete_motor: - response = client.delete('/motors/123') - assert response.status_code == 200 - assert response.json() == { - 'motor_id': '123', - 'message': 'Motor successfully deleted', - } - mock_delete_motor.assert_called_once_with('123') - - -def test_delete_motor_server_error(): - with patch.object( - MotorController, - 'delete_motor_by_id', - side_effect=HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR - ), - ): - response = client.delete('/motors/123') - assert response.status_code == 500 - assert response.json() == {'detail': 'Internal Server Error'} - - -def test_simulate_motor(stub_motor_summary): - with patch.object( - MotorController, - 'simulate_motor', - return_value=MotorSummary(**stub_motor_summary), - ) as mock_simulate_motor: - response = client.get('/motors/123/summary') - assert response.status_code == 200 - assert response.json() == stub_motor_summary - mock_simulate_motor.assert_called_once_with('123') - - -def test_simulate_motor_not_found(): - with patch.object( - MotorController, - 'simulate_motor', - side_effect=HTTPException(status_code=status.HTTP_404_NOT_FOUND), - ) as mock_simulate_motor: - response = client.get('/motors/123/summary') - assert response.status_code == 404 - assert response.json() == {'detail': 'Not Found'} - mock_simulate_motor.assert_called_once_with('123') - - -def test_simulate_motor_server_error(): - with patch.object( - MotorController, - 'simulate_motor', - side_effect=HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR - ), - ): - response = client.get('/motors/123/summary') - assert response.status_code == 500 - assert response.json() == {'detail': 'Internal Server Error'} - - -def test_read_rocketpy_motor(): - with patch.object( - MotorController, 'get_rocketpy_motor_binary', return_value=b'rocketpy' - ) as mock_read_rocketpy_motor: - response = client.get('/motors/123/rocketpy') - assert response.status_code == 203 - assert response.content == b'rocketpy' - assert response.headers['content-type'] == 'application/octet-stream' - mock_read_rocketpy_motor.assert_called_once_with('123') - - -def test_read_rocketpy_motor_not_found(): - with patch.object( - MotorController, - 'get_rocketpy_motor_binary', - side_effect=HTTPException(status_code=status.HTTP_404_NOT_FOUND), - ) as mock_read_rocketpy_motor: - response = client.get('/motors/123/rocketpy') - assert response.status_code == 404 - assert response.json() == {'detail': 'Not Found'} - mock_read_rocketpy_motor.assert_called_once_with('123') - - -def test_read_rocketpy_motor_server_error(): - with patch.object( - MotorController, - 'get_rocketpy_motor_binary', - side_effect=HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR - ), - ): - response = client.get('/motors/123/rocketpy') - assert response.status_code == 500 - assert response.json() == {'detail': 'Internal Server Error'} +def test_delete_motor(mock_controller_instance): + mock_response = AsyncMock(return_value=MotorDeleted(motor_id='123')) + mock_controller_instance.delete_motor_by_id = mock_response + response = client.delete('/motors/123') + assert response.status_code == 200 + assert response.json() == { + 'motor_id': '123', + 'message': 'motor successfully deleted', + } + mock_controller_instance.delete_motor_by_id.assert_called_once_with('123') + +def test_delete_motor_server_error(mock_controller_instance): + mock_response = AsyncMock(side_effect=HTTPException(status_code=500)) + mock_controller_instance.delete_motor_by_id = mock_response + response = client.delete('/motors/123') + assert response.status_code == 500 + assert response.json() == {'detail': 'Internal Server Error'} + mock_controller_instance.delete_motor_by_id.assert_called_once_with('123') + + +def test_simulate_motor(mock_controller_instance, stub_motor_dump_summary): + mock_response = AsyncMock(return_value=MotorSummary(**stub_motor_dump_summary)) + mock_controller_instance.simulate_motor = mock_response + response = client.get('/motors/123/summary') + assert response.status_code == 200 + assert response.json() == stub_motor_dump_summary + mock_controller_instance.simulate_motor.assert_called_once_with('123') + + +def test_simulate_motor_not_found(mock_controller_instance): + mock_response = AsyncMock(side_effect=HTTPException(status_code=404)) + mock_controller_instance.simulate_motor = mock_response + response = client.get('/motors/123/summary') + assert response.status_code == 404 + assert response.json() == {'detail': 'Not Found'} + mock_controller_instance.simulate_motor.assert_called_once_with('123') + +def test_simulate_motor_server_error(mock_controller_instance): + mock_response = AsyncMock(side_effect=HTTPException(status_code=500)) + mock_controller_instance.simulate_motor = mock_response + response = client.get('/motors/123/summary') + assert response.status_code == 500 + assert response.json() == {'detail': 'Internal Server Error'} + mock_controller_instance.simulate_motor.assert_called_once_with('123') + +def test_read_rocketpy_motor(mock_controller_instance): + mock_response = AsyncMock(return_value=b'rocketpy') + mock_controller_instance.get_rocketpy_motor_binary = mock_response + response = client.get('/motors/123/rocketpy') + assert response.status_code == 203 + assert response.content == b'rocketpy' + assert response.headers['content-type'] == 'application/octet-stream' + mock_controller_instance.get_rocketpy_motor_binary.assert_called_once_with('123') + +def test_read_rocketpy_motor_not_found(mock_controller_instance): + mock_response = AsyncMock(side_effect=HTTPException(status_code=404)) + mock_controller_instance.get_rocketpy_motor_binary = mock_response + response = client.get('/motors/123/rocketpy') + assert response.status_code == 404 + assert response.json() == {'detail': 'Not Found'} + mock_controller_instance.get_rocketpy_motor_binary.assert_called_once_with('123') + +def test_read_rocketpy_motor_server_error(mock_controller_instance): + mock_response = AsyncMock(side_effect=HTTPException(status_code=500)) + mock_controller_instance.get_rocketpy_motor_binary = mock_response + response = client.get('/motors/123/rocketpy') + assert response.status_code == 500 + assert response.json() == {'detail': 'Internal Server Error'} + mock_controller_instance.get_rocketpy_motor_binary.assert_called_once_with('123') From 0eb8d8a17025da3ce0f81c6cb1f8d4697ca24ee4 Mon Sep 17 00:00:00 2001 From: Gabriel Barberini Date: Sat, 15 Feb 2025 19:56:25 -0300 Subject: [PATCH 06/34] refactor test rockets route --- lib/controllers/flight.py | 10 +- lib/models/motor.py | 2 +- lib/routes/environment.py | 7 +- lib/routes/flight.py | 19 +- lib/routes/motor.py | 7 +- lib/routes/rocket.py | 7 +- tests/test_routes/conftest.py | 32 +- tests/test_routes/test_environments_route.py | 38 +- tests/test_routes/test_flights_route.py | 26 +- tests/test_routes/test_motors_route.py | 73 +- tests/test_routes/test_rockets_route.py | 688 +++++++++---------- 11 files changed, 449 insertions(+), 460 deletions(-) diff --git a/lib/controllers/flight.py b/lib/controllers/flight.py index 0612eb5..d01514f 100644 --- a/lib/controllers/flight.py +++ b/lib/controllers/flight.py @@ -22,15 +22,15 @@ def __init__(self): super().__init__(models=[FlightModel]) @controller_exception_handler - async def update_env_by_flight_id( - self, flight_id: str, *, env: EnvironmentModel + async def update_environment_by_flight_id( + self, flight_id: str, *, environment: EnvironmentModel ) -> FlightUpdated: """ - Update a models.Flight.env in the database. + Update a models.Flight.environment in the database. Args: flight_id: str - env: models.Env + environment: models.Environment Returns: views.FlightUpdated @@ -39,7 +39,7 @@ async def update_env_by_flight_id( HTTP 404 Not Found: If the flight is not found in the database. """ flight = await self.get_flight_by_id(flight_id) - flight.environment = env + flight.environment = environment self.update_flight_by_id(flight_id, flight) return FlightUpdated(flight_id=flight_id) diff --git a/lib/models/motor.py b/lib/models/motor.py index 1f658e9..85517a3 100644 --- a/lib/models/motor.py +++ b/lib/models/motor.py @@ -1,5 +1,5 @@ from enum import Enum -from typing import Optional, Tuple, List, Union, Self, ClassVar +from typing import Optional, Tuple, List, Union, Self, ClassVar, Literal from pydantic import PrivateAttr, model_validator from lib.models.interface import ApiBaseModel diff --git a/lib/routes/environment.py b/lib/routes/environment.py index 5525a1a..94e339e 100644 --- a/lib/routes/environment.py +++ b/lib/routes/environment.py @@ -94,14 +94,15 @@ async def delete_environment(environment_id: str) -> EnvironmentDeleted: status_code=203, response_class=Response, ) -async def read_rocketpy_env(environment_id: str): +async def get_rocketpy_environment_binary(environment_id: str): """ - Loads rocketpy.environment as a dill binary + Loads rocketpy.environment as a dill binary. + Currently only amd64 architecture is supported. ## Args ``` environment_id: str ``` """ - with tracer.start_as_current_span("read_rocketpy_env"): + with tracer.start_as_current_span("get_rocketpy_environment_binary"): headers = { 'Content-Disposition': f'attachment; filename="rocketpy_environment_{environment_id}.dill"' } diff --git a/lib/routes/flight.py b/lib/routes/flight.py index 2e71499..4ae5d9c 100644 --- a/lib/routes/flight.py +++ b/lib/routes/flight.py @@ -99,14 +99,15 @@ async def delete_flight(flight_id: str) -> FlightDeleted: status_code=203, response_class=Response, ) -async def read_rocketpy_flight(flight_id: str): +async def get_rocketpy_flight_binary(flight_id: str): """ - Loads rocketpy.flight as a dill binary + Loads rocketpy.flight as a dill binary. + Currently only amd64 architecture is supported. ## Args ``` flight_id: str ``` """ - with tracer.start_as_current_span("read_rocketpy_flight"): + with tracer.start_as_current_span("get_rocketpy_flight_binary"): controller = FlightController() headers = { 'Content-Disposition': f'attachment; filename="rocketpy_flight_{flight_id}.dill"' @@ -120,21 +121,21 @@ async def read_rocketpy_flight(flight_id: str): ) -@router.put("/{flight_id}/env") -async def update_flight_env(flight_id: str, env: EnvironmentModel) -> FlightUpdated: +@router.put("/{flight_id}/environment") +async def update_flight_environment(flight_id: str, environment: EnvironmentModel) -> FlightUpdated: """ Updates flight environment ## Args ``` flight_id: Flight ID - env: env object as JSON + environment: env object as JSON ``` """ - with tracer.start_as_current_span("update_flight_env"): + with tracer.start_as_current_span("update_flight_environment"): controller = FlightController() - return await controller.update_env_by_flight_id( - flight_id, env=env + return await controller.update_environment_by_flight_id( + flight_id, environment=environment ) diff --git a/lib/routes/motor.py b/lib/routes/motor.py index ced0169..969a419 100644 --- a/lib/routes/motor.py +++ b/lib/routes/motor.py @@ -96,14 +96,15 @@ async def delete_motor(motor_id: str) -> MotorDeleted: status_code=203, response_class=Response, ) -async def read_rocketpy_motor(motor_id: str): +async def get_rocketpy_motor_binary(motor_id: str): """ - Loads rocketpy.motor as a dill binary + Loads rocketpy.motor as a dill binary. + Currently only amd64 architecture is supported. ## Args ``` motor_id: str ``` """ - with tracer.start_as_current_span("read_rocketpy_motor"): + with tracer.start_as_current_span("get_rocketpy_motor_binary"): headers = { 'Content-Disposition': f'attachment; filename="rocketpy_motor_{motor_id}.dill"' } diff --git a/lib/routes/rocket.py b/lib/routes/rocket.py index 711582b..d9be0c4 100644 --- a/lib/routes/rocket.py +++ b/lib/routes/rocket.py @@ -97,14 +97,15 @@ async def delete_rocket(rocket_id: str) -> RocketDeleted: status_code=203, response_class=Response, ) -async def read_rocketpy_rocket(rocket_id: str): +async def get_rocketpy_rocket_binary(rocket_id: str): """ - Loads rocketpy.rocket as a dill binary + Loads rocketpy.rocket as a dill binary. + Currently only amd64 architecture is supported. ## Args ``` rocket_id: str ``` """ - with tracer.start_as_current_span("read_rocketpy_rocket"): + with tracer.start_as_current_span("get_rocketpy_rocket_binary"): headers = { 'Content-Disposition': f'attachment; filename="rocketpy_rocket_{rocket_id}.dill"' } diff --git a/tests/test_routes/conftest.py b/tests/test_routes/conftest.py index e793714..260a907 100644 --- a/tests/test_routes/conftest.py +++ b/tests/test_routes/conftest.py @@ -45,14 +45,14 @@ def stub_tank_dump(): @pytest.fixture -def stub_level_tank(stub_tank): - stub_tank.update({'tank_kind': TankKinds.LEVEL, 'liquid_height': 0}) - return stub_tank +def stub_level_tank_dump(stub_tank_dump): + stub_tank_dump.update({'tank_kind': TankKinds.LEVEL, 'liquid_height': 0}) + return stub_tank_dump @pytest.fixture -def stub_mass_flow_tank(stub_tank): - stub_tank.update( +def stub_mass_flow_tank_dump(stub_tank_dump): + stub_tank_dump.update( { 'tank_kind': TankKinds.MASS_FLOW, 'gas_mass_flow_rate_in': 0, @@ -63,21 +63,21 @@ def stub_mass_flow_tank(stub_tank): 'initial_gas_mass': 0, } ) - return stub_tank + return stub_tank_dump @pytest.fixture -def stub_ullage_tank(stub_tank): - stub_tank.update({'tank_kind': TankKinds.ULLAGE, 'ullage': 0}) - return stub_tank +def stub_ullage_tank_dump(stub_tank_dump): + stub_tank_dump.update({'tank_kind': TankKinds.ULLAGE, 'ullage': 0}) + return stub_tank_dump @pytest.fixture -def stub_mass_tank(stub_tank): - stub_tank.update( +def stub_mass_tank_dump(stub_tank_dump): + stub_tank_dump.update( {'tank_kind': TankKinds.MASS, 'liquid_mass': 0, 'gas_mass': 0} ) - return stub_tank + return stub_tank_dump @pytest.fixture @@ -109,9 +109,9 @@ def stub_fins_dump(): @pytest.fixture -def stub_rocket_dump(stub_motor, stub_nose_cone, stub_fins): +def stub_rocket_dump(stub_motor_dump, stub_nose_cone_dump, stub_fins_dump): rocket = RocketModel( - motor=stub_motor, + motor=stub_motor_dump, radius=0, mass=0, motor_position=0, @@ -119,8 +119,8 @@ def stub_rocket_dump(stub_motor, stub_nose_cone, stub_fins): inertia=[0, 0, 0], power_off_drag=[(0, 0)], power_on_drag=[(0, 0)], - nose=stub_nose_cone, - fins=[stub_fins], + nose=stub_nose_cone_dump, + fins=[stub_fins_dump], coordinate_system_orientation='tail_to_nose', ) rocket_json = rocket.model_dump_json() diff --git a/tests/test_routes/test_environments_route.py b/tests/test_routes/test_environments_route.py index 761a975..c1f3fe3 100644 --- a/tests/test_routes/test_environments_route.py +++ b/tests/test_routes/test_environments_route.py @@ -1,13 +1,14 @@ -from unittest.mock import patch +from unittest.mock import patch, Mock, AsyncMock import json import pytest from fastapi.testclient import TestClient -from fastapi import HTTPException, status +from fastapi import HTTPException from lib.models.environment import EnvironmentModel -from lib.controllers.environment import EnvironmentController from lib.views.environment import ( + EnvironmentView, EnvironmentCreated, EnvironmentUpdated, + EnvironmentRetrieved, EnvironmentDeleted, EnvironmentSummary, ) @@ -22,6 +23,7 @@ def stub_environment_summary_dump(): env_summary_json = env_summary.model_dump_json() return json.loads(env_summary_json) + @pytest.fixture(autouse=True) def mock_controller_instance(): with patch( @@ -34,6 +36,7 @@ def mock_controller_instance(): mock_controller_instance.delete_environment_by_id = Mock() yield mock_controller_instance + def test_create_environment(stub_environment_dump, mock_controller_instance): mock_response = AsyncMock(return_value=EnvironmentCreated(environment_id='123')) mock_controller_instance.post_environment = mock_response @@ -47,6 +50,7 @@ def test_create_environment(stub_environment_dump, mock_controller_instance): EnvironmentModel(**stub_environment_dump) ) + def test_create_environment_optional_params(stub_environment_dump, mock_controller_instance): stub_environment_dump.update({ 'latitude': 0, @@ -65,12 +69,14 @@ def test_create_environment_optional_params(stub_environment_dump, mock_controll 'message': 'Environment successfully created', } + def test_create_environment_invalid_input(): response = client.post( '/environments/', json={'latitude': 'foo', 'longitude': 'bar'} ) assert response.status_code == 422 + def test_create_environment_server_error( stub_environment_dump, mock_controller_instance ): @@ -80,8 +86,9 @@ def test_create_environment_server_error( assert response.status_code == 500 assert response.json() == {'detail': 'Internal Server Error'} -def test_read_environment(stub_env): - stub_environment_out = EnvironmentModelOut(environment_id='123', **stub_environment_dump) + +def test_read_environment(stub_environment_dump, mock_controller_instance): + stub_environment_out = EnvironmentView(environment_id='123', **stub_environment_dump) mock_response = AsyncMock( return_value=EnvironmentRetrieved(environment=stub_environment_out) ) @@ -92,6 +99,8 @@ def test_read_environment(stub_env): 'message': 'Environment successfully retrieved', 'environment': json.loads(stub_environment_out.model_dump_json()), } + mock_controller_instance.get_environment_by_id.assert_called_once_with('123') + def test_read_environment_not_found(mock_controller_instance): mock_response = AsyncMock(side_effect=HTTPException(status_code=404)) @@ -101,6 +110,7 @@ def test_read_environment_not_found(mock_controller_instance): assert response.json() == {'detail': 'Not Found'} mock_controller_instance.get_environment_by_id.assert_called_once_with('123') + def test_read_environment_server_error(mock_controller_instance): mock_response = AsyncMock(side_effect=HTTPException(status_code=500)) mock_controller_instance.get_environment_by_id = mock_response @@ -108,6 +118,7 @@ def test_read_environment_server_error(mock_controller_instance): assert response.status_code == 500 assert response.json() == {'detail': 'Internal Server Error'} + def test_update_environment(stub_environment_dump, mock_controller_instance): mock_reponse = AsyncMock(return_value=EnvironmentUpdated(environment_id='123')) mock_controller_instance.put_environment_by_id = mock_reponse @@ -120,12 +131,14 @@ def test_update_environment(stub_environment_dump, mock_controller_instance): '123', EnvironmentModel(**stub_environment_dump) ) + def test_update_environment_invalid_input(): response = client.put( '/environments/123', json={'consignment': 'foo', 'delivery': 'bar'} ) assert response.status_code == 422 + def test_update_environment_not_found(stub_environment_dump, mock_controller_instance): mock_reponse = AsyncMock(side_effect=HTTPException(status_code=404)) mock_controller_instance.put_environment_by_id = mock_reponse @@ -136,6 +149,7 @@ def test_update_environment_not_found(stub_environment_dump, mock_controller_ins '123', EnvironmentModel(**stub_environment_dump) ) + def test_update_environment_server_error( stub_environment_dump, mock_controller_instance ): @@ -145,6 +159,7 @@ def test_update_environment_server_error( assert response.status_code == 500 assert response.json() == {'detail': 'Internal Server Error'} + def test_delete_environment(mock_controller_instance): mock_reponse = AsyncMock(return_value=EnvironmentDeleted(environment_id='123')) mock_controller_instance.delete_environment_by_id = mock_reponse @@ -155,6 +170,7 @@ def test_delete_environment(mock_controller_instance): } mock_controller_instance.delete_environment_by_id.assert_called_once_with('123') + def test_delete_environment_server_error(mock_controller_instance): mock_response = AsyncMock(side_effect=HTTPException(status_code=500)) mock_controller_instance.delete_environment_by_id = mock_response @@ -162,6 +178,7 @@ def test_delete_environment_server_error(mock_controller_instance): assert response.status_code == 500 assert response.json() == {'detail': 'Internal Server Error'} + def test_simulate_environment_success( stub_environment_summary_dump, mock_controller_instance ): @@ -172,6 +189,7 @@ def test_simulate_environment_success( assert response.json() == stub_environment_summary_dump mock_controller_instance.get_environment_simulation.assert_called_once_with('123') + def test_simulate_environment_not_found(mock_controller_instance): mock_response = AsyncMock(side_effect=HTTPException(status_code=404)) mock_controller_instance.get_environment_simulation = mock_response @@ -180,6 +198,7 @@ def test_simulate_environment_not_found(mock_controller_instance): assert response.json() == {'detail': 'Not Found'} mock_controller_instance.get_environment_simulation.assert_called_once_with('123') + def test_simulate_environment_server_error(mock_controller_instance): mock_response = AsyncMock(side_effect=HTTPException(status_code=500)) mock_controller_instance.get_environment_simulation = mock_response @@ -187,7 +206,8 @@ def test_simulate_environment_server_error(mock_controller_instance): assert response.status_code == 500 assert response.json() == {'detail': 'Internal Server Error'} -def test_read_rocketpy_environment(mock_controller_instance): + +def test_read_rocketpy_environment_binary(mock_controller_instance): mock_response = AsyncMock(return_value=b'rocketpy') mock_controller_instance.get_rocketpy_environment_binary = mock_response response = client.get('/environments/123/rocketpy') @@ -198,7 +218,8 @@ def test_read_rocketpy_environment(mock_controller_instance): '123' ) -def test_read_rocketpy_environment_not_found(mock_controller_instance): + +def test_read_rocketpy_environment_binary_not_found(mock_controller_instance): mock_response = AsyncMock(side_effect=HTTPException(status_code=404)) mock_controller_instance.get_rocketpy_environment_binary = mock_response response = client.get('/environments/123/rocketpy') @@ -208,7 +229,8 @@ def test_read_rocketpy_environment_not_found(mock_controller_instance): '123' ) -def test_read_rocketpy_environment_server_error(mock_controller_instance): + +def test_read_rocketpy_environment_binary_server_error(mock_controller_instance): mock_response = AsyncMock(side_effect=HTTPException(status_code=500)) mock_controller_instance.get_rocketpy_environment_binary = mock_response response = client.get('/environments/123/rocketpy') diff --git a/tests/test_routes/test_flights_route.py b/tests/test_routes/test_flights_route.py index 2e483da..0f0c98a 100644 --- a/tests/test_routes/test_flights_route.py +++ b/tests/test_routes/test_flights_route.py @@ -23,11 +23,11 @@ @pytest.fixture -def stub_flight(stub_env, stub_rocket): +def stub_flight(stub_environment_dump, stub_rocket_dump): flight = { 'name': 'Test Flight', - 'environment': stub_env, - 'rocket': stub_rocket, + 'environment': stub_environment_dump, + 'rocket': stub_rocket_dump, 'rail_length': 1, 'time_overshoot': True, 'terminate_on_apogee': True, @@ -119,11 +119,11 @@ def test_create_flight_server_error(stub_flight): assert response.json() == {'detail': 'Internal Server Error'} -def test_read_flight(stub_flight, stub_rocket, stub_motor): - del stub_rocket['motor'] +def test_read_flight(stub_flight, stub_rocket_dump, stub_motor_dump): + del stub_rocket_dump['motor'] del stub_flight['rocket'] - motor_view = MotorView(**stub_motor, selected_motor_kind=MotorKinds.HYBRID) - rocket_view = RocketView(**stub_rocket, motor=motor_view) + motor_view = MotorView(**stub_motor_dump, selected_motor_kind=MotorKinds.HYBRID) + rocket_view = RocketView(**stub_rocket_dump, motor=motor_view) flight_view = FlightView(**stub_flight, rocket=rocket_view) with patch.object( FlightController, @@ -186,22 +186,22 @@ def test_update_flight(stub_flight): mock_set_motor_kind.assert_called_once_with(MotorKinds.GENERIC) -def test_update_env_by_flight_id(stub_env): +def test_update_env_by_flight_id(stub_environment_dump): with patch.object( FlightController, 'update_env_by_flight_id', return_value=FlightUpdated(flight_id='123'), ) as mock_update_flight: - response = client.put('/flights/123/env', json=stub_env) + response = client.put('/flights/123/env', json=stub_environment_dump) assert response.status_code == 200 assert response.json() == { 'flight_id': '123', 'message': 'Flight successfully updated', } - mock_update_flight.assert_called_once_with('123', env=EnvironmentModel(**stub_env)) + mock_update_flight.assert_called_once_with('123', env=EnvironmentModel(**stub_environment_dump)) -def test_update_rocket_by_flight_id(stub_rocket): +def test_update_rocket_by_flight_id(stub_rocket_dump): with patch.object( FlightController, 'update_rocket_by_flight_id', @@ -209,7 +209,7 @@ def test_update_rocket_by_flight_id(stub_rocket): ) as mock_update_flight: response = client.put( '/flights/123/rocket', - json=stub_rocket, + json=stub_rocket_dump, params={'motor_kind': 'GENERIC'}, ) assert response.status_code == 200 @@ -219,7 +219,7 @@ def test_update_rocket_by_flight_id(stub_rocket): } assert mock_update_flight.call_count == 1 assert mock_update_flight.call_args[0][0] == '123' - assert mock_update_flight.call_args[1]['rocket'].model_dump() == RocketModel(**stub_rocket).model_dump() + assert mock_update_flight.call_args[1]['rocket'].model_dump() == RocketModel(**stub_rocket_dump).model_dump() def test_update_env_by_flight_id_invalid_input(): diff --git a/tests/test_routes/test_motors_route.py b/tests/test_routes/test_motors_route.py index 41857fe..1b22bfd 100644 --- a/tests/test_routes/test_motors_route.py +++ b/tests/test_routes/test_motors_route.py @@ -1,15 +1,15 @@ -from unittest.mock import patch +from unittest.mock import patch, AsyncMock, Mock import json import pytest from fastapi.testclient import TestClient -from fastapi import HTTPException, status +from fastapi import HTTPException from lib.models.motor import ( MotorModel, MotorKinds, ) -from lib.controllers.motor import MotorController from lib.views.motor import ( MotorCreated, + MotorRetrieved, MotorUpdated, MotorDeleted, MotorSummary, @@ -26,6 +26,7 @@ def stub_motor_dump_summary(): motor_summary_json = motor_summary.model_dump_json() return json.loads(motor_summary_json) + @pytest.fixture(autouse=True) def mock_controller_instance(): with patch( @@ -38,12 +39,13 @@ def mock_controller_instance(): mock_controller_instance.delete_motor_by_id = Mock() yield mock_controller_instance + def test_create_motor(stub_motor_dump, mock_controller_instance): mock_response = AsyncMock(return_value=MotorCreated(motor_id='123')) mock_controller_instance.post_motor = mock_response with patch.object( - Motor, 'set_motor_kind', side_effect=None - ) as mock_set_motor_kind: + MotorModel, 'set_motor_kind', side_effect=None + ) as mock_set_motor_kind: response = client.post('/motors/', json=stub_motor_dump, params={'motor_kind': 'HYBRID'}) assert response.status_code == 200 assert response.json() == { @@ -54,6 +56,7 @@ def test_create_motor(stub_motor_dump, mock_controller_instance): mock_controller_instance.post_motor.assert_called_once_with( MotorModel(**stub_motor_dump)) + def test_create_motor_optional_params(stub_motor_dump, mock_controller_instance): stub_motor_dump.update({ 'interpolation_method': 'linear', @@ -63,7 +66,7 @@ def test_create_motor_optional_params(stub_motor_dump, mock_controller_instance) mock_response = AsyncMock(return_value=MotorCreated(motor_id='123')) mock_controller_instance.post_motor = mock_response with patch.object( - Motor, 'set_motor_kind', side_effect=None + MotorModel, 'set_motor_kind', side_effect=None ) as mock_set_motor_kind: response = client.post( '/motors/', json=stub_motor_dump, params={'motor_kind': 'HYBRID'} @@ -76,6 +79,7 @@ def test_create_motor_optional_params(stub_motor_dump, mock_controller_instance) mock_set_motor_kind.assert_called_once_with(MotorKinds.HYBRID) mock_controller_instance.post_motor.assert_called_once_with(MotorModel(**stub_motor_dump)) + def test_create_generic_motor(stub_motor_dump, mock_controller_instance): stub_motor_dump.update( { @@ -89,7 +93,7 @@ def test_create_generic_motor(stub_motor_dump, mock_controller_instance): mock_response = AsyncMock(return_value=MotorCreated(motor_id='123')) mock_controller_instance.post_motor = mock_response with patch.object( - Motor, 'set_motor_kind', side_effect=None + MotorModel, 'set_motor_kind', side_effect=None ) as mock_set_motor_kind: response = client.post( '/motors/', json=stub_motor_dump, params={'motor_kind': 'GENERIC'} @@ -102,12 +106,13 @@ def test_create_generic_motor(stub_motor_dump, mock_controller_instance): mock_set_motor_kind.assert_called_once_with(MotorKinds.GENERIC) mock_controller_instance.post_motor.assert_called_once_with(MotorModel(**stub_motor_dump)) + def test_create_liquid_motor_level_tank(stub_motor_dump, stub_level_tank, mock_controller_instance): stub_motor_dump.update({'tanks': [stub_level_tank]}) mock_response = AsyncMock(return_value=MotorCreated(motor_id='123')) mock_controller_instance.post_motor = mock_response with patch.object( - Motor, 'set_motor_kind', side_effect=None + MotorModel, 'set_motor_kind', side_effect=None ) as mock_set_motor_kind: response = client.post( '/motors/', json=stub_motor_dump, params={'motor_kind': 'LIQUID'} @@ -120,12 +125,13 @@ def test_create_liquid_motor_level_tank(stub_motor_dump, stub_level_tank, mock_c mock_set_motor_kind.assert_called_once_with(MotorKinds.LIQUID) mock_controller_instance.post_motor.assert_called_once_with(MotorModel(**stub_motor_dump)) + def test_create_liquid_motor_mass_flow_tank(stub_motor_dump, stub_mass_flow_tank, mock_controller_instance): stub_motor_dump.update({'tanks': [stub_mass_flow_tank]}) mock_response = AsyncMock(return_value=MotorCreated(motor_id='123')) mock_controller_instance.post_motor = mock_response with patch.object( - Motor, 'set_motor_kind', side_effect=None + MotorModel, 'set_motor_kind', side_effect=None ) as mock_set_motor_kind: response = client.post( '/motors/', json=stub_motor_dump, params={'motor_kind': 'LIQUID'} @@ -138,12 +144,13 @@ def test_create_liquid_motor_mass_flow_tank(stub_motor_dump, stub_mass_flow_tank mock_set_motor_kind.assert_called_once_with(MotorKinds.LIQUID) mock_controller_instance.post_motor.assert_called_once_with(MotorModel(**stub_motor_dump)) + def test_create_liquid_motor_ullage_tank(stub_motor_dump, stub_ullage_tank, mock_controller_instance): stub_motor_dump.update({'tanks': [stub_ullage_tank]}) mock_response = AsyncMock(return_value=MotorCreated(motor_id='123')) mock_controller_instance.post_motor = mock_response with patch.object( - Motor, 'set_motor_kind', side_effect=None + MotorModel, 'set_motor_kind', side_effect=None ) as mock_set_motor_kind: response = client.post( '/motors/', json=stub_motor_dump, params={'motor_kind': 'LIQUID'} @@ -156,12 +163,13 @@ def test_create_liquid_motor_ullage_tank(stub_motor_dump, stub_ullage_tank, mock mock_set_motor_kind.assert_called_once_with(MotorKinds.LIQUID) mock_controller_instance.post_motor.assert_called_once_with(MotorModel(**stub_motor_dump)) + def test_create_liquid_motor_mass_tank(stub_motor_dump, stub_mass_tank, mock_controller_instance): stub_motor_dump.update({'tanks': [stub_mass_tank]}) mock_response = AsyncMock(return_value=MotorCreated(motor_id='123')) mock_controller_instance.post_motor = mock_response with patch.object( - Motor, 'set_motor_kind', side_effect=None + MotorModel, 'set_motor_kind', side_effect=None ) as mock_set_motor_kind: response = client.post( '/motors/', json=stub_motor_dump, params={'motor_kind': 'LIQUID'} @@ -174,6 +182,7 @@ def test_create_liquid_motor_mass_tank(stub_motor_dump, stub_mass_tank, mock_con mock_set_motor_kind.assert_called_once_with(MotorKinds.LIQUID) mock_controller_instance.post_motor.assert_called_once_with(MotorModel(**stub_motor_dump)) + def test_create_hybrid_motor(stub_motor_dump, stub_level_tank, mock_controller_instance): stub_motor_dump.update( { @@ -191,7 +200,7 @@ def test_create_hybrid_motor(stub_motor_dump, stub_level_tank, mock_controller_i mock_response = AsyncMock(return_value=MotorCreated(motor_id='123')) mock_controller_instance.post_motor = mock_response with patch.object( - Motor, 'set_motor_kind', side_effect=None + MotorModel, 'set_motor_kind', side_effect=None ) as mock_set_motor_kind: response = client.post( '/motors/', json=stub_motor_dump, params={'motor_kind': 'HYBRID'} @@ -204,6 +213,7 @@ def test_create_hybrid_motor(stub_motor_dump, stub_level_tank, mock_controller_i mock_set_motor_kind.assert_called_once_with(MotorKinds.HYBRID) mock_controller_instance.post_motor.assert_called_once_with(MotorModel(**stub_motor_dump)) + def test_create_solid_motor(stub_motor_dump, mock_controller_instance): stub_motor_dump.update( { @@ -219,7 +229,7 @@ def test_create_solid_motor(stub_motor_dump, mock_controller_instance): mock_response = AsyncMock(return_value=MotorCreated(motor_id='123')) mock_controller_instance.post_motor = mock_response with patch.object( - Motor, 'set_motor_kind', side_effect=None + MotorModel, 'set_motor_kind', side_effect=None ) as mock_set_motor_kind: response = client.post( '/motors/', json=stub_motor_dump, params={'motor_kind': 'SOLID'} @@ -232,17 +242,19 @@ def test_create_solid_motor(stub_motor_dump, mock_controller_instance): mock_set_motor_kind.assert_called_once_with(MotorKinds.SOLID) mock_controller_instance.post_motor.assert_called_once_with(MotorModel(**stub_motor_dump)) + def test_create_motor_invalid_input(): response = client.post( '/motors/', json={'burn_time': 'foo', 'nozzle_radius': 'bar'} ) assert response.status_code == 422 + def test_create_motor_server_error(stub_motor_dump, mock_controller_instance): mock_response = AsyncMock(side_effect=HTTPException(status_code=500)) mock_controller_instance.post_motor = mock_response with patch.object( - Motor, 'set_motor_kind', side_effect=None + MotorModel, 'set_motor_kind', side_effect=None ) as mock_set_motor_kind: response = client.post('/motors/', json=stub_motor_dump, params={'motor_kind': 'HYBRID'}) assert response.status_code == 500 @@ -250,14 +262,20 @@ def test_create_motor_server_error(stub_motor_dump, mock_controller_instance): mock_set_motor_kind.assert_called_once_with(MotorKinds.HYBRID) mock_controller_instance.post_motor.assert_called_once_with(MotorModel(**stub_motor_dump)) + def test_read_motor(stub_motor_dump, mock_controller_instance): - mock_response = AsyncMock(return_value=MotorView(**stub_motor_dump)) + stub_motor_out = MotorView(motor_id='123', **stub_motor_dump) + mock_response = AsyncMock(return_value=MotorRetrieved(motor=stub_motor_out)) mock_controller_instance.get_motor_by_id = mock_response response = client.get('/motors/123') assert response.status_code == 200 - assert response.json() == stub_motor_dump + assert response.json() == { + 'message': 'motor successfully retrieved', + 'motor': json.loads(stub_motor_out.model_dump_json()), + } mock_controller_instance.get_motor_by_id.assert_called_once_with('123') + def test_read_motor_not_found(mock_controller_instance): mock_response = AsyncMock(side_effect=HTTPException(status_code=404)) mock_controller_instance.get_motor_by_id = mock_response @@ -266,6 +284,7 @@ def test_read_motor_not_found(mock_controller_instance): assert response.json() == {'detail': 'Not Found'} mock_controller_instance.get_motor_by_id.assert_called_once_with('123') + def test_read_motor_server_error(mock_controller_instance): mock_response = AsyncMock(side_effect=HTTPException(status_code=500)) mock_controller_instance.get_motor_by_id = mock_response @@ -273,11 +292,12 @@ def test_read_motor_server_error(mock_controller_instance): assert response.status_code == 500 assert response.json() == {'detail': 'Internal Server Error'} + def test_update_motor(stub_motor_dump, mock_controller_instance): mock_response = AsyncMock(return_value=MotorUpdated(motor_id='123')) mock_controller_instance.put_motor_by_id = mock_response with patch.object( - Motor, 'set_motor_kind', side_effect=None + MotorModel, 'set_motor_kind', side_effect=None ) as mock_set_motor_kind: response = client.put( '/motors/123', json=stub_motor_dump, params={'motor_kind': 'HYBRID'} @@ -292,6 +312,7 @@ def test_update_motor(stub_motor_dump, mock_controller_instance): '123', MotorModel(**stub_motor_dump) ) + def test_update_motor_invalid_input(): response = client.put( '/motors/123', @@ -300,11 +321,12 @@ def test_update_motor_invalid_input(): ) assert response.status_code == 422 + def test_update_motor_not_found(stub_motor_dump, mock_controller_instance): mock_response = AsyncMock(side_effect=HTTPException(status_code=404)) mock_controller_instance.put_motor_by_id = mock_response with patch.object( - Motor, 'set_motor_kind', side_effect=None + MotorModel, 'set_motor_kind', side_effect=None ) as mock_set_motor_kind: response = client.put( '/motors/123', json=stub_motor_dump, params={'motor_kind': 'HYBRID'} @@ -316,11 +338,12 @@ def test_update_motor_not_found(stub_motor_dump, mock_controller_instance): '123', MotorModel(**stub_motor_dump) ) + def test_update_motor_server_error(stub_motor_dump, mock_controller_instance): mock_response = AsyncMock(side_effect=HTTPException(status_code=500)) mock_controller_instance.put_motor_by_id = mock_response with patch.object( - Motor, 'set_motor_kind', side_effect=None + MotorModel, 'set_motor_kind', side_effect=None ) as mock_set_motor_kind: response = client.put( '/motors/123', json=stub_motor_dump, params={'motor_kind': 'HYBRID'} @@ -332,6 +355,7 @@ def test_update_motor_server_error(stub_motor_dump, mock_controller_instance): '123', MotorModel(**stub_motor_dump) ) + def test_delete_motor(mock_controller_instance): mock_response = AsyncMock(return_value=MotorDeleted(motor_id='123')) mock_controller_instance.delete_motor_by_id = mock_response @@ -343,6 +367,7 @@ def test_delete_motor(mock_controller_instance): } mock_controller_instance.delete_motor_by_id.assert_called_once_with('123') + def test_delete_motor_server_error(mock_controller_instance): mock_response = AsyncMock(side_effect=HTTPException(status_code=500)) mock_controller_instance.delete_motor_by_id = mock_response @@ -369,6 +394,7 @@ def test_simulate_motor_not_found(mock_controller_instance): assert response.json() == {'detail': 'Not Found'} mock_controller_instance.simulate_motor.assert_called_once_with('123') + def test_simulate_motor_server_error(mock_controller_instance): mock_response = AsyncMock(side_effect=HTTPException(status_code=500)) mock_controller_instance.simulate_motor = mock_response @@ -377,7 +403,8 @@ def test_simulate_motor_server_error(mock_controller_instance): assert response.json() == {'detail': 'Internal Server Error'} mock_controller_instance.simulate_motor.assert_called_once_with('123') -def test_read_rocketpy_motor(mock_controller_instance): + +def test_read_rocketpy_motor_binary(mock_controller_instance): mock_response = AsyncMock(return_value=b'rocketpy') mock_controller_instance.get_rocketpy_motor_binary = mock_response response = client.get('/motors/123/rocketpy') @@ -386,7 +413,8 @@ def test_read_rocketpy_motor(mock_controller_instance): assert response.headers['content-type'] == 'application/octet-stream' mock_controller_instance.get_rocketpy_motor_binary.assert_called_once_with('123') -def test_read_rocketpy_motor_not_found(mock_controller_instance): + +def test_read_rocketpy_motor_binary_not_found(mock_controller_instance): mock_response = AsyncMock(side_effect=HTTPException(status_code=404)) mock_controller_instance.get_rocketpy_motor_binary = mock_response response = client.get('/motors/123/rocketpy') @@ -394,7 +422,8 @@ def test_read_rocketpy_motor_not_found(mock_controller_instance): assert response.json() == {'detail': 'Not Found'} mock_controller_instance.get_rocketpy_motor_binary.assert_called_once_with('123') -def test_read_rocketpy_motor_server_error(mock_controller_instance): + +def test_read_rocketpy_motor_binary_server_error(mock_controller_instance): mock_response = AsyncMock(side_effect=HTTPException(status_code=500)) mock_controller_instance.get_rocketpy_motor_binary = mock_response response = client.get('/motors/123/rocketpy') diff --git a/tests/test_routes/test_rockets_route.py b/tests/test_routes/test_rockets_route.py index 3e0e514..5e17eb4 100644 --- a/tests/test_routes/test_rockets_route.py +++ b/tests/test_routes/test_rockets_route.py @@ -1,4 +1,4 @@ -from unittest.mock import patch +from unittest.mock import patch, Mock, AsyncMock import json import pytest from fastapi.testclient import TestClient @@ -13,11 +13,10 @@ MotorModel, MotorKinds, ) -from lib.controllers.rocket import RocketController -from lib.views.motor import MotorView from lib.views.rocket import ( RocketCreated, RocketUpdated, + RocketRetrieved, RocketDeleted, RocketSummary, RocketView, @@ -28,14 +27,14 @@ @pytest.fixture -def stub_rocket_summary(): +def stub_rocket_summary_dump(): rocket_summary = RocketSummary() rocket_summary_json = rocket_summary.model_dump_json() return json.loads(rocket_summary_json) @pytest.fixture -def stub_tail(): +def stub_tail_dump(): tail = Tail( name='tail', top_radius=0, @@ -49,7 +48,7 @@ def stub_tail(): @pytest.fixture -def stub_rail_buttons(): +def stub_rail_buttons_dump(): rail_buttons = RailButtons( upper_button_position=0, lower_button_position=0, @@ -60,7 +59,7 @@ def stub_rail_buttons(): @pytest.fixture -def stub_parachute(): +def stub_parachute_dump(): parachute = Parachute( name='parachute', cd_s=0, @@ -73,62 +72,65 @@ def stub_parachute(): return json.loads(parachute_json) -def test_create_rocket(stub_rocket): +@pytest.fixture(autouse=True) +def mock_controller_instance(): + with patch( + "lib.routes.rocket.RocketController", autospec=True + ) as mock_controller: + mock_controller_instance = mock_controller.return_value + mock_controller_instance.post_brecho = Mock() + mock_controller_instance.get_brecho_by_id = Mock() + mock_controller_instance.put_brecho_by_id = Mock() + mock_controller_instance.delete_brecho_by_id = Mock() + yield mock_controller_instance + + +def test_create_rocket(stub_rocket_dump, mock_controller_instance): + mock_response = AsyncMock(return_value=RocketCreated(rocket_id='123')) + mock_controller_instance.post_rocket = mock_response with patch.object( - RocketController, - 'create_rocket', - return_value=RocketCreated(rocket_id='123'), - ) as mock_create_rocket: - with patch.object( MotorModel, 'set_motor_kind', side_effect=None - ) as mock_set_motor_kind: - response = client.post( - '/rockets/', json=stub_rocket, params={'motor_kind': 'HYBRID'} - ) - assert response.status_code == 200 - assert response.json() == { - 'rocket_id': '123', - 'message': 'Rocket successfully created', - } - mock_set_motor_kind.assert_called_once_with(MotorKinds.HYBRID) - mock_create_rocket.assert_called_once_with(RocketModel(**stub_rocket)) + ) as mock_set_motor_kind: + response = client.post('/rockets/', json=stub_rocket_dump, params={'motor_kind': 'HYBRID'}) + assert response.status_code == 200 + assert response.json() == { + 'rocket_id': '123', + 'message': 'rocket successfully created', + } + mock_set_motor_kind.assert_called_once_with(MotorKinds.HYBRID) + mock_controller_instance.post_rocket.assert_called_once_with( + RocketModel(**stub_rocket_dump)) def test_create_rocket_optional_params( - stub_rocket, - stub_tail, - stub_rail_buttons, - stub_parachute, + stub_rocket_dump, stub_tail_dump, stub_rail_buttons_dump, stub_parachute_dump, mock_controller_instance ): - stub_rocket.update( + stub_rocket_dump.update( { - 'parachutes': [stub_parachute], - 'rail_buttons': stub_rail_buttons, - 'tail': stub_tail, + 'parachutes': [stub_parachute_dump], + 'rail_buttons': stub_rail_buttons_dump, + 'tail': stub_tail_dump, } ) + mock_response = AsyncMock(return_value=RocketCreated(rocket_id='123')) + mock_controller_instance.post_rocket = mock_response with patch.object( - RocketController, - 'create_rocket', - return_value=RocketCreated(rocket_id='123'), - ) as mock_create_rocket: - with patch.object( MotorModel, 'set_motor_kind', side_effect=None - ) as mock_set_motor_kind: - response = client.post( - '/rockets/', json=stub_rocket, params={'motor_kind': 'HYBRID'} - ) - assert response.status_code == 200 - assert response.json() == { - 'rocket_id': '123', - 'message': 'Rocket successfully created', - } - mock_set_motor_kind.assert_called_once_with(MotorKinds.HYBRID) - mock_create_rocket.assert_called_once_with(RocketModel(**stub_rocket)) - - -def test_create_generic_motor_rocket(stub_rocket, stub_motor): - stub_motor.update( + ) as mock_set_motor_kind: + response = client.post( + '/rockets/', json=stub_rocket_dump, params={'motor_kind': 'HYBRID'} + ) + assert response.status_code == 200 + assert response.json() == { + 'rocket_id': '123', + 'message': 'rocket successfully created', + } + mock_set_motor_kind.assert_called_once_with(MotorKinds.HYBRID) + mock_controller_instance.post_rocket.assert_called_once_with(RocketModel(**stub_rocket_dump)) + + +def test_create_generic_motor_rocket(stub_rocket_dump, stub_motor_dump, mock_controller_instance): + stub_motor_dump.update( { 'chamber_radius': 0, 'chamber_height': 0, @@ -137,129 +139,114 @@ def test_create_generic_motor_rocket(stub_rocket, stub_motor): 'nozzle_position': 0, } ) - stub_rocket.update({'motor': stub_motor}) + stub_rocket_dump.update({'motor': stub_motor_dump}) + mock_response = AsyncMock(return_value=RocketCreated(rocket_id='123')) + mock_controller_instance.post_rocket = mock_response with patch.object( - RocketController, - 'create_rocket', - return_value=RocketCreated(rocket_id='123'), - ) as mock_create_rocket: - with patch.object( MotorModel, 'set_motor_kind', side_effect=None - ) as mock_set_motor_kind: - response = client.post( - '/rockets/', json=stub_rocket, params={'motor_kind': 'GENERIC'} - ) - assert response.status_code == 200 - assert response.json() == { - 'rocket_id': '123', - 'message': 'Rocket successfully created', - } - mock_set_motor_kind.assert_called_once_with(MotorKinds.GENERIC) - mock_create_rocket.assert_called_once_with(RocketModel(**stub_rocket)) + ) as mock_set_motor_kind: + response = client.post( + '/rockets/', json=stub_rocket_dump, params={'motor_kind': 'GENERIC'} + ) + assert response.status_code == 200 + assert response.json() == { + 'rocket_id': '123', + 'message': 'rocket successfully created', + } + mock_set_motor_kind.assert_called_once_with(MotorKinds.GENERIC) + mock_controller_instance.post_rocket.assert_called_once_with(RocketModel(**stub_rocket_dump)) def test_create_liquid_motor_level_tank_rocket( - stub_rocket, stub_motor, stub_level_tank + stub_rocket_dump, stub_motor_dump, stub_level_tank_dump, mock_controller_instance ): - stub_motor.update({'tanks': [stub_level_tank]}) - stub_rocket.update({'motor': stub_motor}) + stub_motor_dump.update({'tanks': [stub_level_tank_dump]}) + stub_rocket_dump.update({'motor': stub_motor_dump}) + mock_response = AsyncMock(return_value=RocketCreated(rocket_id='123')) + mock_controller_instance.post_rocket = mock_response with patch.object( - RocketController, - 'create_rocket', - return_value=RocketCreated(rocket_id='123'), - ) as mock_create_rocket: - with patch.object( MotorModel, 'set_motor_kind', side_effect=None - ) as mock_set_motor_kind: - response = client.post( - '/rockets/', json=stub_rocket, params={'motor_kind': 'LIQUID'} - ) - assert response.status_code == 200 - assert response.json() == { - 'rocket_id': '123', - 'message': 'Rocket successfully created', - } - mock_set_motor_kind.assert_called_once_with(MotorKinds.LIQUID) - mock_create_rocket.assert_called_once_with(RocketModel(**stub_rocket)) + ) as mock_set_motor_kind: + response = client.post( + '/rockets/', json=stub_rocket_dump, params={'motor_kind': 'LIQUID'} + ) + assert response.status_code == 200 + assert response.json() == { + 'rocket_id': '123', + 'message': 'rocket successfully created', + } + mock_set_motor_kind.assert_called_once_with(MotorKinds.LIQUID) + mock_controller_instance.post_rocket.assert_called_once_with(RocketModel(**stub_rocket_dump)) def test_create_liquid_motor_mass_flow_tank_rocket( - stub_rocket, stub_motor, stub_mass_flow_tank + stub_rocket_dump, stub_motor_dump, stub_mass_flow_tank_dump, mock_controller_instance ): - stub_motor.update({'tanks': [stub_mass_flow_tank]}) - stub_rocket.update({'motor': stub_motor}) + stub_motor_dump.update({'tanks': [stub_mass_flow_tank_dump]}) + stub_rocket_dump.update({'motor': stub_motor_dump}) + mock_response = AsyncMock(return_value=RocketCreated(rocket_id='123')) + mock_controller_instance.post_rocket = mock_response with patch.object( - RocketController, - 'create_rocket', - return_value=RocketCreated(rocket_id='123'), - ) as mock_create_rocket: - with patch.object( MotorModel, 'set_motor_kind', side_effect=None - ) as mock_set_motor_kind: - response = client.post( - '/rockets/', json=stub_rocket, params={'motor_kind': 'LIQUID'} - ) - assert response.status_code == 200 - assert response.json() == { - 'rocket_id': '123', - 'message': 'Rocket successfully created', - } - mock_set_motor_kind.assert_called_once_with(MotorKinds.LIQUID) - mock_create_rocket.assert_called_once_with(RocketModel(**stub_rocket)) + ) as mock_set_motor_kind: + response = client.post( + '/rockets/', json=stub_rocket_dump, params={'motor_kind': 'LIQUID'} + ) + assert response.status_code == 200 + assert response.json() == { + 'rocket_id': '123', + 'message': 'rocket successfully created', + } + mock_set_motor_kind.assert_called_once_with(MotorKinds.LIQUID) + mock_controller_instance.post_rocket.assert_called_once_with(RocketModel(**stub_rocket_dump)) def test_create_liquid_motor_ullage_tank_rocket( - stub_rocket, stub_motor, stub_ullage_tank + stub_rocket_dump, stub_motor_dump, stub_ullage_tank_dump, mock_controller_instance ): - stub_motor.update({'tanks': [stub_ullage_tank]}) - stub_rocket.update({'motor': stub_motor}) + stub_motor_dump.update({'tanks': [stub_ullage_tank_dump]}) + stub_rocket_dump.update({'motor': stub_motor_dump}) + mock_response = AsyncMock(return_value=RocketCreated(rocket_id='123')) + mock_controller_instance.post_rocket = mock_response with patch.object( - RocketController, - 'create_rocket', - return_value=RocketCreated(rocket_id='123'), - ) as mock_create_rocket: - with patch.object( MotorModel, 'set_motor_kind', side_effect=None - ) as mock_set_motor_kind: - response = client.post( - '/rockets/', json=stub_rocket, params={'motor_kind': 'LIQUID'} - ) - assert response.status_code == 200 - assert response.json() == { - 'rocket_id': '123', - 'message': 'Rocket successfully created', - } - mock_set_motor_kind.assert_called_once_with(MotorKinds.LIQUID) - mock_create_rocket.assert_called_once_with(RocketModel(**stub_rocket)) + ) as mock_set_motor_kind: + response = client.post( + '/rockets/', json=stub_rocket_dump, params={'motor_kind': 'LIQUID'} + ) + assert response.status_code == 200 + assert response.json() == { + 'rocket_id': '123', + 'message': 'rocket successfully created', + } + mock_set_motor_kind.assert_called_once_with(MotorKinds.LIQUID) + mock_controller_instance.post_rocket.assert_called_once_with(RocketModel(**stub_rocket_dump)) def test_create_liquid_motor_mass_tank_rocket( - stub_rocket, stub_motor, stub_mass_tank + stub_rocket_dump, stub_motor_dump, stub_mass_tank_dump, mock_controller_instance ): - stub_motor.update({'tanks': [stub_mass_tank]}) - stub_rocket.update({'motor': stub_motor}) + stub_motor_dump.update({'tanks': [stub_mass_tank_dump]}) + stub_rocket_dump.update({'motor': stub_motor_dump}) + mock_response = AsyncMock(return_value=RocketCreated(rocket_id='123')) + mock_controller_instance.post_rocket = mock_response with patch.object( - RocketController, - 'create_rocket', - return_value=RocketCreated(rocket_id='123'), - ) as mock_create_rocket: - with patch.object( MotorModel, 'set_motor_kind', side_effect=None - ) as mock_set_motor_kind: - response = client.post( - '/rockets/', json=stub_rocket, params={'motor_kind': 'LIQUID'} - ) - assert response.status_code == 200 - assert response.json() == { - 'rocket_id': '123', - 'message': 'Rocket successfully created', - } - mock_set_motor_kind.assert_called_once_with(MotorKinds.LIQUID) - mock_create_rocket.assert_called_once_with(RocketModel(**stub_rocket)) - - -def test_create_hybrid_motor_rocket(stub_rocket, stub_motor, stub_level_tank): - stub_motor.update( + ) as mock_set_motor_kind: + response = client.post( + '/rockets/', json=stub_rocket_dump, params={'motor_kind': 'LIQUID'} + ) + assert response.status_code == 200 + assert response.json() == { + 'rocket_id': '123', + 'message': 'rocket successfully created', + } + mock_set_motor_kind.assert_called_once_with(MotorKinds.LIQUID) + mock_controller_instance.post_rocket.assert_called_once_with(RocketModel(**stub_rocket_dump)) + + +def test_create_hybrid_motor_rocket(stub_rocket_dump, stub_motor_dump, stub_level_tank_dump, mock_controller_instance): + stub_motor_dump.update( { 'grain_number': 0, 'grain_density': 0, @@ -269,32 +256,29 @@ def test_create_hybrid_motor_rocket(stub_rocket, stub_motor, stub_level_tank): 'grains_center_of_mass_position': 0, 'grain_separation': 0, 'throat_radius': 0, - 'tanks': [stub_level_tank], + 'tanks': [stub_level_tank_dump], } ) - stub_rocket.update({'motor': stub_motor}) + stub_rocket_dump.update({'motor': stub_motor_dump}) + mock_response = AsyncMock(return_value=RocketCreated(rocket_id='123')) + mock_controller_instance.post_rocket = mock_response with patch.object( - RocketController, - 'create_rocket', - return_value=RocketCreated(rocket_id='123'), - ) as mock_create_rocket: - with patch.object( MotorModel, 'set_motor_kind', side_effect=None - ) as mock_set_motor_kind: - response = client.post( - '/rockets/', json=stub_rocket, params={'motor_kind': 'HYBRID'} - ) - assert response.status_code == 200 - assert response.json() == { - 'rocket_id': '123', - 'message': 'Rocket successfully created', - } - mock_set_motor_kind.assert_called_once_with(MotorKinds.HYBRID) - mock_create_rocket.assert_called_once_with(RocketModel(**stub_rocket)) - - -def test_create_solid_motor_rocket(stub_rocket, stub_motor): - stub_motor.update( + ) as mock_set_motor_kind: + response = client.post( + '/rockets/', json=stub_rocket_dump, params={'motor_kind': 'HYBRID'} + ) + assert response.status_code == 200 + assert response.json() == { + 'rocket_id': '123', + 'message': 'rocket successfully created', + } + mock_set_motor_kind.assert_called_once_with(MotorKinds.HYBRID) + mock_controller_instance.post_rocket.assert_called_once_with(RocketModel(**stub_rocket_dump)) + + +def test_create_solid_motor_rocket(stub_rocket_dump, stub_motor_dump, mock_controller_instance): + stub_motor_dump.update( { 'grain_number': 0, 'grain_density': 0, @@ -305,25 +289,22 @@ def test_create_solid_motor_rocket(stub_rocket, stub_motor): 'grain_separation': 0, } ) - stub_rocket.update({'motor': stub_motor}) + stub_rocket_dump.update({'motor': stub_motor_dump}) + mock_response = AsyncMock(return_value=RocketCreated(rocket_id='123')) + mock_controller_instance.post_rocket = mock_response with patch.object( - RocketController, - 'create_rocket', - return_value=RocketCreated(rocket_id='123'), - ) as mock_create_rocket: - with patch.object( MotorModel, 'set_motor_kind', side_effect=None - ) as mock_set_motor_kind: - response = client.post( - '/rockets/', json=stub_rocket, params={'motor_kind': 'SOLID'} - ) - assert response.status_code == 200 - assert response.json() == { - 'rocket_id': '123', - 'message': 'Rocket successfully created', - } - mock_set_motor_kind.assert_called_once_with(MotorKinds.SOLID) - mock_create_rocket.assert_called_once_with(RocketModel(**stub_rocket)) + ) as mock_set_motor_kind: + response = client.post( + '/rockets/', json=stub_rocket_dump, params={'motor_kind': 'SOLID'} + ) + assert response.status_code == 200 + assert response.json() == { + 'rocket_id': '123', + 'message': 'rocket successfully created', + } + mock_set_motor_kind.assert_called_once_with(MotorKinds.SOLID) + mock_controller_instance.post_rocket.assert_called_once_with(RocketModel(**stub_rocket_dump)) def test_create_rocket_invalid_input(): @@ -331,84 +312,67 @@ def test_create_rocket_invalid_input(): assert response.status_code == 422 -def test_create_rocket_server_error(stub_rocket): - with patch.object( - RocketController, - 'create_rocket', - side_effect=HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR - ), - ): - response = client.post( - '/rockets/', json=stub_rocket, params={'motor_kind': 'HYBRID'} - ) - assert response.status_code == 500 - assert response.json() == {'detail': 'Internal Server Error'} - - -def test_read_rocket(stub_rocket, stub_motor): - del stub_rocket['motor'] - motor_view = MotorView(**stub_motor, selected_motor_kind=MotorKinds.HYBRID) - rocket_view = RocketView(**stub_rocket, motor=motor_view) - with patch.object( - RocketController, - 'get_rocket_by_id', - return_value=rocket_view, - ) as mock_read_rocket: - response = client.get('/rockets/123') - assert response.status_code == 200 - assert response.json() == json.loads(rocket_view.model_dump_json()) - mock_read_rocket.assert_called_once_with('123') +def test_create_rocket_server_error(stub_rocket_dump, mock_controller_instance): + mock_controller_instance.post_rocket.side_effect = HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR + ) + response = client.post( + '/rockets/', json=stub_rocket_dump, params={'motor_kind': 'HYBRID'} + ) + assert response.status_code == 500 + assert response.json() == {'detail': 'Internal Server Error'} + + +def test_read_rocket(stub_rocket_dump, stub_motor_dump, mock_controller_instance): + stub_rocket_dump.update({'motor': stub_motor_dump}) + stub_rocket_out = RocketView(rocket_id='123', **stub_rocket_dump) + mock_response = AsyncMock(return_value=RocketRetrieved(rocket=stub_rocket_out)) + mock_controller_instance.get_rocket_by_id = mock_response + response = client.get('/rockets/123') + assert response.status_code == 200 + assert response.json() == { + 'message': 'rocket successfully retrieved', + 'rocket': json.loads(stub_rocket_out.model_dump_json()), + } + mock_controller_instance.get_rocket_by_id.assert_called_once_with('123') + + +def test_read_rocket_not_found(mock_controller_instance): + mock_controller_instance.get_rocket_by_id.side_effect = HTTPException( + status_code=status.HTTP_404_NOT_FOUND + ) + response = client.get('/rockets/123') + assert response.status_code == 404 + assert response.json() == {'detail': 'Not Found'} -def test_read_rocket_not_found(): - with patch.object( - RocketController, - 'get_rocket_by_id', - side_effect=HTTPException(status_code=status.HTTP_404_NOT_FOUND), - ) as mock_read_rocket: - response = client.get('/rockets/123') - assert response.status_code == 404 - assert response.json() == {'detail': 'Not Found'} - mock_read_rocket.assert_called_once_with('123') +def test_read_rocket_server_error(mock_controller_instance): + mock_controller_instance.get_rocket_by_id.side_effect = HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR + ) + response = client.get('/rockets/123') + assert response.status_code == 500 + assert response.json() == {'detail': 'Internal Server Error'} -def test_read_rocket_server_error(): - with patch.object( - RocketController, - 'get_rocket_by_id', - side_effect=HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR - ), - ): - response = client.get('/rockets/123') - assert response.status_code == 500 - assert response.json() == {'detail': 'Internal Server Error'} - - -def test_update_rocket(stub_rocket): +def test_update_rocket(stub_rocket_dump, mock_controller_instance): + mock_response = AsyncMock(return_value=RocketUpdated(rocket_id='123')) + mock_controller_instance.put_rocket_by_id = mock_response with patch.object( - RocketController, - 'update_rocket_by_id', - return_value=RocketUpdated(rocket_id='123'), - ) as mock_update_rocket: - with patch.object( MotorModel, 'set_motor_kind', side_effect=None - ) as mock_set_motor_kind: - response = client.put( - '/rockets/123', - json=stub_rocket, - params={'motor_kind': 'GENERIC'}, - ) - assert response.status_code == 200 - assert response.json() == { - 'rocket_id': '123', - 'message': 'Rocket successfully updated', - } - mock_update_rocket.assert_called_once_with( - '123', RocketModel(**stub_rocket) - ) - mock_set_motor_kind.assert_called_once_with(MotorKinds.GENERIC) + ) as mock_set_motor_kind: + response = client.put( + '/rockets/123', + json=stub_rocket_dump, + params={'motor_kind': 'HYBRID'}, + ) + assert response.status_code == 200 + assert response.json() == { + 'rocket_id': '123', + 'message': 'Rocket successfully updated', + } + mock_set_motor_kind.assert_called_once_with(MotorKinds.HYBRID) + mock_controller_instance.put_rocket_by_id.assert_called_once_with('123', RocketModel(**stub_rocket_dump)) def test_update_rocket_invalid_input(): @@ -420,132 +384,102 @@ def test_update_rocket_invalid_input(): assert response.status_code == 422 -def test_update_rocket_not_found(stub_rocket): - with patch.object( - RocketController, - 'update_rocket_by_id', - side_effect=HTTPException(status_code=status.HTTP_404_NOT_FOUND), - ): - response = client.put( - '/rockets/123', json=stub_rocket, params={'motor_kind': 'HYBRID'} - ) - assert response.status_code == 404 - assert response.json() == {'detail': 'Not Found'} +def test_update_rocket_not_found(stub_rocket_dump, mock_controller_instance): + mock_controller_instance.put_rocket_by_id.side_effect = HTTPException( + status_code=status.HTTP_404_NOT_FOUND + ) + response = client.put( + '/rockets/123', + json=stub_rocket_dump, + params={'motor_kind': 'HYBRID'}, + ) + assert response.status_code == 404 + assert response.json() == {'detail': 'Not Found'} -def test_update_rocket_server_error(stub_rocket): - with patch.object( - RocketController, - 'update_rocket_by_id', - side_effect=HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR - ), - ): - response = client.put( - '/rockets/123', json=stub_rocket, params={'motor_kind': 'HYBRID'} - ) - assert response.status_code == 500 - assert response.json() == {'detail': 'Internal Server Error'} +def test_update_rocket_server_error(stub_rocket_dump, mock_controller_instance): + mock_controller_instance.put_rocket_by_id.side_effect = HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR + ) + response = client.put( + '/rockets/123', + json=stub_rocket_dump, + params={'motor_kind': 'HYBRID'}, + ) + assert response.status_code == 500 + assert response.json() == {'detail': 'Internal Server Error'} -def test_delete_rocket(): - with patch.object( - RocketController, - 'delete_rocket_by_id', - return_value=RocketDeleted(rocket_id='123'), - ) as mock_delete_rocket: - response = client.delete('/rockets/123') - assert response.status_code == 200 - assert response.json() == { - 'rocket_id': '123', - 'message': 'Rocket successfully deleted', - } - mock_delete_rocket.assert_called_once_with('123') +def test_delete_rocket(mock_controller_instance): + mock_response = AsyncMock(return_value=RocketDeleted(rocket_id='123')) + mock_controller_instance.delete_rocket_by_id = mock_response + response = client.delete('/rockets/123') + assert response.status_code == 200 + assert response.json() == { + 'rocket_id': '123', + 'message': 'Rocket successfully deleted', + } + mock_controller_instance.delete_rocket_by_id.assert_called_once_with('123') -def test_delete_rocket_server_error(): - with patch.object( - RocketController, - 'delete_rocket_by_id', - side_effect=HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR - ), - ): - response = client.delete('/rockets/123') - assert response.status_code == 500 - assert response.json() == {'detail': 'Internal Server Error'} - - -def test_simulate_rocket(stub_rocket_summary): - with patch.object( - RocketController, - 'simulate_rocket', - return_value=RocketSummary(**stub_rocket_summary), - ) as mock_simulate_rocket: - response = client.get('/rockets/123/summary') - assert response.status_code == 200 - assert response.json() == stub_rocket_summary - mock_simulate_rocket.assert_called_once_with('123') +def test_delete_rocket_server_error(mock_controller_instance): + mock_controller_instance.delete_rocket_by_id.side_effect = HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR + ) + response = client.delete('/rockets/123') + assert response.status_code == 500 + assert response.json() == {'detail': 'Internal Server Error'} -def test_simulate_rocket_not_found(): - with patch.object( - RocketController, - 'simulate_rocket', - side_effect=HTTPException(status_code=status.HTTP_404_NOT_FOUND), - ) as mock_simulate_rocket: - response = client.get('/rockets/123/summary') - assert response.status_code == 404 - assert response.json() == {'detail': 'Not Found'} - mock_simulate_rocket.assert_called_once_with('123') +def test_simulate_rocket(stub_rocket_summary_dump, mock_controller_instance): + mock_response = AsyncMock(return_value=RocketSummary(**stub_rocket_summary_dump)) + mock_controller_instance.simulate_rocket = mock_response + response = client.get('/rockets/123/summary') + assert response.status_code == 200 + assert response.json() == stub_rocket_summary_dump + mock_controller_instance.simulate_rocket.assert_called_once_with('123') -def test_simulate_rocket_server_error(): - with patch.object( - RocketController, - 'simulate_rocket', - side_effect=HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR - ), - ): - response = client.get('/rockets/123/summary') - assert response.status_code == 500 - assert response.json() == {'detail': 'Internal Server Error'} - - -def test_read_rocketpy_rocket(): - with patch.object( - RocketController, - 'get_rocketpy_rocket_binary', - return_value=b'rocketpy', - ) as mock_read_rocketpy_rocket: - response = client.get('/rockets/123/rocketpy') - assert response.status_code == 203 - assert response.content == b'rocketpy' - assert response.headers['content-type'] == 'application/octet-stream' - mock_read_rocketpy_rocket.assert_called_once_with('123') - - -def test_read_rocketpy_rocket_not_found(): - with patch.object( - RocketController, - 'get_rocketpy_rocket_binary', - side_effect=HTTPException(status_code=status.HTTP_404_NOT_FOUND), - ) as mock_read_rocketpy_rocket: - response = client.get('/rockets/123/rocketpy') - assert response.status_code == 404 - assert response.json() == {'detail': 'Not Found'} - mock_read_rocketpy_rocket.assert_called_once_with('123') +def test_simulate_rocket_not_found(mock_controller_instance): + mock_controller_instance.simulate_rocket.side_effect = HTTPException( + status_code=status.HTTP_404_NOT_FOUND + ) + response = client.get('/rockets/123/summary') + assert response.status_code == 404 + assert response.json() == {'detail': 'Not Found'} -def test_read_rocketpy_rocket_server_error(): - with patch.object( - RocketController, - 'get_rocketpy_rocket_binary', - side_effect=HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR - ), - ): - response = client.get('/rockets/123/rocketpy') - assert response.status_code == 500 - assert response.json() == {'detail': 'Internal Server Error'} +def test_simulate_rocket_server_error(mock_controller_instance): + mock_controller_instance.simulate_rocket.side_effect = HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR + ) + response = client.get('/rockets/123/summary') + assert response.status_code == 500 + assert response.json() == {'detail': 'Internal Server Error'} + + +def test_read_rocketpy_rocket_binary(mock_controller_instance): + mock_controller_instance.get_rocketpy_rocket_binary.return_value = b'rocketpy' + response = client.get('/rockets/123/rocketpy') + assert response.status_code == 203 + assert response.content == b'rocketpy' + assert response.headers['content-type'] == 'application/octet-stream' + mock_controller_instance.get_rocketpy_rocket_binary.assert_called_once_with('123') + + +def test_read_rocketpy_rocket_binary_not_found(mock_controller_instance): + mock_controller_instance.get_rocketpy_rocket_binary.side_effect = HTTPException( + status_code=status.HTTP_404_NOT_FOUND + ) + response = client.get('/rockets/123/rocketpy') + assert response.status_code == 404 + assert response.json() == {'detail': 'Not Found'} + + +def test_read_rocketpy_rocket_binary_server_error(mock_controller_instance): + mock_controller_instance.get_rocketpy_rocket_binary.side_effect = HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR + ) + response = client.get('/rockets/123/rocketpy') + assert response.status_code == 500 + assert response.json() == {'detail': 'Internal Server Error'} From 2083d363b6524a3350b6384361c4b1990997a986 Mon Sep 17 00:00:00 2001 From: Gabriel Barberini Date: Sun, 16 Feb 2025 13:23:55 -0300 Subject: [PATCH 07/34] adapts selected motor kind to new API interface --- lib/models/motor.py | 7 ++++--- lib/services/flight.py | 5 +++-- lib/services/motor.py | 6 +++--- lib/services/rocket.py | 6 +++--- lib/views/motor.py | 2 +- 5 files changed, 14 insertions(+), 12 deletions(-) diff --git a/lib/models/motor.py b/lib/models/motor.py index 85517a3..6b4c8af 100644 --- a/lib/models/motor.py +++ b/lib/models/motor.py @@ -1,6 +1,6 @@ from enum import Enum from typing import Optional, Tuple, List, Union, Self, ClassVar, Literal -from pydantic import PrivateAttr, model_validator +from pydantic import PrivateAttr, model_validator, computed_field from lib.models.interface import ApiBaseModel from lib.models.sub.tanks import MotorTank @@ -65,9 +65,10 @@ def validate_motor_kind(self): raise ValueError("Tanks must be provided for liquid and hybrid motors.") return self + @computed_field @property - def motor_kind(self) -> MotorKinds: - return self._motor_kind + def selected_motor_kind(self) -> str: + return self._motor_kind.value def set_motor_kind(self, motor_kind: MotorKinds): self._motor_kind = motor_kind diff --git a/lib/services/flight.py b/lib/services/flight.py index 1c300b8..65d3c10 100644 --- a/lib/services/flight.py +++ b/lib/services/flight.py @@ -7,7 +7,8 @@ from lib.services.environment import EnvironmentService from lib.services.rocket import RocketService -from lib.views.flight import FlightSummary, FlightView +from lib.models.flight import FlightModel +from lib.views.flight import FlightSummary class FlightService: @@ -17,7 +18,7 @@ def __init__(self, flight: RocketPyFlight = None): self._flight = flight @classmethod - def from_flight_model(cls, flight: FlightView) -> Self: + def from_flight_model(cls, flight: FlightModel) -> Self: """ Get the rocketpy flight object. diff --git a/lib/services/motor.py b/lib/services/motor.py index bfb75ec..be62d82 100644 --- a/lib/services/motor.py +++ b/lib/services/motor.py @@ -16,8 +16,8 @@ ) from lib.models.sub.tanks import TankKinds -from lib.models.motor import MotorKinds -from lib.views.motor import MotorSummary, MotorView +from lib.models.motor import MotorKinds, MotorModel +from lib.views.motor import MotorSummary class MotorService: @@ -27,7 +27,7 @@ def __init__(self, motor: RocketPyMotor = None): self._motor = motor @classmethod - def from_motor_model(cls, motor: MotorView) -> Self: + def from_motor_model(cls, motor: MotorModel) -> Self: """ Get the rocketpy motor object. diff --git a/lib/services/rocket.py b/lib/services/rocket.py index d282be5..0bc8ed4 100644 --- a/lib/services/rocket.py +++ b/lib/services/rocket.py @@ -14,10 +14,10 @@ from rocketpy.utilities import get_instance_attributes from lib import logger -from lib.models.rocket import Parachute +from lib.models.rocket import RocketModel, Parachute from lib.models.sub.aerosurfaces import NoseCone, Tail, Fins from lib.services.motor import MotorService -from lib.views.rocket import RocketView, RocketSummary +from lib.views.rocket import RocketSummary class RocketService: @@ -27,7 +27,7 @@ def __init__(self, rocket: RocketPyRocket = None): self._rocket = rocket @classmethod - def from_rocket_model(cls, rocket: RocketView) -> Self: + def from_rocket_model(cls, rocket: RocketModel) -> Self: """ Get the rocketpy rocket object. diff --git a/lib/views/motor.py b/lib/views/motor.py index 0f213d1..76d1a7c 100644 --- a/lib/views/motor.py +++ b/lib/views/motor.py @@ -73,7 +73,7 @@ class MotorSummary(BaseModel): class MotorView(MotorModel): motor_id: Optional[str] = None - selected_motor_kind: MotorKinds + selected_motor_kind: str class MotorCreated(ApiBaseView): From 4a8c79c113916e7d9b0ec7177b8aad451b078656 Mon Sep 17 00:00:00 2001 From: Gabriel Barberini Date: Sun, 16 Feb 2025 13:38:07 -0300 Subject: [PATCH 08/34] allows extra fields on model instantiation to bridge for views --- lib/models/interface.py | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/models/interface.py b/lib/models/interface.py index 1d7a085..6bf4a5a 100644 --- a/lib/models/interface.py +++ b/lib/models/interface.py @@ -7,6 +7,7 @@ class ApiBaseModel(BaseModel, ABC): _id: Optional[ObjectId] = PrivateAttr(default=None) model_config = ConfigDict( + extra="allow", json_encoders={ObjectId: str}, use_enum_values=True, validate_default=True, From 2a1635dc71665ce6eb565165c28549bb215a010e Mon Sep 17 00:00:00 2001 From: Gabriel Barberini Date: Sun, 16 Feb 2025 14:15:21 -0300 Subject: [PATCH 09/34] adapts flight route tests --- lib/views/motor.py | 2 +- tests/test_routes/test_flights_route.py | 485 ++++++++++++------------ tests/test_routes/test_rockets_route.py | 8 +- 3 files changed, 248 insertions(+), 247 deletions(-) diff --git a/lib/views/motor.py b/lib/views/motor.py index 76d1a7c..a8368b8 100644 --- a/lib/views/motor.py +++ b/lib/views/motor.py @@ -1,7 +1,7 @@ from typing import List, Any, Optional from pydantic import BaseModel, ConfigDict from lib.views.interface import ApiBaseView -from lib.models.motor import MotorModel, MotorKinds +from lib.models.motor import MotorModel from lib.utils import to_python_primitive diff --git a/tests/test_routes/test_flights_route.py b/tests/test_routes/test_flights_route.py index 0f0c98a..5ff58bc 100644 --- a/tests/test_routes/test_flights_route.py +++ b/tests/test_routes/test_flights_route.py @@ -1,4 +1,4 @@ -from unittest.mock import patch +from unittest.mock import patch, Mock, AsyncMock import json import pytest from fastapi.testclient import TestClient @@ -7,12 +7,12 @@ from lib.models.flight import FlightModel from lib.models.motor import MotorModel, MotorKinds from lib.models.rocket import RocketModel -from lib.controllers.flight import FlightController from lib.views.motor import MotorView from lib.views.rocket import RocketView from lib.views.flight import ( FlightCreated, FlightUpdated, + FlightRetrieved, FlightDeleted, FlightSummary, FlightView, @@ -23,7 +23,7 @@ @pytest.fixture -def stub_flight(stub_environment_dump, stub_rocket_dump): +def stub_flight_dump(stub_environment_dump, stub_rocket_dump): flight = { 'name': 'Test Flight', 'environment': stub_environment_dump, @@ -37,35 +37,44 @@ def stub_flight(stub_environment_dump, stub_rocket_dump): @pytest.fixture -def stub_flight_summary(): +def stub_flight_summary_dump(): flight_summary = FlightSummary() flight_summary_json = flight_summary.model_dump_json() return json.loads(flight_summary_json) -def test_create_flight(stub_flight): +@pytest.fixture(autouse=True) +def mock_controller_instance(): + with patch( + "lib.routes.flight.FlightController", autospec=True + ) as mock_controller: + mock_controller_instance = mock_controller.return_value + mock_controller_instance.post_flight = Mock() + mock_controller_instance.get_flight = Mock() + mock_controller_instance.put_flight = Mock() + mock_controller_instance.delete_flight_by_id = Mock() + yield mock_controller_instance + + +def test_create_flight(stub_flight_dump, mock_controller_instance): + mock_response = AsyncMock(return_value=FlightCreated(flight_id='123')) + mock_controller_instance.post_flight = mock_response with patch.object( - FlightController, - 'create_flight', - return_value=FlightCreated(flight_id='123'), - ) as mock_create_flight: - with patch.object( MotorModel, 'set_motor_kind', side_effect=None - ) as mock_set_motor_kind: - response = client.post( - '/flights/', json=stub_flight, params={'motor_kind': 'HYBRID'} - ) - assert response.status_code == 200 - assert response.json() == { - 'flight_id': '123', - 'message': 'Flight successfully created', - } - mock_set_motor_kind.assert_called_once_with(MotorKinds.HYBRID) - mock_create_flight.assert_called_once_with(FlightModel(**stub_flight)) - - -def test_create_flight_optional_params(stub_flight): - stub_flight.update( + ) as mock_set_motor_kind: + response = client.post('/flights/', json=stub_flight_dump, params={'motor_kind': 'HYBRID'}) + assert response.status_code == 200 + assert response.json() == { + 'flight_id': '123', + 'message': 'flight successfully created', + } + mock_set_motor_kind.assert_called_once_with(MotorKinds.HYBRID) + mock_controller_instance.post_flight.assert_called_once_with( + FlightModel(**stub_flight_dump)) + + +def test_create_flight_optional_params(stub_flight_dump, mock_controller_instance): + stub_flight_dump.update( { 'inclination': 0, 'heading': 0, @@ -77,24 +86,20 @@ def test_create_flight_optional_params(stub_flight): 'verbose': True, } ) + mock_response = AsyncMock(return_value=FlightCreated(flight_id='123')) + mock_controller_instance.post_flight = mock_response with patch.object( - FlightController, - 'create_flight', - return_value=FlightCreated(flight_id='123'), - ) as mock_create_flight: - with patch.object( MotorModel, 'set_motor_kind', side_effect=None - ) as mock_set_motor_kind: - response = client.post( - '/flights/', json=stub_flight, params={'motor_kind': 'HYBRID'} - ) - assert response.status_code == 200 - assert response.json() == { - 'flight_id': '123', - 'message': 'Flight successfully created', - } - mock_set_motor_kind.assert_called_once_with(MotorKinds.HYBRID) - mock_create_flight.assert_called_once_with(FlightModel(**stub_flight)) + ) as mock_set_motor_kind: + response = client.post('/flights/', json=stub_flight_dump, params={'motor_kind': 'HYBRID'}) + assert response.status_code == 200 + assert response.json() == { + 'flight_id': '123', + 'message': 'flight successfully created', + } + mock_set_motor_kind.assert_called_once_with(MotorKinds.HYBRID) + mock_controller_instance.post_flight.assert_called_once_with( + FlightModel(**stub_flight_dump)) def test_create_flight_invalid_input(): @@ -104,122 +109,104 @@ def test_create_flight_invalid_input(): assert response.status_code == 422 -def test_create_flight_server_error(stub_flight): - with patch.object( - FlightController, - 'create_flight', +def test_create_flight_server_error(stub_flight_dump, mock_controller_instance): + mock_response = AsyncMock( side_effect=HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR - ), - ): - response = client.post( - '/flights/', json=stub_flight, params={'motor_kind': 'HYBRID'} ) - assert response.status_code == 500 - assert response.json() == {'detail': 'Internal Server Error'} + ) + mock_controller_instance.post_flight = mock_response + response = client.post('/flights/', json=stub_flight_dump, params={'motor_kind': 'HYBRID'}) + assert response.status_code == 500 + assert response.json() == {'detail': 'Internal Server Error'} -def test_read_flight(stub_flight, stub_rocket_dump, stub_motor_dump): +def test_read_flight(stub_flight_dump, stub_rocket_dump, stub_motor_dump, mock_controller_instance): del stub_rocket_dump['motor'] - del stub_flight['rocket'] - motor_view = MotorView(**stub_motor_dump, selected_motor_kind=MotorKinds.HYBRID) + del stub_flight_dump['rocket'] + motor_view = MotorView(**stub_motor_dump, selected_motor_kind=MotorKinds.HYBRID.value) rocket_view = RocketView(**stub_rocket_dump, motor=motor_view) - flight_view = FlightView(**stub_flight, rocket=rocket_view) - with patch.object( - FlightController, - 'get_flight_by_id', - return_value=flight_view, - ) as mock_read_flight: - response = client.get('/flights/123') - assert response.status_code == 200 - assert response.json() == json.loads(flight_view.model_dump_json()) - mock_read_flight.assert_called_once_with('123') - - -def test_read_flight_not_found(): - with patch.object( - FlightController, - 'get_flight_by_id', - side_effect=HTTPException(status_code=status.HTTP_404_NOT_FOUND), - ) as mock_read_flight: - response = client.get('/flights/123') - assert response.status_code == 404 - assert response.json() == {'detail': 'Not Found'} - mock_read_flight.assert_called_once_with('123') - + flight_view = FlightView(**stub_flight_dump, rocket=rocket_view) + mock_response = AsyncMock(return_value=FlightRetrieved(flight=flight_view)) + mock_controller_instance.get_flight = mock_response + response = client.get('/flights/123') + assert response.status_code == 200 + assert response.json() == json.loads(flight_view.model_dump_json()) + mock_controller_instance.get_flight.assert_called_once_with('123') + + +def test_read_flight_not_found(mock_controller_instance): + mock_controller_instance.get_flight.side_effect = HTTPException( + status_code=status.HTTP_404_NOT_FOUND + ) + response = client.get('/flights/123') + assert response.status_code == 404 + assert response.json() == {'detail': 'Not Found'} -def test_read_flight_server_error(): - with patch.object( - FlightController, - 'get_flight_by_id', - side_effect=HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR - ), - ): - response = client.get('/flights/123') - assert response.status_code == 500 - assert response.json() == {'detail': 'Internal Server Error'} +def test_read_flight_server_error(mock_controller_instance): + mock_controller_instance.get_flight.side_effect = HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR + ) + response = client.get('/flights/123') + assert response.status_code == 500 + assert response.json() == {'detail': 'Internal Server Error'} -def test_update_flight(stub_flight): - with patch.object( - FlightController, - 'update_flight_by_id', - return_value=FlightUpdated(flight_id='123'), - ) as mock_update_flight: - with patch.object( - MotorModel, 'set_motor_kind', side_effect=None - ) as mock_set_motor_kind: - response = client.put( - '/flights/123', - json=stub_flight, - params={'motor_kind': 'GENERIC'}, - ) - assert response.status_code == 200 - assert response.json() == { - 'flight_id': '123', - 'message': 'Flight successfully updated', - } - mock_update_flight.assert_called_once_with( - '123', FlightModel(**stub_flight) - ) - mock_set_motor_kind.assert_called_once_with(MotorKinds.GENERIC) - - -def test_update_env_by_flight_id(stub_environment_dump): +def test_update_flight_by_id(stub_flight_dump, mock_controller_instance): + mock_response = AsyncMock(return_value=FlightUpdated(flight_id='123')) + mock_controller_instance.put_flight = mock_response with patch.object( - FlightController, - 'update_env_by_flight_id', - return_value=FlightUpdated(flight_id='123'), - ) as mock_update_flight: - response = client.put('/flights/123/env', json=stub_environment_dump) + MotorModel, 'set_motor_kind', side_effect=None + ) as mock_set_motor_kind: + response = client.put( + '/flights/123', + json=stub_flight_dump, + params={'motor_kind': 'HYBRID'}, + ) assert response.status_code == 200 assert response.json() == { 'flight_id': '123', 'message': 'Flight successfully updated', } - mock_update_flight.assert_called_once_with('123', env=EnvironmentModel(**stub_environment_dump)) + mock_set_motor_kind.assert_called_once_with(MotorKinds.HYBRID) + mock_controller_instance.put_flight.assert_called_once_with( + '123', FlightModel(**stub_flight_dump) + ) + +def test_update_env_by_flight_id(stub_environment_dump, mock_controller_instance): + mock_response = AsyncMock(return_value=FlightUpdated(flight_id='123')) + mock_controller_instance.update_env_by_flight_id = mock_response + response = client.put('/flights/123/env', json=stub_environment_dump) + assert response.status_code == 200 + assert response.json() == { + 'flight_id': '123', + 'message': 'Flight successfully updated', + } + mock_controller_instance.update_env_by_flight_id.assert_called_once_with( + '123', env=EnvironmentModel(**stub_environment_dump) + ) -def test_update_rocket_by_flight_id(stub_rocket_dump): +def test_update_rocket_by_flight_id(stub_rocket_dump, mock_controller_instance): + mock_response = AsyncMock(return_value=FlightUpdated(flight_id='123')) + mock_controller_instance.update_rocket_by_flight_id = mock_response with patch.object( - FlightController, - 'update_rocket_by_flight_id', - return_value=FlightUpdated(flight_id='123'), - ) as mock_update_flight: + MotorModel, 'set_motor_kind', side_effect=None + ) as mock_set_motor_kind: response = client.put( '/flights/123/rocket', json=stub_rocket_dump, - params={'motor_kind': 'GENERIC'}, + params={'motor_kind': 'HYBRID'}, ) assert response.status_code == 200 assert response.json() == { 'flight_id': '123', 'message': 'Flight successfully updated', } - assert mock_update_flight.call_count == 1 - assert mock_update_flight.call_args[0][0] == '123' - assert mock_update_flight.call_args[1]['rocket'].model_dump() == RocketModel(**stub_rocket_dump).model_dump() + mock_set_motor_kind.assert_called_once_with(MotorKinds.HYBRID) + mock_controller_instance.update_rocket_by_flight_id.assert_called_once_with( + '123', RocketModel(**stub_rocket_dump) + ) def test_update_env_by_flight_id_invalid_input(): @@ -241,132 +228,146 @@ def test_update_flight_invalid_input(): assert response.status_code == 422 -def test_update_flight_not_found(stub_flight): - with patch.object( - FlightController, - 'update_flight_by_id', - side_effect=HTTPException(status_code=status.HTTP_404_NOT_FOUND), - ): - response = client.put( - '/flights/123', json=stub_flight, params={'motor_kind': 'HYBRID'} - ) - assert response.status_code == 404 - assert response.json() == {'detail': 'Not Found'} +def test_update_flight_not_found(stub_flight_dump, mock_controller_instance): + mock_controller_instance.put_flight.side_effect = HTTPException( + status_code=status.HTTP_404_NOT_FOUND + ) + response = client.put( + '/flights/123', + json=stub_flight_dump, + params={'motor_kind': 'HYBRID'}, + ) + assert response.status_code == 404 + assert response.json() == {'detail': 'Not Found'} -def test_update_flight_server_error(stub_flight): - with patch.object( - FlightController, - 'update_flight_by_id', - side_effect=HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR - ), - ): - response = client.put( - '/flights/123', json=stub_flight, params={'motor_kind': 'HYBRID'} - ) - assert response.status_code == 500 - assert response.json() == {'detail': 'Internal Server Error'} +def test_update_flight_server_error(stub_flight_dump, mock_controller_instance): + mock_controller_instance.put_flight.side_effect = HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR + ) + response = client.put( + '/flights/123', + json=stub_flight_dump, + params={'motor_kind': 'HYBRID'}, + ) + assert response.status_code == 500 + assert response.json() == {'detail': 'Internal Server Error'} -def test_delete_flight(): - with patch.object( - FlightController, - 'delete_flight_by_id', - return_value=FlightDeleted(flight_id='123'), - ) as mock_delete_flight: - response = client.delete('/flights/123') - assert response.status_code == 200 - assert response.json() == { - 'flight_id': '123', - 'message': 'Flight successfully deleted', - } - mock_delete_flight.assert_called_once_with('123') +def test_update_env_by_flight_id_not_found(stub_environment_dump, mock_controller_instance): + mock_controller_instance.update_env_by_flight_id.side_effect = HTTPException( + status_code=status.HTTP_404_NOT_FOUND + ) + response = client.put('/flights/123/env', json=stub_environment_dump) + assert response.status_code == 404 + assert response.json() == {'detail': 'Not Found'} -def test_delete_flight_server_error(): - with patch.object( - FlightController, - 'delete_flight_by_id', - side_effect=HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR - ), - ): - response = client.delete('/flights/123') - assert response.status_code == 500 - assert response.json() == {'detail': 'Internal Server Error'} +def test_update_env_by_flight_id_server_error(stub_environment_dump, mock_controller_instance): + mock_controller_instance.update_env_by_flight_id.side_effect = HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR + ) + response = client.put('/flights/123/env', json=stub_environment_dump) + assert response.status_code == 500 + assert response.json() == {'detail': 'Internal Server Error'} -def test_simulate_flight(stub_flight_summary): - with patch.object( - FlightController, - 'simulate_flight', - return_value=FlightSummary(**stub_flight_summary), - ) as mock_simulate_flight: - response = client.get('/flights/123/summary') - assert response.status_code == 200 - assert response.json() == stub_flight_summary - mock_simulate_flight.assert_called_once_with('123') +def test_update_rocket_by_flight_id_not_found(stub_rocket_dump, mock_controller_instance): + mock_controller_instance.update_rocket_by_flight_id.side_effect = HTTPException( + status_code=status.HTTP_404_NOT_FOUND + ) + response = client.put( + '/flights/123/rocket', + json=stub_rocket_dump, + params={'motor_kind': 'HYBRID'}, + ) + assert response.status_code == 404 + assert response.json() == {'detail': 'Not Found'} -def test_simulate_flight_not_found(): - with patch.object( - FlightController, - 'simulate_flight', - side_effect=HTTPException(status_code=status.HTTP_404_NOT_FOUND), - ) as mock_simulate_flight: - response = client.get('/flights/123/summary') - assert response.status_code == 404 - assert response.json() == {'detail': 'Not Found'} - mock_simulate_flight.assert_called_once_with('123') +def test_update_rocket_by_flight_id_server_error(stub_rocket_dump, mock_controller_instance): + mock_controller_instance.update_rocket_by_flight_id.side_effect = HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR + ) + response = client.put( + '/flights/123/rocket', + json=stub_rocket_dump, + params={'motor_kind': 'HYBRID'}, + ) + assert response.status_code == 500 + assert response.json() == {'detail': 'Internal Server Error'} + + +def test_delete_flight(mock_controller_instance): + mock_response = AsyncMock(return_value=FlightDeleted(flight_id='123')) + mock_controller_instance.delete_flight_by_id = mock_response + response = client.delete('/flights/123') + assert response.status_code == 200 + assert response.json() == { + 'flight_id': '123', + 'message': 'Flight successfully deleted', + } + mock_controller_instance.delete_flight_by_id.assert_called_once_with('123') -def test_simulate_flight_server_error(): - with patch.object( - FlightController, - 'simulate_flight', - side_effect=HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR - ), - ): - response = client.get('/flights/123/summary') - assert response.status_code == 500 - assert response.json() == {'detail': 'Internal Server Error'} +def test_delete_flight_server_error(mock_controller_instance): + mock_controller_instance.delete_flight_by_id.side_effect = HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR + ) + response = client.delete('/flights/123') + assert response.status_code == 500 + assert response.json() == {'detail': 'Internal Server Error'} -def test_read_rocketpy_flight(): - with patch.object( - FlightController, - 'get_rocketpy_flight_binary', - return_value=b'rocketpy', - ) as mock_read_rocketpy_flight: - response = client.get('/flights/123/rocketpy') - assert response.status_code == 203 - assert response.content == b'rocketpy' - assert response.headers['content-type'] == 'application/octet-stream' - mock_read_rocketpy_flight.assert_called_once_with('123') - - -def test_read_rocketpy_flight_not_found(): - with patch.object( - FlightController, - 'get_rocketpy_flight_binary', - side_effect=HTTPException(status_code=status.HTTP_404_NOT_FOUND), - ) as mock_read_rocketpy_flight: - response = client.get('/flights/123/rocketpy') - assert response.status_code == 404 - assert response.json() == {'detail': 'Not Found'} - mock_read_rocketpy_flight.assert_called_once_with('123') +def test_simulate_flight(stub_flight_summary_dump, mock_controller_instance): + mock_response = AsyncMock(return_value=FlightSummary(**stub_flight_summary_dump)) + mock_controller_instance.simulate_flight = mock_response + response = client.get('/flights/123/summary') + assert response.status_code == 200 + assert response.json() == stub_flight_summary_dump + mock_controller_instance.simulate_flight.assert_called_once_with('123') -def test_read_rocketpy_flight_server_error(): - with patch.object( - FlightController, - 'get_rocketpy_flight_binary', - side_effect=HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR - ), - ): - response = client.get('/flights/123/rocketpy') - assert response.status_code == 500 - assert response.json() == {'detail': 'Internal Server Error'} +def test_simulate_flight_not_found(mock_controller_instance): + mock_controller_instance.simulate_flight.side_effect = HTTPException( + status_code=status.HTTP_404_NOT_FOUND + ) + response = client.get('/flights/123/summary') + assert response.status_code == 404 + assert response.json() == {'detail': 'Not Found'} + + +def test_simulate_flight_server_error(mock_controller_instance): + mock_controller_instance.simulate_flight.side_effect = HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR + ) + response = client.get('/flights/123/summary') + assert response.status_code == 500 + assert response.json() == {'detail': 'Internal Server Error'} + + +def test_read_rocketpy_flight_binary(mock_controller_instance): + mock_controller_instance.get_rocketpy_flight_binary = AsyncMock(return_value=b'rocketpy') + response = client.get('/flights/123/rocketpy') + assert response.status_code == 203 + assert response.content == b'rocketpy' + assert response.headers['content-type'] == 'application/octet-stream' + mock_controller_instance.get_rocketpy_flight_binary.assert_called_once_with('123') + + +def test_read_rocketpy_flight_binary_not_found(mock_controller_instance): + mock_controller_instance.get_rocketpy_flight_binary.side_effect = HTTPException( + status_code=status.HTTP_404_NOT_FOUND + ) + response = client.get('/flights/123/rocketpy') + assert response.status_code == 404 + assert response.json() == {'detail': 'Not Found'} + + +def test_read_rocketpy_flight_binary_server_error(mock_controller_instance): + mock_controller_instance.get_rocketpy_flight_binary.side_effect = HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR + ) + response = client.get('/flights/123/rocketpy') + assert response.status_code == 500 + assert response.json() == {'detail': 'Internal Server Error'} diff --git a/tests/test_routes/test_rockets_route.py b/tests/test_routes/test_rockets_route.py index 5e17eb4..ad6998c 100644 --- a/tests/test_routes/test_rockets_route.py +++ b/tests/test_routes/test_rockets_route.py @@ -78,10 +78,10 @@ def mock_controller_instance(): "lib.routes.rocket.RocketController", autospec=True ) as mock_controller: mock_controller_instance = mock_controller.return_value - mock_controller_instance.post_brecho = Mock() - mock_controller_instance.get_brecho_by_id = Mock() - mock_controller_instance.put_brecho_by_id = Mock() - mock_controller_instance.delete_brecho_by_id = Mock() + mock_controller_instance.post_rocket = Mock() + mock_controller_instance.get_rocket_by_id = Mock() + mock_controller_instance.put_rocket_by_id = Mock() + mock_controller_instance.delete_rocket_by_id = Mock() yield mock_controller_instance From 6a9ff31ea0c99afccdfe7d1f87d9a99fc69e08ba Mon Sep 17 00:00:00 2001 From: Gabriel Barberini Date: Sun, 16 Feb 2025 17:50:09 -0300 Subject: [PATCH 10/34] fixes tests and runs formatting --- lib/controllers/environment.py | 14 +- lib/controllers/flight.py | 10 +- lib/controllers/motor.py | 10 +- lib/controllers/rocket.py | 14 +- lib/models/environment.py | 9 +- lib/models/interface.py | 19 +- lib/models/motor.py | 12 +- lib/models/rocket.py | 4 +- lib/models/sub/aerosurfaces.py | 4 +- lib/repositories/environment.py | 12 +- lib/repositories/interface.py | 7 +- lib/routes/environment.py | 30 +- lib/routes/flight.py | 22 +- lib/routes/motor.py | 18 +- lib/routes/rocket.py | 18 +- lib/services/environment.py | 14 +- lib/services/flight.py | 12 +- lib/services/motor.py | 12 +- lib/services/rocket.py | 12 +- lib/utils.py | 39 ++- lib/views/environment.py | 57 ++-- lib/views/flight.py | 261 +++++++++--------- lib/views/motor.py | 67 +++-- lib/views/rocket.py | 47 ++-- pyproject.toml | 5 + requirements-dev.txt | 1 + .../test_controller_interface.py | 4 +- tests/test_routes/test_environments_route.py | 108 +++++--- tests/test_routes/test_flights_route.py | 205 +++++++++----- tests/test_routes/test_motors_route.py | 201 +++++++++----- tests/test_routes/test_rockets_route.py | 205 +++++++++----- 31 files changed, 863 insertions(+), 590 deletions(-) diff --git a/lib/controllers/environment.py b/lib/controllers/environment.py index 42d3a6b..c5a81e7 100644 --- a/lib/controllers/environment.py +++ b/lib/controllers/environment.py @@ -2,7 +2,7 @@ ControllerInterface, controller_exception_handler, ) -from lib.views.environment import EnvironmentSummary +from lib.views.environment import EnvironmentSimulation from lib.models.environment import EnvironmentModel from lib.services.environment import EnvironmentService @@ -36,14 +36,14 @@ async def get_rocketpy_env_binary( Raises: HTTP 404 Not Found: If the env is not found in the database. """ - env = await self.get_env_by_id(env_id) + env = await self.get_environment_by_id(env_id) env_service = EnvironmentService.from_env_model(env) - return env_service.get_env_binary() + return env_service.get_environment_binary() @controller_exception_handler async def get_environment_simulation( self, env_id: str - ) -> EnvironmentSummary: + ) -> EnvironmentSimulation: """ Simulate a rocket environment. @@ -51,11 +51,11 @@ async def get_environment_simulation( env_id: str. Returns: - EnvironmentSummary + EnvironmentSimulation Raises: HTTP 404 Not Found: If the env does not exist in the database. """ - env = await self.get_env_by_id(env_id) + env = await self.get_environment_by_id(env_id) env_service = EnvironmentService.from_env_model(env) - return env_service.get_env_summary() + return env_service.get_environment_simulation() diff --git a/lib/controllers/flight.py b/lib/controllers/flight.py index d01514f..00f3c16 100644 --- a/lib/controllers/flight.py +++ b/lib/controllers/flight.py @@ -2,7 +2,7 @@ ControllerInterface, controller_exception_handler, ) -from lib.views.flight import FlightSummary, FlightUpdated +from lib.views.flight import FlightSimulation, FlightUpdated from lib.models.flight import FlightModel from lib.models.environment import EnvironmentModel from lib.models.rocket import RocketModel @@ -87,10 +87,10 @@ async def get_rocketpy_flight_binary( return flight_service.get_flight_binary() @controller_exception_handler - async def simulate_flight( + async def get_flight_simulation( self, flight_id: str, - ) -> FlightSummary: + ) -> FlightSimulation: """ Simulate a rocket flight. @@ -98,11 +98,11 @@ async def simulate_flight( flight_id: str Returns: - Flight summary view. + Flight simulation view. Raises: HTTP 404 Not Found: If the flight does not exist in the database. """ flight = await self.get_flight_by_id(flight_id=flight_id) flight_service = FlightService.from_flight_model(flight) - return flight_service.get_flight_summary() + return flight_service.get_flight_simmulation() diff --git a/lib/controllers/motor.py b/lib/controllers/motor.py index c73590c..73b2704 100644 --- a/lib/controllers/motor.py +++ b/lib/controllers/motor.py @@ -2,7 +2,7 @@ ControllerInterface, controller_exception_handler, ) -from lib.views.motor import MotorSummary +from lib.views.motor import MotorSimulation from lib.models.motor import MotorModel from lib.services.motor import MotorService @@ -41,9 +41,7 @@ async def get_rocketpy_motor_binary( return motor_service.get_motor_binary() @controller_exception_handler - async def simulate_motor( - self, motor_id: str - ) -> MotorSummary: + async def get_motor_simulation(self, motor_id: str) -> MotorSimulation: """ Simulate a rocketpy motor. @@ -51,11 +49,11 @@ async def simulate_motor( motor_id: str Returns: - views.MotorSummary + views.MotorSimulation Raises: HTTP 404 Not Found: If the motor does not exist in the database. """ motor = await self.get_motor_by_id(motor_id) motor_service = MotorService.from_motor_model(motor) - return motor_service.get_motor_summary() + return motor_service.get_motor_simulation() diff --git a/lib/controllers/rocket.py b/lib/controllers/rocket.py index 8cbedca..4a1a131 100644 --- a/lib/controllers/rocket.py +++ b/lib/controllers/rocket.py @@ -2,7 +2,7 @@ ControllerInterface, controller_exception_handler, ) -from lib.views.rocket import RocketSummary +from lib.views.rocket import RocketSimulation from lib.models.rocket import RocketModel from lib.services.rocket import RocketService @@ -20,9 +20,7 @@ def __init__(self): super().__init__(models=[RocketModel]) @controller_exception_handler - async def get_rocketpy_rocket_binary( - self, rocket_id: str - ) -> bytes: + async def get_rocketpy_rocket_binary(self, rocket_id: str) -> bytes: """ Get a rocketpy.Rocket object as dill binary. @@ -40,10 +38,10 @@ async def get_rocketpy_rocket_binary( return rocket_service.get_rocket_binary() @controller_exception_handler - async def simulate_rocket( + async def get_rocket_simulation( self, rocket_id: str, - ) -> RocketSummary: + ) -> RocketSimulation: """ Simulate a rocketpy rocket. @@ -51,11 +49,11 @@ async def simulate_rocket( rocket_id: str Returns: - views.RocketSummary + views.RocketSimulation Raises: HTTP 404 Not Found: If the rocket does not exist in the database. """ rocket = await self.get_rocket_by_id(rocket_id) rocket_service = RocketService.from_rocket_model(rocket) - return rocket_service.get_rocket_summary() + return rocket_service.get_rocket_simulation() diff --git a/lib/models/environment.py b/lib/models/environment.py index e922506..58d31c4 100644 --- a/lib/models/environment.py +++ b/lib/models/environment.py @@ -11,7 +11,14 @@ class EnvironmentModel(ApiBaseModel): elevation: Optional[int] = 1 # Optional parameters - atmospheric_model_type: Literal['standard_atmosphere', 'custom_atmosphere', 'wyoming_sounding', 'forecast', 'reanalysis', 'ensemble'] = 'standard_atmosphere' + atmospheric_model_type: Literal[ + 'standard_atmosphere', + 'custom_atmosphere', + 'wyoming_sounding', + 'forecast', + 'reanalysis', + 'ensemble', + ] = 'standard_atmosphere' atmospheric_model_file: Optional[str] = None date: Optional[datetime.datetime] = ( datetime.datetime.today() + datetime.timedelta(days=1) diff --git a/lib/models/interface.py b/lib/models/interface.py index 6bf4a5a..ddebd9f 100644 --- a/lib/models/interface.py +++ b/lib/models/interface.py @@ -1,6 +1,11 @@ from typing import Self, Optional from abc import abstractmethod, ABC -from pydantic import BaseModel, PrivateAttr, ConfigDict +from pydantic import ( + BaseModel, + PrivateAttr, + ConfigDict, + model_validator, +) from bson import ObjectId @@ -8,7 +13,6 @@ class ApiBaseModel(BaseModel, ABC): _id: Optional[ObjectId] = PrivateAttr(default=None) model_config = ConfigDict( extra="allow", - json_encoders={ObjectId: str}, use_enum_values=True, validate_default=True, validate_all_in_root=True, @@ -21,6 +25,17 @@ def set_id(self, value): def get_id(self): return self._id + @model_validator(mode='after') + def validate_computed_id(self): + """Validate _id after model instantiation""" + if self._id is not None: + if not isinstance(self._id, ObjectId): + try: + self._id = ObjectId(str(self._id)) + except Exception as e: + raise ValueError(f"Invalid ObjectId: {e}") + return self + @property @abstractmethod def NAME(): # pylint: disable=invalid-name, no-method-argument diff --git a/lib/models/motor.py b/lib/models/motor.py index 6b4c8af..94a59ae 100644 --- a/lib/models/motor.py +++ b/lib/models/motor.py @@ -48,8 +48,12 @@ class MotorModel(ApiBaseModel): throat_radius: Optional[float] = None # Optional parameters - interpolation_method: Literal['linear', 'spline', 'akima', 'polynomial', 'shepard', 'rbf'] = 'linear' - coordinate_system_orientation: Literal['nozzle_to_combustion_chamber', 'combustion_chamber_to_nozzle'] = 'nose_to_combustion_chamber' + interpolation_method: Literal[ + 'linear', 'spline', 'akima', 'polynomial', 'shepard', 'rbf' + ] = 'linear' + coordinate_system_orientation: Literal[ + 'nozzle_to_combustion_chamber', 'combustion_chamber_to_nozzle' + ] = 'nozzle_to_combustion_chamber' reshape_thrust_curve: Union[bool, tuple] = False # Computed parameters @@ -62,7 +66,9 @@ def validate_motor_kind(self): self._motor_kind not in (MotorKinds.SOLID, MotorKinds.GENERIC) and self.tanks is None ): - raise ValueError("Tanks must be provided for liquid and hybrid motors.") + raise ValueError( + "Tanks must be provided for liquid and hybrid motors." + ) return self @computed_field diff --git a/lib/models/rocket.py b/lib/models/rocket.py index 39def1e..872a80a 100644 --- a/lib/models/rocket.py +++ b/lib/models/rocket.py @@ -26,7 +26,9 @@ class RocketModel(ApiBaseModel): ] = (0, 0, 0) power_off_drag: List[Tuple[float, float]] = [(0, 0)] power_on_drag: List[Tuple[float, float]] = [(0, 0)] - coordinate_system_orientation: Literal['tail_to_nose', 'nose_to_tail'] = 'tail_to_nose' + coordinate_system_orientation: Literal['tail_to_nose', 'nose_to_tail'] = ( + 'tail_to_nose' + ) nose: NoseCone fins: List[Fins] diff --git a/lib/models/sub/aerosurfaces.py b/lib/models/sub/aerosurfaces.py index 6e29ee7..966770e 100644 --- a/lib/models/sub/aerosurfaces.py +++ b/lib/models/sub/aerosurfaces.py @@ -39,7 +39,9 @@ class Fins(BaseModel): tip_chord: Optional[float] = None cant_angle: Optional[float] = None rocket_radius: Optional[float] = None - airfoil: Optional[Tuple[List[Tuple[float, float]], Literal['radians', 'degrees']]] = None + airfoil: Optional[ + Tuple[List[Tuple[float, float]], Literal['radians', 'degrees']] + ] = None def get_additional_parameters(self): return { diff --git a/lib/repositories/environment.py b/lib/repositories/environment.py index 2f37632..5f30911 100644 --- a/lib/repositories/environment.py +++ b/lib/repositories/environment.py @@ -22,12 +22,18 @@ async def create_environment(self, environment: EnvironmentModel) -> str: return await self.insert(environment.model_dump()) @repository_exception_handler - async def read_environment_by_id(self, environment_id: str) -> Optional[EnvironmentModel]: + async def read_environment_by_id( + self, environment_id: str + ) -> Optional[EnvironmentModel]: return await self.find_by_id(data_id=environment_id) @repository_exception_handler - async def update_environment_by_id(self, environment_id: str, environment: EnvironmentModel): - await self.update_by_id(environment.model_dump(), data_id=environment_id) + async def update_environment_by_id( + self, environment_id: str, environment: EnvironmentModel + ): + await self.update_by_id( + environment.model_dump(), data_id=environment_id + ) @repository_exception_handler async def delete_environment_by_id(self, environment_id: str): diff --git a/lib/repositories/interface.py b/lib/repositories/interface.py index 9b97595..4242da6 100644 --- a/lib/repositories/interface.py +++ b/lib/repositories/interface.py @@ -40,9 +40,12 @@ async def wrapper(self, *args, **kwargs): return await method(self, *args, **kwargs) except PyMongoError as e: logger.exception(f"{method.__name__} - caught PyMongoError: {e}") - raise e from e + raise except RepositoryNotInitializedException as e: - raise e from e + logger.exception( + f"{method.__name__} - Repository not initialized: {e}" + ) + raise except Exception as e: logger.exception( f"{method.__name__} - caught unexpected error: {e}" diff --git a/lib/routes/environment.py b/lib/routes/environment.py index 94e339e..a0837e3 100644 --- a/lib/routes/environment.py +++ b/lib/routes/environment.py @@ -6,7 +6,7 @@ from opentelemetry import trace from lib.views.environment import ( - EnvironmentSummary, + EnvironmentSimulation, EnvironmentCreated, EnvironmentRetrieved, EnvironmentUpdated, @@ -29,7 +29,9 @@ @router.post("/") -async def create_environment(environment: EnvironmentModel) -> EnvironmentCreated: +async def create_environment( + environment: EnvironmentModel, +) -> EnvironmentCreated: """ Creates a new environment @@ -55,7 +57,9 @@ async def read_environment(environment_id: str) -> EnvironmentRetrieved: @router.put("/{environment_id}") -async def update_environment(environment_id: str, environment: EnvironmentModel) -> EnvironmentUpdated: +async def update_environment( + environment_id: str, environment: EnvironmentModel +) -> EnvironmentUpdated: """ Updates an existing environment @@ -67,7 +71,9 @@ async def update_environment(environment_id: str, environment: EnvironmentModel) """ with tracer.start_as_current_span("update_becho"): controller = EnvironmentController() - return await controller.put_environment_by_id(environment_id, environment) + return await controller.put_environment_by_id( + environment_id, environment + ) @router.delete("/{environment_id}") @@ -107,7 +113,9 @@ async def get_rocketpy_environment_binary(environment_id: str): 'Content-Disposition': f'attachment; filename="rocketpy_environment_{environment_id}.dill"' } controller = EnvironmentController() - binary = await controller.get_rocketpy_environment_binary(environment_id) + binary = await controller.get_rocketpy_environment_binary( + environment_id + ) return Response( content=binary, headers=headers, @@ -116,14 +124,16 @@ async def get_rocketpy_environment_binary(environment_id: str): ) -@router.get("/{environment_id}/summary") -async def get_environment_simulation(environment_id: str) -> EnvironmentSummary: +@router.get("/{environment_id}/simulate") +async def get_environment_simulation( + environment_id: str, +) -> EnvironmentSimulation: """ - Loads rocketpy.environment simulation + Simulates an environment ## Args - ``` environment_id: str ``` + ``` environment_id: Environment ID``` """ with tracer.start_as_current_span("get_environment_simulation"): controller = EnvironmentController() - return await controller.get_environment_summary(environment_id) + return await controller.get_environment_simulation(environment_id) diff --git a/lib/routes/flight.py b/lib/routes/flight.py index 4ae5d9c..6b84684 100644 --- a/lib/routes/flight.py +++ b/lib/routes/flight.py @@ -6,7 +6,7 @@ from opentelemetry import trace from lib.views.flight import ( - FlightSummary, + FlightSimulation, FlightCreated, FlightRetrieved, FlightUpdated, @@ -32,7 +32,9 @@ @router.post("/") -async def create_flight(flight: FlightModel, motor_kind: MotorKinds) -> FlightCreated: +async def create_flight( + flight: FlightModel, motor_kind: MotorKinds +) -> FlightCreated: """ Creates a new flight @@ -59,7 +61,9 @@ async def read_flight(flight_id: str) -> FlightRetrieved: @router.put("/{flight_id}") -async def update_flight(flight_id: str, flight: FlightModel, motor_kind: MotorKinds) -> FlightUpdated: +async def update_flight( + flight_id: str, flight: FlightModel, motor_kind: MotorKinds +) -> FlightUpdated: """ Updates an existing flight @@ -122,7 +126,9 @@ async def get_rocketpy_flight_binary(flight_id: str): @router.put("/{flight_id}/environment") -async def update_flight_environment(flight_id: str, environment: EnvironmentModel) -> FlightUpdated: +async def update_flight_environment( + flight_id: str, environment: EnvironmentModel +) -> FlightUpdated: """ Updates flight environment @@ -163,14 +169,14 @@ async def update_flight_rocket( ) -@router.get("/{flight_id}/summary") -async def simulate_flight(flight_id: str) -> FlightSummary: +@router.get("/{flight_id}/simulate") +async def get_flight_simulation(flight_id: str) -> FlightSimulation: """ Simulates a flight ## Args ``` flight_id: Flight ID ``` """ - with tracer.start_as_current_span("simulate_flight"): + with tracer.start_as_current_span("get_flight_simulation"): controller = FlightController() - return await controller.simulate_flight(flight_id) + return await controller.get_flight_simulation(flight_id) diff --git a/lib/routes/motor.py b/lib/routes/motor.py index 969a419..b5525a3 100644 --- a/lib/routes/motor.py +++ b/lib/routes/motor.py @@ -6,7 +6,7 @@ from opentelemetry import trace from lib.views.motor import ( - MotorSummary, + MotorSimulation, MotorCreated, MotorRetrieved, MotorUpdated, @@ -29,7 +29,9 @@ @router.post("/") -async def create_motor(motor: MotorModel, motor_kind: MotorKinds) -> MotorCreated: +async def create_motor( + motor: MotorModel, motor_kind: MotorKinds +) -> MotorCreated: """ Creates a new motor @@ -56,7 +58,9 @@ async def read_motor(motor_id: str) -> MotorRetrieved: @router.put("/{motor_id}") -async def update_motor(motor_id: str, motor: MotorModel, motor_kind: MotorKinds) -> MotorUpdated: +async def update_motor( + motor_id: str, motor: MotorModel, motor_kind: MotorKinds +) -> MotorUpdated: """ Updates an existing motor @@ -118,14 +122,14 @@ async def get_rocketpy_motor_binary(motor_id: str): ) -@router.get("/{motor_id}/summary") -async def simulate_motor(motor_id: str) -> MotorSummary: +@router.get("/{motor_id}/simulate") +async def get_motor_simulation(motor_id: str) -> MotorSimulation: """ Simulates a motor ## Args ``` motor_id: Motor ID ``` """ - with tracer.start_as_current_span("simulate_motor"): + with tracer.start_as_current_span("get_motor_simulation"): controller = MotorController() - return await controller.simulate_motor(motor_id) + return await controller.get_motor_simulation(motor_id) diff --git a/lib/routes/rocket.py b/lib/routes/rocket.py index d9be0c4..1fa0e23 100644 --- a/lib/routes/rocket.py +++ b/lib/routes/rocket.py @@ -6,7 +6,7 @@ from opentelemetry import trace from lib.views.rocket import ( - RocketSummary, + RocketSimulation, RocketCreated, RocketRetrieved, RocketUpdated, @@ -30,7 +30,9 @@ @router.post("/") -async def create_rocket(rocket: RocketModel, motor_kind: MotorKinds) -> RocketCreated: +async def create_rocket( + rocket: RocketModel, motor_kind: MotorKinds +) -> RocketCreated: """ Creates a new rocket @@ -57,7 +59,9 @@ async def read_rocket(rocket_id: str) -> RocketRetrieved: @router.put("/{rocket_id}") -async def update_rocket(rocket_id: str, rocket: RocketModel, motor_kind: MotorKinds) -> RocketUpdated: +async def update_rocket( + rocket_id: str, rocket: RocketModel, motor_kind: MotorKinds +) -> RocketUpdated: """ Updates an existing rocket @@ -119,14 +123,14 @@ async def get_rocketpy_rocket_binary(rocket_id: str): ) -@router.get("/{rocket_id}/summary") -async def simulate_rocket(rocket_id: str) -> RocketSummary: +@router.get("/{rocket_id}/simulate") +async def simulate_rocket(rocket_id: str) -> RocketSimulation: """ Simulates a rocket ## Args ``` rocket_id: Rocket ID ``` """ - with tracer.start_as_current_span("simulate_rocket"): + with tracer.start_as_current_span("get_rocket_simulation"): controller = RocketController() - return await controller.simulate_rocket(rocket_id) + return await controller.get_rocket_simulation(rocket_id) diff --git a/lib/services/environment.py b/lib/services/environment.py index 0220ffc..2391b5b 100644 --- a/lib/services/environment.py +++ b/lib/services/environment.py @@ -5,7 +5,7 @@ from rocketpy.environment.environment import Environment as RocketPyEnvironment from rocketpy.utilities import get_instance_attributes from lib.models.environment import EnvironmentModel -from lib.views.environment import EnvironmentSummary +from lib.views.environment import EnvironmentSimulation class EnvironmentService: @@ -42,19 +42,19 @@ def environment(self) -> RocketPyEnvironment: def environment(self, environment: RocketPyEnvironment): self._environment = environment - def get_env_summary(self) -> EnvironmentSummary: + def get_environment_simulation(self) -> EnvironmentSimulation: """ - Get the summary of the environment. + Get the simulation of the environment. Returns: - EnvironmentSummary + EnvironmentSimulation """ attributes = get_instance_attributes(self.environment) - env_summary = EnvironmentSummary(**attributes) - return env_summary + env_simulation = EnvironmentSimulation(**attributes) + return env_simulation - def get_env_binary(self) -> bytes: + def get_environment_binary(self) -> bytes: """ Get the binary representation of the environment. diff --git a/lib/services/flight.py b/lib/services/flight.py index 65d3c10..47e50da 100644 --- a/lib/services/flight.py +++ b/lib/services/flight.py @@ -8,7 +8,7 @@ from lib.services.environment import EnvironmentService from lib.services.rocket import RocketService from lib.models.flight import FlightModel -from lib.views.flight import FlightSummary +from lib.views.flight import FlightSimulation class FlightService: @@ -48,16 +48,16 @@ def flight(self) -> RocketPyFlight: def flight(self, flight: RocketPyFlight): self._flight = flight - def get_flight_summary(self) -> FlightSummary: + def get_flight_simulation(self) -> FlightSimulation: """ - Get the summary of the flight. + Get the simulation of the flight. Returns: - FlightSummary + FlightSimulation """ attributes = get_instance_attributes(self.flight) - flight_summary = FlightSummary(**attributes) - return flight_summary + flight_simulation = FlightSimulation(**attributes) + return flight_simulation def get_flight_binary(self) -> bytes: """ diff --git a/lib/services/motor.py b/lib/services/motor.py index be62d82..df04302 100644 --- a/lib/services/motor.py +++ b/lib/services/motor.py @@ -17,7 +17,7 @@ from lib.models.sub.tanks import TankKinds from lib.models.motor import MotorKinds, MotorModel -from lib.views.motor import MotorSummary +from lib.views.motor import MotorSimulation class MotorService: @@ -133,16 +133,16 @@ def motor(self) -> RocketPyMotor: def motor(self, motor: RocketPyMotor): self._motor = motor - def get_motor_summary(self) -> MotorSummary: + def get_motor_simulation(self) -> MotorSimulation: """ - Get the summary of the motor. + Get the simulation of the motor. Returns: - MotorSummary + MotorSimulation """ attributes = get_instance_attributes(self.motor) - motor_summary = MotorSummary(**attributes) - return motor_summary + motor_simulation = MotorSimulation(**attributes) + return motor_simulation def get_motor_binary(self) -> bytes: """ diff --git a/lib/services/rocket.py b/lib/services/rocket.py index 0bc8ed4..77c7533 100644 --- a/lib/services/rocket.py +++ b/lib/services/rocket.py @@ -17,7 +17,7 @@ from lib.models.rocket import RocketModel, Parachute from lib.models.sub.aerosurfaces import NoseCone, Tail, Fins from lib.services.motor import MotorService -from lib.views.rocket import RocketSummary +from lib.views.rocket import RocketSimulation class RocketService: @@ -100,16 +100,16 @@ def rocket(self) -> RocketPyRocket: def rocket(self, rocket: RocketPyRocket): self._rocket = rocket - def get_rocket_summary(self) -> RocketSummary: + def get_rocket_simulation(self) -> RocketSimulation: """ - Get the summary of the rocket. + Get the simulation of the rocket. Returns: - RocketSummary + RocketSimulation """ attributes = get_instance_attributes(self.rocket) - rocket_summary = RocketSummary(**attributes) - return rocket_summary + rocket_simulation = RocketSimulation(**attributes) + return rocket_simulation def get_rocket_binary(self) -> bytes: """ diff --git a/lib/utils.py b/lib/utils.py index db3077c..9fdd7f8 100644 --- a/lib/utils.py +++ b/lib/utils.py @@ -1,14 +1,35 @@ # fork of https://github.com/encode/starlette/blob/master/starlette/middleware/gzip.py import gzip import io -import typing +from typing import Annotated, NoReturn, Any import numpy as np +from pydantic import BeforeValidator, PlainSerializer from starlette.datastructures import Headers, MutableHeaders from starlette.types import ASGIApp, Message, Receive, Scope, Send +def to_python_primitive(v): + if hasattr(v, "source"): + if isinstance(v.source, np.ndarray): + return v.source.tolist() + + if isinstance(v.source, (np.generic,)): + return v.source.item() + + return str(v.source) + + return str(v) + + +AnyToPrimitive = Annotated[ + Any, + BeforeValidator(lambda v: v), + PlainSerializer(to_python_primitive), +] + + class RocketPyGZipMiddleware: def __init__( self, app: ASGIApp, minimum_size: int = 500, compresslevel: int = 9 @@ -124,20 +145,8 @@ async def send_with_gzip(self, message: Message) -> None: self.gzip_buffer.truncate() await self.send(message) + return -async def unattached_send(message: Message) -> typing.NoReturn: +async def unattached_send(message: Message) -> NoReturn: raise RuntimeError("send awaitable not set") # pragma: no cover - - -def to_python_primitive(v): - if hasattr(v, "source"): - if isinstance(v.source, np.ndarray): - return v.source.tolist() - - if isinstance(v.source, (np.generic,)): - return v.source.item() - - return str(v.source) - - return str(v) diff --git a/lib/views/environment.py b/lib/views/environment.py index 60af42f..d0ca44d 100644 --- a/lib/views/environment.py +++ b/lib/views/environment.py @@ -1,18 +1,15 @@ -from typing import Optional, Any +from typing import Optional from datetime import datetime, timedelta -from pydantic import ConfigDict from lib.views.interface import ApiBaseView -from lib.models.environment import AtmosphericModelTypes, EnvironmentModel -from lib.utils import to_python_primitive +from lib.models.environment import EnvironmentModel +from lib.utils import AnyToPrimitive -class EnvironmentSummary(ApiBaseView): +class EnvironmentSimulation(ApiBaseView): latitude: Optional[float] = None longitude: Optional[float] = None elevation: Optional[float] = 1 - atmospheric_model_type: Optional[str] = ( - AtmosphericModelTypes.STANDARD_ATMOSPHERE.value - ) + atmospheric_model_type: Optional[str] = None air_gas_constant: Optional[float] = None standard_g: Optional[float] = None earth_radius: Optional[float] = None @@ -28,29 +25,27 @@ class EnvironmentSummary(ApiBaseView): date: Optional[datetime] = datetime.today() + timedelta(days=1) local_date: Optional[datetime] = datetime.today() + timedelta(days=1) datetime_date: Optional[datetime] = datetime.today() + timedelta(days=1) - ellipsoid: Optional[Any] = None - barometric_height: Optional[Any] = None - barometric_height_ISA: Optional[Any] = None - pressure: Optional[Any] = None - pressure_ISA: Optional[Any] = None - temperature: Optional[Any] = None - temperature_ISA: Optional[Any] = None - density: Optional[Any] = None - speed_of_sound: Optional[Any] = None - dynamic_viscosity: Optional[Any] = None - gravity: Optional[Any] = None - somigliana_gravity: Optional[Any] = None - wind_speed: Optional[Any] = None - wind_direction: Optional[Any] = None - wind_heading: Optional[Any] = None - wind_velocity_x: Optional[Any] = None - wind_velocity_y: Optional[Any] = None - calculate_earth_radius: Optional[Any] = None - decimal_degrees_to_arc_seconds: Optional[Any] = None - geodesic_to_utm: Optional[Any] = None - utm_to_geodesic: Optional[Any] = None - - model_config = ConfigDict(json_encoders={Any: to_python_primitive}) + ellipsoid: Optional[AnyToPrimitive] = None + barometric_height: Optional[AnyToPrimitive] = None + barometric_height_ISA: Optional[AnyToPrimitive] = None + pressure: Optional[AnyToPrimitive] = None + pressure_ISA: Optional[AnyToPrimitive] = None + temperature: Optional[AnyToPrimitive] = None + temperature_ISA: Optional[AnyToPrimitive] = None + density: Optional[AnyToPrimitive] = None + speed_of_sound: Optional[AnyToPrimitive] = None + dynamic_viscosity: Optional[AnyToPrimitive] = None + gravity: Optional[AnyToPrimitive] = None + somigliana_gravity: Optional[AnyToPrimitive] = None + wind_speed: Optional[AnyToPrimitive] = None + wind_direction: Optional[AnyToPrimitive] = None + wind_heading: Optional[AnyToPrimitive] = None + wind_velocity_x: Optional[AnyToPrimitive] = None + wind_velocity_y: Optional[AnyToPrimitive] = None + calculate_earth_radius: Optional[AnyToPrimitive] = None + decimal_degrees_to_arc_seconds: Optional[AnyToPrimitive] = None + geodesic_to_utm: Optional[AnyToPrimitive] = None + utm_to_geodesic: Optional[AnyToPrimitive] = None class EnvironmentView(EnvironmentModel): diff --git a/lib/views/flight.py b/lib/views/flight.py index 203545c..ddcdf56 100644 --- a/lib/views/flight.py +++ b/lib/views/flight.py @@ -1,17 +1,16 @@ -from typing import Optional, Any -from pydantic import ConfigDict +from typing import Optional from lib.models.flight import FlightModel from lib.views.interface import ApiBaseView -from lib.views.rocket import RocketView, RocketSummary -from lib.views.environment import EnvironmentSummary -from lib.utils import to_python_primitive +from lib.views.rocket import RocketView, RocketSimulation +from lib.views.environment import EnvironmentSimulation +from lib.utils import AnyToPrimitive -class FlightSummary(RocketSummary, EnvironmentSummary): +class FlightSimulation(RocketSimulation, EnvironmentSimulation): name: Optional[str] = None max_time: Optional[int] = None min_time_step: Optional[int] = None - max_time_step: Optional[Any] = None + max_time_step: Optional[AnyToPrimitive] = None equations_of_motion: Optional[str] = None heading: Optional[int] = None inclination: Optional[int] = None @@ -28,131 +27,129 @@ class FlightSummary(RocketSummary, EnvironmentSummary): t_initial: Optional[int] = None terminate_on_apogee: Optional[bool] = None time_overshoot: Optional[bool] = None - latitude: Optional[Any] = None - longitude: Optional[Any] = None - M1: Optional[Any] = None - M2: Optional[Any] = None - M3: Optional[Any] = None - R1: Optional[Any] = None - R2: Optional[Any] = None - R3: Optional[Any] = None - acceleration: Optional[Any] = None - aerodynamic_bending_moment: Optional[Any] = None - aerodynamic_drag: Optional[Any] = None - aerodynamic_lift: Optional[Any] = None - aerodynamic_spin_moment: Optional[Any] = None - alpha1: Optional[Any] = None - alpha2: Optional[Any] = None - alpha3: Optional[Any] = None - altitude: Optional[Any] = None - angle_of_attack: Optional[Any] = None - apogee: Optional[Any] = None - apogee_freestream_speed: Optional[Any] = None - apogee_state: Optional[Any] = None - apogee_time: Optional[Any] = None - apogee_x: Optional[Any] = None - apogee_y: Optional[Any] = None - atol: Optional[Any] = None - attitude_angle: Optional[Any] = None - attitude_frequency_response: Optional[Any] = None - attitude_vector_x: Optional[Any] = None - attitude_vector_y: Optional[Any] = None - attitude_vector_z: Optional[Any] = None - ax: Optional[Any] = None - ay: Optional[Any] = None - az: Optional[Any] = None - bearing: Optional[Any] = None - drag_power: Optional[Any] = None - drift: Optional[Any] = None - dynamic_pressure: Optional[Any] = None - e0: Optional[Any] = None - e1: Optional[Any] = None - e2: Optional[Any] = None - e3: Optional[Any] = None - free_stream_speed: Optional[Any] = None - frontal_surface_wind: Optional[Any] = None - function_evaluations: Optional[Any] = None - function_evaluations_per_time_step: Optional[Any] = None - horizontal_speed: Optional[Any] = None - impact_state: Optional[Any] = None - impact_velocity: Optional[Any] = None - initial_stability_margin: Optional[Any] = None - kinetic_energy: Optional[Any] = None - lateral_attitude_angle: Optional[Any] = None - lateral_surface_wind: Optional[Any] = None - mach_number: Optional[Any] = None - max_acceleration: Optional[Any] = None - max_acceleration_power_off: Optional[Any] = None - max_acceleration_power_off_time: Optional[Any] = None - max_acceleration_power_on: Optional[Any] = None - max_acceleration_power_on_time: Optional[Any] = None - max_acceleration_time: Optional[Any] = None - max_dynamic_pressure: Optional[Any] = None - max_dynamic_pressure_time: Optional[Any] = None - max_mach_number: Optional[Any] = None - max_mach_number_time: Optional[Any] = None - max_rail_button1_normal_force: Optional[Any] = None - max_rail_button1_shear_force: Optional[Any] = None - max_rail_button2_normal_force: Optional[Any] = None - max_rail_button2_shear_force: Optional[Any] = None - max_reynolds_number: Optional[Any] = None - max_reynolds_number_time: Optional[Any] = None - max_speed: Optional[Any] = None - max_speed_time: Optional[Any] = None - max_stability_margin: Optional[Any] = None - max_stability_margin_time: Optional[Any] = None - max_total_pressure: Optional[Any] = None - max_total_pressure_time: Optional[Any] = None - min_stability_margin: Optional[Any] = None - min_stability_margin_time: Optional[Any] = None - omega1_frequency_response: Optional[Any] = None - omega2_frequency_response: Optional[Any] = None - omega3_frequency_response: Optional[Any] = None - out_of_rail_stability_margin: Optional[Any] = None - out_of_rail_state: Optional[Any] = None - out_of_rail_velocity: Optional[Any] = None - parachute_events: Optional[Any] = None - path_angle: Optional[Any] = None - phi: Optional[Any] = None - potential_energy: Optional[Any] = None - psi: Optional[Any] = None - rail_button1_normal_force: Optional[Any] = None - rail_button1_shear_force: Optional[Any] = None - rail_button2_normal_force: Optional[Any] = None - rail_button2_shear_force: Optional[Any] = None - reynolds_number: Optional[Any] = None - rotational_energy: Optional[Any] = None - solution: Optional[Any] = None - solution_array: Optional[Any] = None - speed: Optional[Any] = None - stability_margin: Optional[Any] = None - static_margin: Optional[Any] = None - stream_velocity_x: Optional[Any] = None - stream_velocity_y: Optional[Any] = None - stream_velocity_z: Optional[Any] = None - theta: Optional[Any] = None - thrust_power: Optional[Any] = None - time: Optional[Any] = None - time_steps: Optional[Any] = None - total_energy: Optional[Any] = None - total_pressure: Optional[Any] = None - translational_energy: Optional[Any] = None - vx: Optional[Any] = None - vy: Optional[Any] = None - vz: Optional[Any] = None - w1: Optional[Any] = None - w2: Optional[Any] = None - w3: Optional[Any] = None - x: Optional[Any] = None - x_impact: Optional[Any] = None - y: Optional[Any] = None - y_impact: Optional[Any] = None - y_sol: Optional[Any] = None - z: Optional[Any] = None - z_impact: Optional[Any] = None - flight_phases: Optional[Any] = None - - model_config = ConfigDict(json_encoders={Any: to_python_primitive}) + latitude: Optional[AnyToPrimitive] = None + longitude: Optional[AnyToPrimitive] = None + M1: Optional[AnyToPrimitive] = None + M2: Optional[AnyToPrimitive] = None + M3: Optional[AnyToPrimitive] = None + R1: Optional[AnyToPrimitive] = None + R2: Optional[AnyToPrimitive] = None + R3: Optional[AnyToPrimitive] = None + acceleration: Optional[AnyToPrimitive] = None + aerodynamic_bending_moment: Optional[AnyToPrimitive] = None + aerodynamic_drag: Optional[AnyToPrimitive] = None + aerodynamic_lift: Optional[AnyToPrimitive] = None + aerodynamic_spin_moment: Optional[AnyToPrimitive] = None + alpha1: Optional[AnyToPrimitive] = None + alpha2: Optional[AnyToPrimitive] = None + alpha3: Optional[AnyToPrimitive] = None + altitude: Optional[AnyToPrimitive] = None + angle_of_attack: Optional[AnyToPrimitive] = None + apogee: Optional[AnyToPrimitive] = None + apogee_freestream_speed: Optional[AnyToPrimitive] = None + apogee_state: Optional[AnyToPrimitive] = None + apogee_time: Optional[AnyToPrimitive] = None + apogee_x: Optional[AnyToPrimitive] = None + apogee_y: Optional[AnyToPrimitive] = None + atol: Optional[AnyToPrimitive] = None + attitude_angle: Optional[AnyToPrimitive] = None + attitude_frequency_response: Optional[AnyToPrimitive] = None + attitude_vector_x: Optional[AnyToPrimitive] = None + attitude_vector_y: Optional[AnyToPrimitive] = None + attitude_vector_z: Optional[AnyToPrimitive] = None + ax: Optional[AnyToPrimitive] = None + ay: Optional[AnyToPrimitive] = None + az: Optional[AnyToPrimitive] = None + bearing: Optional[AnyToPrimitive] = None + drag_power: Optional[AnyToPrimitive] = None + drift: Optional[AnyToPrimitive] = None + dynamic_pressure: Optional[AnyToPrimitive] = None + e0: Optional[AnyToPrimitive] = None + e1: Optional[AnyToPrimitive] = None + e2: Optional[AnyToPrimitive] = None + e3: Optional[AnyToPrimitive] = None + free_stream_speed: Optional[AnyToPrimitive] = None + frontal_surface_wind: Optional[AnyToPrimitive] = None + function_evaluations: Optional[AnyToPrimitive] = None + function_evaluations_per_time_step: Optional[AnyToPrimitive] = None + horizontal_speed: Optional[AnyToPrimitive] = None + impact_state: Optional[AnyToPrimitive] = None + impact_velocity: Optional[AnyToPrimitive] = None + initial_stability_margin: Optional[AnyToPrimitive] = None + kinetic_energy: Optional[AnyToPrimitive] = None + lateral_attitude_angle: Optional[AnyToPrimitive] = None + lateral_surface_wind: Optional[AnyToPrimitive] = None + mach_number: Optional[AnyToPrimitive] = None + max_acceleration: Optional[AnyToPrimitive] = None + max_acceleration_power_off: Optional[AnyToPrimitive] = None + max_acceleration_power_off_time: Optional[AnyToPrimitive] = None + max_acceleration_power_on: Optional[AnyToPrimitive] = None + max_acceleration_power_on_time: Optional[AnyToPrimitive] = None + max_acceleration_time: Optional[AnyToPrimitive] = None + max_dynamic_pressure: Optional[AnyToPrimitive] = None + max_dynamic_pressure_time: Optional[AnyToPrimitive] = None + max_mach_number: Optional[AnyToPrimitive] = None + max_mach_number_time: Optional[AnyToPrimitive] = None + max_rail_button1_normal_force: Optional[AnyToPrimitive] = None + max_rail_button1_shear_force: Optional[AnyToPrimitive] = None + max_rail_button2_normal_force: Optional[AnyToPrimitive] = None + max_rail_button2_shear_force: Optional[AnyToPrimitive] = None + max_reynolds_number: Optional[AnyToPrimitive] = None + max_reynolds_number_time: Optional[AnyToPrimitive] = None + max_speed: Optional[AnyToPrimitive] = None + max_speed_time: Optional[AnyToPrimitive] = None + max_stability_margin: Optional[AnyToPrimitive] = None + max_stability_margin_time: Optional[AnyToPrimitive] = None + max_total_pressure: Optional[AnyToPrimitive] = None + max_total_pressure_time: Optional[AnyToPrimitive] = None + min_stability_margin: Optional[AnyToPrimitive] = None + min_stability_margin_time: Optional[AnyToPrimitive] = None + omega1_frequency_response: Optional[AnyToPrimitive] = None + omega2_frequency_response: Optional[AnyToPrimitive] = None + omega3_frequency_response: Optional[AnyToPrimitive] = None + out_of_rail_stability_margin: Optional[AnyToPrimitive] = None + out_of_rail_state: Optional[AnyToPrimitive] = None + out_of_rail_velocity: Optional[AnyToPrimitive] = None + parachute_events: Optional[AnyToPrimitive] = None + path_angle: Optional[AnyToPrimitive] = None + phi: Optional[AnyToPrimitive] = None + potential_energy: Optional[AnyToPrimitive] = None + psi: Optional[AnyToPrimitive] = None + rail_button1_normal_force: Optional[AnyToPrimitive] = None + rail_button1_shear_force: Optional[AnyToPrimitive] = None + rail_button2_normal_force: Optional[AnyToPrimitive] = None + rail_button2_shear_force: Optional[AnyToPrimitive] = None + reynolds_number: Optional[AnyToPrimitive] = None + rotational_energy: Optional[AnyToPrimitive] = None + solution: Optional[AnyToPrimitive] = None + solution_array: Optional[AnyToPrimitive] = None + speed: Optional[AnyToPrimitive] = None + stability_margin: Optional[AnyToPrimitive] = None + static_margin: Optional[AnyToPrimitive] = None + stream_velocity_x: Optional[AnyToPrimitive] = None + stream_velocity_y: Optional[AnyToPrimitive] = None + stream_velocity_z: Optional[AnyToPrimitive] = None + theta: Optional[AnyToPrimitive] = None + thrust_power: Optional[AnyToPrimitive] = None + time: Optional[AnyToPrimitive] = None + time_steps: Optional[AnyToPrimitive] = None + total_energy: Optional[AnyToPrimitive] = None + total_pressure: Optional[AnyToPrimitive] = None + translational_energy: Optional[AnyToPrimitive] = None + vx: Optional[AnyToPrimitive] = None + vy: Optional[AnyToPrimitive] = None + vz: Optional[AnyToPrimitive] = None + w1: Optional[AnyToPrimitive] = None + w2: Optional[AnyToPrimitive] = None + w3: Optional[AnyToPrimitive] = None + x: Optional[AnyToPrimitive] = None + x_impact: Optional[AnyToPrimitive] = None + y: Optional[AnyToPrimitive] = None + y_impact: Optional[AnyToPrimitive] = None + y_sol: Optional[AnyToPrimitive] = None + z: Optional[AnyToPrimitive] = None + z_impact: Optional[AnyToPrimitive] = None + flight_phases: Optional[AnyToPrimitive] = None class FlightView(FlightModel): diff --git a/lib/views/motor.py b/lib/views/motor.py index a8368b8..7ab72bb 100644 --- a/lib/views/motor.py +++ b/lib/views/motor.py @@ -1,11 +1,11 @@ -from typing import List, Any, Optional -from pydantic import BaseModel, ConfigDict +from typing import List, Optional +from pydantic import BaseModel from lib.views.interface import ApiBaseView from lib.models.motor import MotorModel -from lib.utils import to_python_primitive +from lib.utils import AnyToPrimitive -class MotorSummary(BaseModel): +class MotorSimulation(BaseModel): average_thrust: Optional[float] = None burn_duration: Optional[float] = None burn_out_time: Optional[float] = None @@ -39,41 +39,38 @@ class MotorSummary(BaseModel): throat_radius: Optional[float] = None thrust_source: Optional[List[List[float]]] = None total_impulse: Optional[float] = None - Kn: Optional[Any] = None - I_11: Optional[Any] = None - I_12: Optional[Any] = None - I_13: Optional[Any] = None - I_22: Optional[Any] = None - I_23: Optional[Any] = None - I_33: Optional[Any] = None - burn_area: Optional[Any] = None - burn_rate: Optional[Any] = None - burn_time: Optional[Any] = None - center_of_mass: Optional[Any] = None - center_of_propellant_mass: Optional[Any] = None - exhaust_velocity: Optional[Any] = None - grain_height: Optional[Any] = None - grain_volume: Optional[Any] = None - grain_inner_radius: Optional[Any] = None - mass_flow_rate: Optional[Any] = None - propellant_I_11: Optional[Any] = None - propellant_I_12: Optional[Any] = None - propellant_I_13: Optional[Any] = None - propellant_I_22: Optional[Any] = None - propellant_I_23: Optional[Any] = None - propellant_I_33: Optional[Any] = None - propellant_mass: Optional[Any] = None - reshape_thrust_curve: Optional[Any] = None - total_mass: Optional[Any] = None - total_mass_flow_rate: Optional[Any] = None - thrust: Optional[Any] = None - - model_config = ConfigDict(json_encoders={Any: to_python_primitive}) + Kn: Optional[AnyToPrimitive] = None + I_11: Optional[AnyToPrimitive] = None + I_12: Optional[AnyToPrimitive] = None + I_13: Optional[AnyToPrimitive] = None + I_22: Optional[AnyToPrimitive] = None + I_23: Optional[AnyToPrimitive] = None + I_33: Optional[AnyToPrimitive] = None + burn_area: Optional[AnyToPrimitive] = None + burn_rate: Optional[AnyToPrimitive] = None + burn_time: Optional[AnyToPrimitive] = None + center_of_mass: Optional[AnyToPrimitive] = None + center_of_propellant_mass: Optional[AnyToPrimitive] = None + exhaust_velocity: Optional[AnyToPrimitive] = None + grain_height: Optional[AnyToPrimitive] = None + grain_volume: Optional[AnyToPrimitive] = None + grain_inner_radius: Optional[AnyToPrimitive] = None + mass_flow_rate: Optional[AnyToPrimitive] = None + propellant_I_11: Optional[AnyToPrimitive] = None + propellant_I_12: Optional[AnyToPrimitive] = None + propellant_I_13: Optional[AnyToPrimitive] = None + propellant_I_22: Optional[AnyToPrimitive] = None + propellant_I_23: Optional[AnyToPrimitive] = None + propellant_I_33: Optional[AnyToPrimitive] = None + propellant_mass: Optional[AnyToPrimitive] = None + reshape_thrust_curve: Optional[AnyToPrimitive] = None + total_mass: Optional[AnyToPrimitive] = None + total_mass_flow_rate: Optional[AnyToPrimitive] = None + thrust: Optional[AnyToPrimitive] = None class MotorView(MotorModel): motor_id: Optional[str] = None - selected_motor_kind: str class MotorCreated(ApiBaseView): diff --git a/lib/views/rocket.py b/lib/views/rocket.py index 97482bd..4d9944c 100644 --- a/lib/views/rocket.py +++ b/lib/views/rocket.py @@ -1,12 +1,11 @@ -from typing import Any, Optional -from pydantic import ConfigDict +from typing import Optional from lib.models.rocket import RocketModel from lib.views.interface import ApiBaseView -from lib.views.motor import MotorView, MotorSummary -from lib.utils import to_python_primitive +from lib.views.motor import MotorView, MotorSimulation +from lib.utils import AnyToPrimitive -class RocketSummary(MotorSummary): +class RocketSimulation(MotorSimulation): area: Optional[float] = None coordinate_system_orientation: str = 'tail_to_nose' center_of_mass_without_motor: Optional[float] = None @@ -18,26 +17,24 @@ class RocketSummary(MotorSummary): cp_eccentricity_y: Optional[float] = None thrust_eccentricity_x: Optional[float] = None thrust_eccentricity_y: Optional[float] = None - I_11_without_motor: Optional[Any] = None - I_12_without_motor: Optional[Any] = None - I_13_without_motor: Optional[Any] = None - I_22_without_motor: Optional[Any] = None - I_23_without_motor: Optional[Any] = None - I_33_without_motor: Optional[Any] = None - check_parachute_trigger: Optional[Any] = None - com_to_cdm_function: Optional[Any] = None - cp_position: Optional[Any] = None - motor_center_of_mass_position: Optional[Any] = None - nozzle_gyration_tensor: Optional[Any] = None - power_off_drag: Optional[Any] = None - power_on_drag: Optional[Any] = None - reduced_mass: Optional[Any] = None - stability_margin: Optional[Any] = None - static_margin: Optional[Any] = None - thrust_to_weight: Optional[Any] = None - total_lift_coeff_der: Optional[Any] = None - - model_config = ConfigDict(json_encoders={Any: to_python_primitive}) + I_11_without_motor: Optional[AnyToPrimitive] = None + I_12_without_motor: Optional[AnyToPrimitive] = None + I_13_without_motor: Optional[AnyToPrimitive] = None + I_22_without_motor: Optional[AnyToPrimitive] = None + I_23_without_motor: Optional[AnyToPrimitive] = None + I_33_without_motor: Optional[AnyToPrimitive] = None + check_parachute_trigger: Optional[AnyToPrimitive] = None + com_to_cdm_function: Optional[AnyToPrimitive] = None + cp_position: Optional[AnyToPrimitive] = None + motor_center_of_mass_position: Optional[AnyToPrimitive] = None + nozzle_gyration_tensor: Optional[AnyToPrimitive] = None + power_off_drag: Optional[AnyToPrimitive] = None + power_on_drag: Optional[AnyToPrimitive] = None + reduced_mass: Optional[AnyToPrimitive] = None + stability_margin: Optional[AnyToPrimitive] = None + static_margin: Optional[AnyToPrimitive] = None + thrust_to_weight: Optional[AnyToPrimitive] = None + total_lift_coeff_der: Optional[AnyToPrimitive] = None class RocketView(RocketModel): diff --git a/pyproject.toml b/pyproject.toml index 3a6271a..45b11af 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,6 +23,11 @@ Documentation = "http://api.rocketpy.org/docs" Repository = "https://github.com/RocketPy-Team/infinity-api" "Bug Tracker" = "https://github.com/RocketPy-Team/Infinity-API/issues" +[tool.pytest.ini_options] +pythonpath = ["."] +testpaths = ["tests"] +addopts = "--import-mode=importlib" + [tool.black] line-length = 79 include = '\.py$' diff --git a/requirements-dev.txt b/requirements-dev.txt index 811bdd1..a20dfb7 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -4,3 +4,4 @@ pylint ruff pytest httpx +black diff --git a/tests/test_controllers/test_controller_interface.py b/tests/test_controllers/test_controller_interface.py index 4672e54..9b190bf 100644 --- a/tests/test_controllers/test_controller_interface.py +++ b/tests/test_controllers/test_controller_interface.py @@ -27,7 +27,9 @@ def stub_controller(stub_model): @pytest.mark.asyncio async def test_controller_exception_handler_no_exception(stub_model): - async def method(self, model, *args, **kwargs): # pylint: disable=unused-argument + async def method( + self, model, *args, **kwargs + ): # pylint: disable=unused-argument return stub_model, args, kwargs test_controller = Mock(method=method) diff --git a/tests/test_routes/test_environments_route.py b/tests/test_routes/test_environments_route.py index c1f3fe3..3995168 100644 --- a/tests/test_routes/test_environments_route.py +++ b/tests/test_routes/test_environments_route.py @@ -10,7 +10,7 @@ EnvironmentUpdated, EnvironmentRetrieved, EnvironmentDeleted, - EnvironmentSummary, + EnvironmentSimulation, ) from lib import app @@ -18,10 +18,10 @@ @pytest.fixture -def stub_environment_summary_dump(): - env_summary = EnvironmentSummary() - env_summary_json = env_summary.model_dump_json() - return json.loads(env_summary_json) +def stub_environment_simulation_dump(): + env_simulation = EnvironmentSimulation() + env_simulation_json = env_simulation.model_dump_json() + return json.loads(env_simulation_json) @pytest.fixture(autouse=True) @@ -34,33 +34,43 @@ def mock_controller_instance(): mock_controller_instance.get_environment_by_id = Mock() mock_controller_instance.put_environment_by_id = Mock() mock_controller_instance.delete_environment_by_id = Mock() + mock_controller_instance.get_environment_simulation = Mock() + mock_controller_instance.get_rocketpy_environment_binary = Mock() yield mock_controller_instance def test_create_environment(stub_environment_dump, mock_controller_instance): - mock_response = AsyncMock(return_value=EnvironmentCreated(environment_id='123')) + mock_response = AsyncMock( + return_value=EnvironmentCreated(environment_id='123') + ) mock_controller_instance.post_environment = mock_response response = client.post('/environments/', json=stub_environment_dump) assert response.status_code == 200 assert response.json() == { 'environment_id': '123', - 'message': 'environment successfully created', + 'message': 'Environment successfully created', } mock_controller_instance.post_environment.assert_called_once_with( EnvironmentModel(**stub_environment_dump) ) -def test_create_environment_optional_params(stub_environment_dump, mock_controller_instance): - stub_environment_dump.update({ - 'latitude': 0, - 'longitude': 0, - 'elevation': 1, - 'atmospheric_model_type': 'STANDARD_ATMOSPHERE', - 'atmospheric_model_file': None, - 'date': '2021-01-01T00:00:00', - }) - mock_response = AsyncMock(return_value=EnvironmentCreated(environment_id='123')) +def test_create_environment_optional_params( + stub_environment_dump, mock_controller_instance +): + stub_environment_dump.update( + { + 'latitude': 0, + 'longitude': 0, + 'elevation': 1, + 'atmospheric_model_type': 'standard_atmosphere', + 'atmospheric_model_file': None, + 'date': '2021-01-01T00:00:00', + } + ) + mock_response = AsyncMock( + return_value=EnvironmentCreated(environment_id='123') + ) mock_controller_instance.post_environment = mock_response response = client.post('/environments/', json=stub_environment_dump) assert response.status_code == 200 @@ -88,18 +98,22 @@ def test_create_environment_server_error( def test_read_environment(stub_environment_dump, mock_controller_instance): - stub_environment_out = EnvironmentView(environment_id='123', **stub_environment_dump) + environment_view = EnvironmentView( + environment_id='123', **stub_environment_dump + ) mock_response = AsyncMock( - return_value=EnvironmentRetrieved(environment=stub_environment_out) + return_value=EnvironmentRetrieved(environment=environment_view) ) mock_controller_instance.get_environment_by_id = mock_response response = client.get('/environments/123') assert response.status_code == 200 assert response.json() == { 'message': 'Environment successfully retrieved', - 'environment': json.loads(stub_environment_out.model_dump_json()), + 'environment': json.loads(environment_view.model_dump_json()), } - mock_controller_instance.get_environment_by_id.assert_called_once_with('123') + mock_controller_instance.get_environment_by_id.assert_called_once_with( + '123' + ) def test_read_environment_not_found(mock_controller_instance): @@ -108,7 +122,9 @@ def test_read_environment_not_found(mock_controller_instance): response = client.get('/environments/123') assert response.status_code == 404 assert response.json() == {'detail': 'Not Found'} - mock_controller_instance.get_environment_by_id.assert_called_once_with('123') + mock_controller_instance.get_environment_by_id.assert_called_once_with( + '123' + ) def test_read_environment_server_error(mock_controller_instance): @@ -120,7 +136,9 @@ def test_read_environment_server_error(mock_controller_instance): def test_update_environment(stub_environment_dump, mock_controller_instance): - mock_reponse = AsyncMock(return_value=EnvironmentUpdated(environment_id='123')) + mock_reponse = AsyncMock( + return_value=EnvironmentUpdated(environment_id='123') + ) mock_controller_instance.put_environment_by_id = mock_reponse response = client.put('/environments/123', json=stub_environment_dump) assert response.status_code == 200 @@ -139,7 +157,9 @@ def test_update_environment_invalid_input(): assert response.status_code == 422 -def test_update_environment_not_found(stub_environment_dump, mock_controller_instance): +def test_update_environment_not_found( + stub_environment_dump, mock_controller_instance +): mock_reponse = AsyncMock(side_effect=HTTPException(status_code=404)) mock_controller_instance.put_environment_by_id = mock_reponse response = client.put('/environments/123', json=stub_environment_dump) @@ -161,14 +181,18 @@ def test_update_environment_server_error( def test_delete_environment(mock_controller_instance): - mock_reponse = AsyncMock(return_value=EnvironmentDeleted(environment_id='123')) + mock_reponse = AsyncMock( + return_value=EnvironmentDeleted(environment_id='123') + ) mock_controller_instance.delete_environment_by_id = mock_reponse response = client.delete('/environments/123') assert response.status_code == 200 assert response.json() == { 'message': 'Environment successfully deleted', } - mock_controller_instance.delete_environment_by_id.assert_called_once_with('123') + mock_controller_instance.delete_environment_by_id.assert_called_once_with( + '123' + ) def test_delete_environment_server_error(mock_controller_instance): @@ -179,30 +203,36 @@ def test_delete_environment_server_error(mock_controller_instance): assert response.json() == {'detail': 'Internal Server Error'} -def test_simulate_environment_success( - stub_environment_summary_dump, mock_controller_instance +def test_get_environment_simulation_success( + stub_environment_simulation_dump, mock_controller_instance ): - mock_reponse = AsyncMock(return_value=EnvironmentSummary(**stub_environment_summary_dump)) + mock_reponse = AsyncMock( + return_value=EnvironmentSimulation(**stub_environment_simulation_dump) + ) mock_controller_instance.get_environment_simulation = mock_reponse - response = client.get('/environments/123/summary') + response = client.get('/environments/123/simulate') assert response.status_code == 200 - assert response.json() == stub_environment_summary_dump - mock_controller_instance.get_environment_simulation.assert_called_once_with('123') + assert response.json() == stub_environment_simulation_dump + mock_controller_instance.get_environment_simulation.assert_called_once_with( + '123' + ) -def test_simulate_environment_not_found(mock_controller_instance): +def test_get_environment_simulation_not_found(mock_controller_instance): mock_response = AsyncMock(side_effect=HTTPException(status_code=404)) mock_controller_instance.get_environment_simulation = mock_response - response = client.get('/environments/123/summary') + response = client.get('/environments/123/simulate') assert response.status_code == 404 assert response.json() == {'detail': 'Not Found'} - mock_controller_instance.get_environment_simulation.assert_called_once_with('123') + mock_controller_instance.get_environment_simulation.assert_called_once_with( + '123' + ) -def test_simulate_environment_server_error(mock_controller_instance): +def test_get_environment_simulation_server_error(mock_controller_instance): mock_response = AsyncMock(side_effect=HTTPException(status_code=500)) mock_controller_instance.get_environment_simulation = mock_response - response = client.get('/environments/123/summary') + response = client.get('/environments/123/simulate') assert response.status_code == 500 assert response.json() == {'detail': 'Internal Server Error'} @@ -230,7 +260,9 @@ def test_read_rocketpy_environment_binary_not_found(mock_controller_instance): ) -def test_read_rocketpy_environment_binary_server_error(mock_controller_instance): +def test_read_rocketpy_environment_binary_server_error( + mock_controller_instance, +): mock_response = AsyncMock(side_effect=HTTPException(status_code=500)) mock_controller_instance.get_rocketpy_environment_binary = mock_response response = client.get('/environments/123/rocketpy') diff --git a/tests/test_routes/test_flights_route.py b/tests/test_routes/test_flights_route.py index 5ff58bc..bc112ca 100644 --- a/tests/test_routes/test_flights_route.py +++ b/tests/test_routes/test_flights_route.py @@ -14,7 +14,7 @@ FlightUpdated, FlightRetrieved, FlightDeleted, - FlightSummary, + FlightSimulation, FlightView, ) from lib import app @@ -37,10 +37,10 @@ def stub_flight_dump(stub_environment_dump, stub_rocket_dump): @pytest.fixture -def stub_flight_summary_dump(): - flight_summary = FlightSummary() - flight_summary_json = flight_summary.model_dump_json() - return json.loads(flight_summary_json) +def stub_flight_simulate_dump(): + flight_simulate = FlightSimulation() + flight_simulate_json = flight_simulate.model_dump_json() + return json.loads(flight_simulate_json) @pytest.fixture(autouse=True) @@ -50,9 +50,13 @@ def mock_controller_instance(): ) as mock_controller: mock_controller_instance = mock_controller.return_value mock_controller_instance.post_flight = Mock() - mock_controller_instance.get_flight = Mock() - mock_controller_instance.put_flight = Mock() + mock_controller_instance.get_flight_by_id = Mock() + mock_controller_instance.put_flight_by_id = Mock() mock_controller_instance.delete_flight_by_id = Mock() + mock_controller_instance.get_flight_simulation = Mock() + mock_controller_instance.get_rocketpy_flight_binary = Mock() + mock_controller_instance.update_environment_by_flight_id = Mock() + mock_controller_instance.update_rocket_by_flight_id = Mock() yield mock_controller_instance @@ -60,20 +64,25 @@ def test_create_flight(stub_flight_dump, mock_controller_instance): mock_response = AsyncMock(return_value=FlightCreated(flight_id='123')) mock_controller_instance.post_flight = mock_response with patch.object( - MotorModel, 'set_motor_kind', side_effect=None + MotorModel, 'set_motor_kind', side_effect=None ) as mock_set_motor_kind: - response = client.post('/flights/', json=stub_flight_dump, params={'motor_kind': 'HYBRID'}) + response = client.post( + '/flights/', json=stub_flight_dump, params={'motor_kind': 'HYBRID'} + ) assert response.status_code == 200 assert response.json() == { 'flight_id': '123', - 'message': 'flight successfully created', + 'message': 'Flight successfully created', } mock_set_motor_kind.assert_called_once_with(MotorKinds.HYBRID) mock_controller_instance.post_flight.assert_called_once_with( - FlightModel(**stub_flight_dump)) + FlightModel(**stub_flight_dump) + ) -def test_create_flight_optional_params(stub_flight_dump, mock_controller_instance): +def test_create_flight_optional_params( + stub_flight_dump, mock_controller_instance +): stub_flight_dump.update( { 'inclination': 0, @@ -89,17 +98,20 @@ def test_create_flight_optional_params(stub_flight_dump, mock_controller_instanc mock_response = AsyncMock(return_value=FlightCreated(flight_id='123')) mock_controller_instance.post_flight = mock_response with patch.object( - MotorModel, 'set_motor_kind', side_effect=None + MotorModel, 'set_motor_kind', side_effect=None ) as mock_set_motor_kind: - response = client.post('/flights/', json=stub_flight_dump, params={'motor_kind': 'HYBRID'}) + response = client.post( + '/flights/', json=stub_flight_dump, params={'motor_kind': 'HYBRID'} + ) assert response.status_code == 200 assert response.json() == { 'flight_id': '123', - 'message': 'flight successfully created', + 'message': 'Flight successfully created', } mock_set_motor_kind.assert_called_once_with(MotorKinds.HYBRID) mock_controller_instance.post_flight.assert_called_once_with( - FlightModel(**stub_flight_dump)) + FlightModel(**stub_flight_dump) + ) def test_create_flight_invalid_input(): @@ -109,34 +121,49 @@ def test_create_flight_invalid_input(): assert response.status_code == 422 -def test_create_flight_server_error(stub_flight_dump, mock_controller_instance): +def test_create_flight_server_error( + stub_flight_dump, mock_controller_instance +): mock_response = AsyncMock( side_effect=HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR ) ) mock_controller_instance.post_flight = mock_response - response = client.post('/flights/', json=stub_flight_dump, params={'motor_kind': 'HYBRID'}) + response = client.post( + '/flights/', json=stub_flight_dump, params={'motor_kind': 'HYBRID'} + ) assert response.status_code == 500 assert response.json() == {'detail': 'Internal Server Error'} -def test_read_flight(stub_flight_dump, stub_rocket_dump, stub_motor_dump, mock_controller_instance): +def test_read_flight( + stub_flight_dump, + stub_rocket_dump, + stub_motor_dump, + mock_controller_instance, +): del stub_rocket_dump['motor'] del stub_flight_dump['rocket'] - motor_view = MotorView(**stub_motor_dump, selected_motor_kind=MotorKinds.HYBRID.value) + stub_motor_dump.update({'selected_motor_kind': 'HYBRID'}) + motor_view = MotorView(**stub_motor_dump) rocket_view = RocketView(**stub_rocket_dump, motor=motor_view) - flight_view = FlightView(**stub_flight_dump, rocket=rocket_view) + flight_view = FlightView( + **stub_flight_dump, flight_id='123', rocket=rocket_view + ) mock_response = AsyncMock(return_value=FlightRetrieved(flight=flight_view)) - mock_controller_instance.get_flight = mock_response + mock_controller_instance.get_flight_by_id = mock_response response = client.get('/flights/123') assert response.status_code == 200 - assert response.json() == json.loads(flight_view.model_dump_json()) - mock_controller_instance.get_flight.assert_called_once_with('123') + assert response.json() == { + 'message': 'Flight successfully retrieved', + 'flight': json.loads(flight_view.model_dump_json()), + } + mock_controller_instance.get_flight_by_id.assert_called_once_with('123') def test_read_flight_not_found(mock_controller_instance): - mock_controller_instance.get_flight.side_effect = HTTPException( + mock_controller_instance.get_flight_by_id.side_effect = HTTPException( status_code=status.HTTP_404_NOT_FOUND ) response = client.get('/flights/123') @@ -145,16 +172,17 @@ def test_read_flight_not_found(mock_controller_instance): def test_read_flight_server_error(mock_controller_instance): - mock_controller_instance.get_flight.side_effect = HTTPException( + mock_controller_instance.get_flight_by_id.side_effect = HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR ) response = client.get('/flights/123') assert response.status_code == 500 assert response.json() == {'detail': 'Internal Server Error'} + def test_update_flight_by_id(stub_flight_dump, mock_controller_instance): mock_response = AsyncMock(return_value=FlightUpdated(flight_id='123')) - mock_controller_instance.put_flight = mock_response + mock_controller_instance.put_flight_by_id = mock_response with patch.object( MotorModel, 'set_motor_kind', side_effect=None ) as mock_set_motor_kind: @@ -165,29 +193,34 @@ def test_update_flight_by_id(stub_flight_dump, mock_controller_instance): ) assert response.status_code == 200 assert response.json() == { - 'flight_id': '123', 'message': 'Flight successfully updated', } mock_set_motor_kind.assert_called_once_with(MotorKinds.HYBRID) - mock_controller_instance.put_flight.assert_called_once_with( + mock_controller_instance.put_flight_by_id.assert_called_once_with( '123', FlightModel(**stub_flight_dump) ) -def test_update_env_by_flight_id(stub_environment_dump, mock_controller_instance): + +def test_update_environment_by_flight_id( + stub_environment_dump, mock_controller_instance +): mock_response = AsyncMock(return_value=FlightUpdated(flight_id='123')) - mock_controller_instance.update_env_by_flight_id = mock_response - response = client.put('/flights/123/env', json=stub_environment_dump) + mock_controller_instance.update_environment_by_flight_id = mock_response + response = client.put( + '/flights/123/environment', json=stub_environment_dump + ) assert response.status_code == 200 assert response.json() == { - 'flight_id': '123', 'message': 'Flight successfully updated', } - mock_controller_instance.update_env_by_flight_id.assert_called_once_with( - '123', env=EnvironmentModel(**stub_environment_dump) + mock_controller_instance.update_environment_by_flight_id.assert_called_once_with( + '123', environment=EnvironmentModel(**stub_environment_dump) ) -def test_update_rocket_by_flight_id(stub_rocket_dump, mock_controller_instance): +def test_update_rocket_by_flight_id( + stub_rocket_dump, mock_controller_instance +): mock_response = AsyncMock(return_value=FlightUpdated(flight_id='123')) mock_controller_instance.update_rocket_by_flight_id = mock_response with patch.object( @@ -200,16 +233,15 @@ def test_update_rocket_by_flight_id(stub_rocket_dump, mock_controller_instance): ) assert response.status_code == 200 assert response.json() == { - 'flight_id': '123', 'message': 'Flight successfully updated', } mock_set_motor_kind.assert_called_once_with(MotorKinds.HYBRID) mock_controller_instance.update_rocket_by_flight_id.assert_called_once_with( - '123', RocketModel(**stub_rocket_dump) + '123', rocket=RocketModel(**stub_rocket_dump) ) -def test_update_env_by_flight_id_invalid_input(): +def test_update_environment_by_flight_id_invalid_input(): response = client.put('/flights/123', json={'environment': 'foo'}) assert response.status_code == 422 @@ -229,7 +261,7 @@ def test_update_flight_invalid_input(): def test_update_flight_not_found(stub_flight_dump, mock_controller_instance): - mock_controller_instance.put_flight.side_effect = HTTPException( + mock_controller_instance.put_flight_by_id.side_effect = HTTPException( status_code=status.HTTP_404_NOT_FOUND ) response = client.put( @@ -241,8 +273,10 @@ def test_update_flight_not_found(stub_flight_dump, mock_controller_instance): assert response.json() == {'detail': 'Not Found'} -def test_update_flight_server_error(stub_flight_dump, mock_controller_instance): - mock_controller_instance.put_flight.side_effect = HTTPException( +def test_update_flight_server_error( + stub_flight_dump, mock_controller_instance +): + mock_controller_instance.put_flight_by_id.side_effect = HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR ) response = client.put( @@ -254,27 +288,37 @@ def test_update_flight_server_error(stub_flight_dump, mock_controller_instance): assert response.json() == {'detail': 'Internal Server Error'} -def test_update_env_by_flight_id_not_found(stub_environment_dump, mock_controller_instance): - mock_controller_instance.update_env_by_flight_id.side_effect = HTTPException( - status_code=status.HTTP_404_NOT_FOUND +def test_update_environment_by_flight_id_not_found( + stub_environment_dump, mock_controller_instance +): + mock_controller_instance.update_environment_by_flight_id.side_effect = ( + HTTPException(status_code=status.HTTP_404_NOT_FOUND) + ) + response = client.put( + '/flights/123/environment', json=stub_environment_dump ) - response = client.put('/flights/123/env', json=stub_environment_dump) assert response.status_code == 404 assert response.json() == {'detail': 'Not Found'} -def test_update_env_by_flight_id_server_error(stub_environment_dump, mock_controller_instance): - mock_controller_instance.update_env_by_flight_id.side_effect = HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR +def test_update_environment_by_flight_id_server_error( + stub_environment_dump, mock_controller_instance +): + mock_controller_instance.update_environment_by_flight_id.side_effect = ( + HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR) + ) + response = client.put( + '/flights/123/environment', json=stub_environment_dump ) - response = client.put('/flights/123/env', json=stub_environment_dump) assert response.status_code == 500 assert response.json() == {'detail': 'Internal Server Error'} -def test_update_rocket_by_flight_id_not_found(stub_rocket_dump, mock_controller_instance): - mock_controller_instance.update_rocket_by_flight_id.side_effect = HTTPException( - status_code=status.HTTP_404_NOT_FOUND +def test_update_rocket_by_flight_id_not_found( + stub_rocket_dump, mock_controller_instance +): + mock_controller_instance.update_rocket_by_flight_id.side_effect = ( + HTTPException(status_code=status.HTTP_404_NOT_FOUND) ) response = client.put( '/flights/123/rocket', @@ -285,9 +329,11 @@ def test_update_rocket_by_flight_id_not_found(stub_rocket_dump, mock_controller_ assert response.json() == {'detail': 'Not Found'} -def test_update_rocket_by_flight_id_server_error(stub_rocket_dump, mock_controller_instance): - mock_controller_instance.update_rocket_by_flight_id.side_effect = HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR +def test_update_rocket_by_flight_id_server_error( + stub_rocket_dump, mock_controller_instance +): + mock_controller_instance.update_rocket_by_flight_id.side_effect = ( + HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR) ) response = client.put( '/flights/123/rocket', @@ -304,7 +350,6 @@ def test_delete_flight(mock_controller_instance): response = client.delete('/flights/123') assert response.status_code == 200 assert response.json() == { - 'flight_id': '123', 'message': 'Flight successfully deleted', } mock_controller_instance.delete_flight_by_id.assert_called_once_with('123') @@ -319,45 +364,55 @@ def test_delete_flight_server_error(mock_controller_instance): assert response.json() == {'detail': 'Internal Server Error'} -def test_simulate_flight(stub_flight_summary_dump, mock_controller_instance): - mock_response = AsyncMock(return_value=FlightSummary(**stub_flight_summary_dump)) - mock_controller_instance.simulate_flight = mock_response - response = client.get('/flights/123/summary') +def test_get_flight_simulation( + stub_flight_simulate_dump, mock_controller_instance +): + mock_response = AsyncMock( + return_value=FlightSimulation(**stub_flight_simulate_dump) + ) + mock_controller_instance.get_flight_simulation = mock_response + response = client.get('/flights/123/simulate') assert response.status_code == 200 - assert response.json() == stub_flight_summary_dump - mock_controller_instance.simulate_flight.assert_called_once_with('123') + assert response.json() == stub_flight_simulate_dump + mock_controller_instance.get_flight_simulation.assert_called_once_with( + '123' + ) -def test_simulate_flight_not_found(mock_controller_instance): - mock_controller_instance.simulate_flight.side_effect = HTTPException( +def test_get_flight_simulation_not_found(mock_controller_instance): + mock_controller_instance.get_flight_simulation.side_effect = HTTPException( status_code=status.HTTP_404_NOT_FOUND ) - response = client.get('/flights/123/summary') + response = client.get('/flights/123/simulate') assert response.status_code == 404 assert response.json() == {'detail': 'Not Found'} -def test_simulate_flight_server_error(mock_controller_instance): - mock_controller_instance.simulate_flight.side_effect = HTTPException( +def test_get_flight_simulation_server_error(mock_controller_instance): + mock_controller_instance.get_flight_simulation.side_effect = HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR ) - response = client.get('/flights/123/summary') + response = client.get('/flights/123/simulate') assert response.status_code == 500 assert response.json() == {'detail': 'Internal Server Error'} def test_read_rocketpy_flight_binary(mock_controller_instance): - mock_controller_instance.get_rocketpy_flight_binary = AsyncMock(return_value=b'rocketpy') + mock_controller_instance.get_rocketpy_flight_binary = AsyncMock( + return_value=b'rocketpy' + ) response = client.get('/flights/123/rocketpy') assert response.status_code == 203 assert response.content == b'rocketpy' assert response.headers['content-type'] == 'application/octet-stream' - mock_controller_instance.get_rocketpy_flight_binary.assert_called_once_with('123') + mock_controller_instance.get_rocketpy_flight_binary.assert_called_once_with( + '123' + ) def test_read_rocketpy_flight_binary_not_found(mock_controller_instance): - mock_controller_instance.get_rocketpy_flight_binary.side_effect = HTTPException( - status_code=status.HTTP_404_NOT_FOUND + mock_controller_instance.get_rocketpy_flight_binary.side_effect = ( + HTTPException(status_code=status.HTTP_404_NOT_FOUND) ) response = client.get('/flights/123/rocketpy') assert response.status_code == 404 @@ -365,8 +420,8 @@ def test_read_rocketpy_flight_binary_not_found(mock_controller_instance): def test_read_rocketpy_flight_binary_server_error(mock_controller_instance): - mock_controller_instance.get_rocketpy_flight_binary.side_effect = HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR + mock_controller_instance.get_rocketpy_flight_binary.side_effect = ( + HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR) ) response = client.get('/flights/123/rocketpy') assert response.status_code == 500 diff --git a/tests/test_routes/test_motors_route.py b/tests/test_routes/test_motors_route.py index 1b22bfd..ee4abe2 100644 --- a/tests/test_routes/test_motors_route.py +++ b/tests/test_routes/test_motors_route.py @@ -12,7 +12,7 @@ MotorRetrieved, MotorUpdated, MotorDeleted, - MotorSummary, + MotorSimulation, MotorView, ) from lib import app @@ -21,10 +21,10 @@ @pytest.fixture -def stub_motor_dump_summary(): - motor_summary = MotorSummary() - motor_summary_json = motor_summary.model_dump_json() - return json.loads(motor_summary_json) +def stub_motor_dump_simulation(): + motor_simulation = MotorSimulation() + motor_simulation_json = motor_simulation.model_dump_json() + return json.loads(motor_simulation_json) @pytest.fixture(autouse=True) @@ -37,6 +37,8 @@ def mock_controller_instance(): mock_controller_instance.get_motor_by_id = Mock() mock_controller_instance.put_motor_by_id = Mock() mock_controller_instance.delete_motor_by_id = Mock() + mock_controller_instance.get_motor_simulation = Mock() + mock_controller_instance.get_rocketpy_motor_binary = Mock() yield mock_controller_instance @@ -44,25 +46,32 @@ def test_create_motor(stub_motor_dump, mock_controller_instance): mock_response = AsyncMock(return_value=MotorCreated(motor_id='123')) mock_controller_instance.post_motor = mock_response with patch.object( - MotorModel, 'set_motor_kind', side_effect=None + MotorModel, 'set_motor_kind', side_effect=None ) as mock_set_motor_kind: - response = client.post('/motors/', json=stub_motor_dump, params={'motor_kind': 'HYBRID'}) + response = client.post( + '/motors/', json=stub_motor_dump, params={'motor_kind': 'HYBRID'} + ) assert response.status_code == 200 assert response.json() == { 'motor_id': '123', - 'message': 'motor successfully created', + 'message': 'Motor successfully created', } mock_set_motor_kind.assert_called_once_with(MotorKinds.HYBRID) mock_controller_instance.post_motor.assert_called_once_with( - MotorModel(**stub_motor_dump)) + MotorModel(**stub_motor_dump) + ) -def test_create_motor_optional_params(stub_motor_dump, mock_controller_instance): - stub_motor_dump.update({ - 'interpolation_method': 'linear', - 'coordinate_system_orientation': 'nozzle_to_combustion_chamber', - 'reshape_thrust_curve': False, - }) +def test_create_motor_optional_params( + stub_motor_dump, mock_controller_instance +): + stub_motor_dump.update( + { + 'interpolation_method': 'linear', + 'coordinate_system_orientation': 'nozzle_to_combustion_chamber', + 'reshape_thrust_curve': False, + } + ) mock_response = AsyncMock(return_value=MotorCreated(motor_id='123')) mock_controller_instance.post_motor = mock_response with patch.object( @@ -74,10 +83,12 @@ def test_create_motor_optional_params(stub_motor_dump, mock_controller_instance) assert response.status_code == 200 assert response.json() == { 'motor_id': '123', - 'message': 'motor successfully created', + 'message': 'Motor successfully created', } mock_set_motor_kind.assert_called_once_with(MotorKinds.HYBRID) - mock_controller_instance.post_motor.assert_called_once_with(MotorModel(**stub_motor_dump)) + mock_controller_instance.post_motor.assert_called_once_with( + MotorModel(**stub_motor_dump) + ) def test_create_generic_motor(stub_motor_dump, mock_controller_instance): @@ -101,14 +112,18 @@ def test_create_generic_motor(stub_motor_dump, mock_controller_instance): assert response.status_code == 200 assert response.json() == { 'motor_id': '123', - 'message': 'motor successfully created', + 'message': 'Motor successfully created', } mock_set_motor_kind.assert_called_once_with(MotorKinds.GENERIC) - mock_controller_instance.post_motor.assert_called_once_with(MotorModel(**stub_motor_dump)) + mock_controller_instance.post_motor.assert_called_once_with( + MotorModel(**stub_motor_dump) + ) -def test_create_liquid_motor_level_tank(stub_motor_dump, stub_level_tank, mock_controller_instance): - stub_motor_dump.update({'tanks': [stub_level_tank]}) +def test_create_liquid_motor_level_tank( + stub_motor_dump, stub_level_tank_dump, mock_controller_instance +): + stub_motor_dump.update({'tanks': [stub_level_tank_dump]}) mock_response = AsyncMock(return_value=MotorCreated(motor_id='123')) mock_controller_instance.post_motor = mock_response with patch.object( @@ -120,14 +135,18 @@ def test_create_liquid_motor_level_tank(stub_motor_dump, stub_level_tank, mock_c assert response.status_code == 200 assert response.json() == { 'motor_id': '123', - 'message': 'motor successfully created', + 'message': 'Motor successfully created', } mock_set_motor_kind.assert_called_once_with(MotorKinds.LIQUID) - mock_controller_instance.post_motor.assert_called_once_with(MotorModel(**stub_motor_dump)) + mock_controller_instance.post_motor.assert_called_once_with( + MotorModel(**stub_motor_dump) + ) -def test_create_liquid_motor_mass_flow_tank(stub_motor_dump, stub_mass_flow_tank, mock_controller_instance): - stub_motor_dump.update({'tanks': [stub_mass_flow_tank]}) +def test_create_liquid_motor_mass_flow_tank( + stub_motor_dump, stub_mass_flow_tank_dump, mock_controller_instance +): + stub_motor_dump.update({'tanks': [stub_mass_flow_tank_dump]}) mock_response = AsyncMock(return_value=MotorCreated(motor_id='123')) mock_controller_instance.post_motor = mock_response with patch.object( @@ -139,14 +158,18 @@ def test_create_liquid_motor_mass_flow_tank(stub_motor_dump, stub_mass_flow_tank assert response.status_code == 200 assert response.json() == { 'motor_id': '123', - 'message': 'motor successfully created', + 'message': 'Motor successfully created', } mock_set_motor_kind.assert_called_once_with(MotorKinds.LIQUID) - mock_controller_instance.post_motor.assert_called_once_with(MotorModel(**stub_motor_dump)) + mock_controller_instance.post_motor.assert_called_once_with( + MotorModel(**stub_motor_dump) + ) -def test_create_liquid_motor_ullage_tank(stub_motor_dump, stub_ullage_tank, mock_controller_instance): - stub_motor_dump.update({'tanks': [stub_ullage_tank]}) +def test_create_liquid_motor_ullage_tank( + stub_motor_dump, stub_ullage_tank_dump, mock_controller_instance +): + stub_motor_dump.update({'tanks': [stub_ullage_tank_dump]}) mock_response = AsyncMock(return_value=MotorCreated(motor_id='123')) mock_controller_instance.post_motor = mock_response with patch.object( @@ -158,14 +181,18 @@ def test_create_liquid_motor_ullage_tank(stub_motor_dump, stub_ullage_tank, mock assert response.status_code == 200 assert response.json() == { 'motor_id': '123', - 'message': 'motor successfully created', + 'message': 'Motor successfully created', } mock_set_motor_kind.assert_called_once_with(MotorKinds.LIQUID) - mock_controller_instance.post_motor.assert_called_once_with(MotorModel(**stub_motor_dump)) + mock_controller_instance.post_motor.assert_called_once_with( + MotorModel(**stub_motor_dump) + ) -def test_create_liquid_motor_mass_tank(stub_motor_dump, stub_mass_tank, mock_controller_instance): - stub_motor_dump.update({'tanks': [stub_mass_tank]}) +def test_create_liquid_motor_mass_tank( + stub_motor_dump, stub_mass_tank_dump, mock_controller_instance +): + stub_motor_dump.update({'tanks': [stub_mass_tank_dump]}) mock_response = AsyncMock(return_value=MotorCreated(motor_id='123')) mock_controller_instance.post_motor = mock_response with patch.object( @@ -177,13 +204,17 @@ def test_create_liquid_motor_mass_tank(stub_motor_dump, stub_mass_tank, mock_con assert response.status_code == 200 assert response.json() == { 'motor_id': '123', - 'message': 'motor successfully created', + 'message': 'Motor successfully created', } mock_set_motor_kind.assert_called_once_with(MotorKinds.LIQUID) - mock_controller_instance.post_motor.assert_called_once_with(MotorModel(**stub_motor_dump)) + mock_controller_instance.post_motor.assert_called_once_with( + MotorModel(**stub_motor_dump) + ) -def test_create_hybrid_motor(stub_motor_dump, stub_level_tank, mock_controller_instance): +def test_create_hybrid_motor( + stub_motor_dump, stub_level_tank_dump, mock_controller_instance +): stub_motor_dump.update( { 'grain_number': 0, @@ -194,7 +225,7 @@ def test_create_hybrid_motor(stub_motor_dump, stub_level_tank, mock_controller_i 'grains_center_of_mass_position': 0, 'grain_separation': 0, 'throat_radius': 0, - 'tanks': [stub_level_tank], + 'tanks': [stub_level_tank_dump], } ) mock_response = AsyncMock(return_value=MotorCreated(motor_id='123')) @@ -208,10 +239,12 @@ def test_create_hybrid_motor(stub_motor_dump, stub_level_tank, mock_controller_i assert response.status_code == 200 assert response.json() == { 'motor_id': '123', - 'message': 'motor successfully created', + 'message': 'Motor successfully created', } mock_set_motor_kind.assert_called_once_with(MotorKinds.HYBRID) - mock_controller_instance.post_motor.assert_called_once_with(MotorModel(**stub_motor_dump)) + mock_controller_instance.post_motor.assert_called_once_with( + MotorModel(**stub_motor_dump) + ) def test_create_solid_motor(stub_motor_dump, mock_controller_instance): @@ -237,10 +270,12 @@ def test_create_solid_motor(stub_motor_dump, mock_controller_instance): assert response.status_code == 200 assert response.json() == { 'motor_id': '123', - 'message': 'motor successfully created', + 'message': 'Motor successfully created', } mock_set_motor_kind.assert_called_once_with(MotorKinds.SOLID) - mock_controller_instance.post_motor.assert_called_once_with(MotorModel(**stub_motor_dump)) + mock_controller_instance.post_motor.assert_called_once_with( + MotorModel(**stub_motor_dump) + ) def test_create_motor_invalid_input(): @@ -256,22 +291,26 @@ def test_create_motor_server_error(stub_motor_dump, mock_controller_instance): with patch.object( MotorModel, 'set_motor_kind', side_effect=None ) as mock_set_motor_kind: - response = client.post('/motors/', json=stub_motor_dump, params={'motor_kind': 'HYBRID'}) + response = client.post( + '/motors/', json=stub_motor_dump, params={'motor_kind': 'HYBRID'} + ) assert response.status_code == 500 assert response.json() == {'detail': 'Internal Server Error'} mock_set_motor_kind.assert_called_once_with(MotorKinds.HYBRID) - mock_controller_instance.post_motor.assert_called_once_with(MotorModel(**stub_motor_dump)) + mock_controller_instance.post_motor.assert_called_once_with( + MotorModel(**stub_motor_dump) + ) def test_read_motor(stub_motor_dump, mock_controller_instance): - stub_motor_out = MotorView(motor_id='123', **stub_motor_dump) - mock_response = AsyncMock(return_value=MotorRetrieved(motor=stub_motor_out)) + motor_view = MotorView(motor_id='123', **stub_motor_dump) + mock_response = AsyncMock(return_value=MotorRetrieved(motor=motor_view)) mock_controller_instance.get_motor_by_id = mock_response response = client.get('/motors/123') assert response.status_code == 200 assert response.json() == { - 'message': 'motor successfully retrieved', - 'motor': json.loads(stub_motor_out.model_dump_json()), + 'message': 'Motor successfully retrieved', + 'motor': json.loads(motor_view.model_dump_json()), } mock_controller_instance.get_motor_by_id.assert_called_once_with('123') @@ -300,12 +339,13 @@ def test_update_motor(stub_motor_dump, mock_controller_instance): MotorModel, 'set_motor_kind', side_effect=None ) as mock_set_motor_kind: response = client.put( - '/motors/123', json=stub_motor_dump, params={'motor_kind': 'HYBRID'} + '/motors/123', + json=stub_motor_dump, + params={'motor_kind': 'HYBRID'}, ) assert response.status_code == 200 assert response.json() == { - 'motor_id': '123', - 'message': 'motor successfully updated', + 'message': 'Motor successfully updated', } mock_set_motor_kind.assert_called_once_with(MotorKinds.HYBRID) mock_controller_instance.put_motor_by_id.assert_called_once_with( @@ -329,7 +369,9 @@ def test_update_motor_not_found(stub_motor_dump, mock_controller_instance): MotorModel, 'set_motor_kind', side_effect=None ) as mock_set_motor_kind: response = client.put( - '/motors/123', json=stub_motor_dump, params={'motor_kind': 'HYBRID'} + '/motors/123', + json=stub_motor_dump, + params={'motor_kind': 'HYBRID'}, ) assert response.status_code == 404 assert response.json() == {'detail': 'Not Found'} @@ -346,7 +388,9 @@ def test_update_motor_server_error(stub_motor_dump, mock_controller_instance): MotorModel, 'set_motor_kind', side_effect=None ) as mock_set_motor_kind: response = client.put( - '/motors/123', json=stub_motor_dump, params={'motor_kind': 'HYBRID'} + '/motors/123', + json=stub_motor_dump, + params={'motor_kind': 'HYBRID'}, ) assert response.status_code == 500 assert response.json() == {'detail': 'Internal Server Error'} @@ -362,8 +406,7 @@ def test_delete_motor(mock_controller_instance): response = client.delete('/motors/123') assert response.status_code == 200 assert response.json() == { - 'motor_id': '123', - 'message': 'motor successfully deleted', + 'message': 'Motor successfully deleted', } mock_controller_instance.delete_motor_by_id.assert_called_once_with('123') @@ -377,31 +420,41 @@ def test_delete_motor_server_error(mock_controller_instance): mock_controller_instance.delete_motor_by_id.assert_called_once_with('123') -def test_simulate_motor(mock_controller_instance, stub_motor_dump_summary): - mock_response = AsyncMock(return_value=MotorSummary(**stub_motor_dump_summary)) - mock_controller_instance.simulate_motor = mock_response - response = client.get('/motors/123/summary') +def test_get_motor_simulation( + mock_controller_instance, stub_motor_dump_simulation +): + mock_response = AsyncMock( + return_value=MotorSimulation(**stub_motor_dump_simulation) + ) + mock_controller_instance.get_motor_simulation = mock_response + response = client.get('/motors/123/simulate') assert response.status_code == 200 - assert response.json() == stub_motor_dump_summary - mock_controller_instance.simulate_motor.assert_called_once_with('123') + assert response.json() == stub_motor_dump_simulation + mock_controller_instance.get_motor_simulation.assert_called_once_with( + '123' + ) -def test_simulate_motor_not_found(mock_controller_instance): +def test_get_motor_simulation_not_found(mock_controller_instance): mock_response = AsyncMock(side_effect=HTTPException(status_code=404)) - mock_controller_instance.simulate_motor = mock_response - response = client.get('/motors/123/summary') + mock_controller_instance.get_motor_simulation = mock_response + response = client.get('/motors/123/simulate') assert response.status_code == 404 assert response.json() == {'detail': 'Not Found'} - mock_controller_instance.simulate_motor.assert_called_once_with('123') + mock_controller_instance.get_motor_simulation.assert_called_once_with( + '123' + ) -def test_simulate_motor_server_error(mock_controller_instance): +def test_get_motor_simulation_server_error(mock_controller_instance): mock_response = AsyncMock(side_effect=HTTPException(status_code=500)) - mock_controller_instance.simulate_motor = mock_response - response = client.get('/motors/123/summary') + mock_controller_instance.get_motor_simulation = mock_response + response = client.get('/motors/123/simulate') assert response.status_code == 500 assert response.json() == {'detail': 'Internal Server Error'} - mock_controller_instance.simulate_motor.assert_called_once_with('123') + mock_controller_instance.get_motor_simulation.assert_called_once_with( + '123' + ) def test_read_rocketpy_motor_binary(mock_controller_instance): @@ -411,7 +464,9 @@ def test_read_rocketpy_motor_binary(mock_controller_instance): assert response.status_code == 203 assert response.content == b'rocketpy' assert response.headers['content-type'] == 'application/octet-stream' - mock_controller_instance.get_rocketpy_motor_binary.assert_called_once_with('123') + mock_controller_instance.get_rocketpy_motor_binary.assert_called_once_with( + '123' + ) def test_read_rocketpy_motor_binary_not_found(mock_controller_instance): @@ -420,7 +475,9 @@ def test_read_rocketpy_motor_binary_not_found(mock_controller_instance): response = client.get('/motors/123/rocketpy') assert response.status_code == 404 assert response.json() == {'detail': 'Not Found'} - mock_controller_instance.get_rocketpy_motor_binary.assert_called_once_with('123') + mock_controller_instance.get_rocketpy_motor_binary.assert_called_once_with( + '123' + ) def test_read_rocketpy_motor_binary_server_error(mock_controller_instance): @@ -429,4 +486,6 @@ def test_read_rocketpy_motor_binary_server_error(mock_controller_instance): response = client.get('/motors/123/rocketpy') assert response.status_code == 500 assert response.json() == {'detail': 'Internal Server Error'} - mock_controller_instance.get_rocketpy_motor_binary.assert_called_once_with('123') + mock_controller_instance.get_rocketpy_motor_binary.assert_called_once_with( + '123' + ) diff --git a/tests/test_routes/test_rockets_route.py b/tests/test_routes/test_rockets_route.py index ad6998c..ac99606 100644 --- a/tests/test_routes/test_rockets_route.py +++ b/tests/test_routes/test_rockets_route.py @@ -18,7 +18,7 @@ RocketUpdated, RocketRetrieved, RocketDeleted, - RocketSummary, + RocketSimulation, RocketView, ) from lib import app @@ -27,10 +27,10 @@ @pytest.fixture -def stub_rocket_summary_dump(): - rocket_summary = RocketSummary() - rocket_summary_json = rocket_summary.model_dump_json() - return json.loads(rocket_summary_json) +def stub_rocket_simulation_dump(): + rocket_simulation = RocketSimulation() + rocket_simulation_json = rocket_simulation.model_dump_json() + return json.loads(rocket_simulation_json) @pytest.fixture @@ -82,6 +82,8 @@ def mock_controller_instance(): mock_controller_instance.get_rocket_by_id = Mock() mock_controller_instance.put_rocket_by_id = Mock() mock_controller_instance.delete_rocket_by_id = Mock() + mock_controller_instance.get_rocket_simulation = Mock() + mock_controller_instance.get_rocketpy_rocket_binary = Mock() yield mock_controller_instance @@ -89,21 +91,28 @@ def test_create_rocket(stub_rocket_dump, mock_controller_instance): mock_response = AsyncMock(return_value=RocketCreated(rocket_id='123')) mock_controller_instance.post_rocket = mock_response with patch.object( - MotorModel, 'set_motor_kind', side_effect=None + MotorModel, 'set_motor_kind', side_effect=None ) as mock_set_motor_kind: - response = client.post('/rockets/', json=stub_rocket_dump, params={'motor_kind': 'HYBRID'}) + response = client.post( + '/rockets/', json=stub_rocket_dump, params={'motor_kind': 'HYBRID'} + ) assert response.status_code == 200 assert response.json() == { 'rocket_id': '123', - 'message': 'rocket successfully created', + 'message': 'Rocket successfully created', } mock_set_motor_kind.assert_called_once_with(MotorKinds.HYBRID) mock_controller_instance.post_rocket.assert_called_once_with( - RocketModel(**stub_rocket_dump)) + RocketModel(**stub_rocket_dump) + ) def test_create_rocket_optional_params( - stub_rocket_dump, stub_tail_dump, stub_rail_buttons_dump, stub_parachute_dump, mock_controller_instance + stub_rocket_dump, + stub_tail_dump, + stub_rail_buttons_dump, + stub_parachute_dump, + mock_controller_instance, ): stub_rocket_dump.update( { @@ -115,7 +124,7 @@ def test_create_rocket_optional_params( mock_response = AsyncMock(return_value=RocketCreated(rocket_id='123')) mock_controller_instance.post_rocket = mock_response with patch.object( - MotorModel, 'set_motor_kind', side_effect=None + MotorModel, 'set_motor_kind', side_effect=None ) as mock_set_motor_kind: response = client.post( '/rockets/', json=stub_rocket_dump, params={'motor_kind': 'HYBRID'} @@ -123,13 +132,17 @@ def test_create_rocket_optional_params( assert response.status_code == 200 assert response.json() == { 'rocket_id': '123', - 'message': 'rocket successfully created', + 'message': 'Rocket successfully created', } mock_set_motor_kind.assert_called_once_with(MotorKinds.HYBRID) - mock_controller_instance.post_rocket.assert_called_once_with(RocketModel(**stub_rocket_dump)) + mock_controller_instance.post_rocket.assert_called_once_with( + RocketModel(**stub_rocket_dump) + ) -def test_create_generic_motor_rocket(stub_rocket_dump, stub_motor_dump, mock_controller_instance): +def test_create_generic_motor_rocket( + stub_rocket_dump, stub_motor_dump, mock_controller_instance +): stub_motor_dump.update( { 'chamber_radius': 0, @@ -143,29 +156,36 @@ def test_create_generic_motor_rocket(stub_rocket_dump, stub_motor_dump, mock_con mock_response = AsyncMock(return_value=RocketCreated(rocket_id='123')) mock_controller_instance.post_rocket = mock_response with patch.object( - MotorModel, 'set_motor_kind', side_effect=None + MotorModel, 'set_motor_kind', side_effect=None ) as mock_set_motor_kind: response = client.post( - '/rockets/', json=stub_rocket_dump, params={'motor_kind': 'GENERIC'} + '/rockets/', + json=stub_rocket_dump, + params={'motor_kind': 'GENERIC'}, ) assert response.status_code == 200 assert response.json() == { 'rocket_id': '123', - 'message': 'rocket successfully created', + 'message': 'Rocket successfully created', } mock_set_motor_kind.assert_called_once_with(MotorKinds.GENERIC) - mock_controller_instance.post_rocket.assert_called_once_with(RocketModel(**stub_rocket_dump)) + mock_controller_instance.post_rocket.assert_called_once_with( + RocketModel(**stub_rocket_dump) + ) def test_create_liquid_motor_level_tank_rocket( - stub_rocket_dump, stub_motor_dump, stub_level_tank_dump, mock_controller_instance + stub_rocket_dump, + stub_motor_dump, + stub_level_tank_dump, + mock_controller_instance, ): stub_motor_dump.update({'tanks': [stub_level_tank_dump]}) stub_rocket_dump.update({'motor': stub_motor_dump}) mock_response = AsyncMock(return_value=RocketCreated(rocket_id='123')) mock_controller_instance.post_rocket = mock_response with patch.object( - MotorModel, 'set_motor_kind', side_effect=None + MotorModel, 'set_motor_kind', side_effect=None ) as mock_set_motor_kind: response = client.post( '/rockets/', json=stub_rocket_dump, params={'motor_kind': 'LIQUID'} @@ -173,21 +193,26 @@ def test_create_liquid_motor_level_tank_rocket( assert response.status_code == 200 assert response.json() == { 'rocket_id': '123', - 'message': 'rocket successfully created', + 'message': 'Rocket successfully created', } mock_set_motor_kind.assert_called_once_with(MotorKinds.LIQUID) - mock_controller_instance.post_rocket.assert_called_once_with(RocketModel(**stub_rocket_dump)) + mock_controller_instance.post_rocket.assert_called_once_with( + RocketModel(**stub_rocket_dump) + ) def test_create_liquid_motor_mass_flow_tank_rocket( - stub_rocket_dump, stub_motor_dump, stub_mass_flow_tank_dump, mock_controller_instance + stub_rocket_dump, + stub_motor_dump, + stub_mass_flow_tank_dump, + mock_controller_instance, ): stub_motor_dump.update({'tanks': [stub_mass_flow_tank_dump]}) stub_rocket_dump.update({'motor': stub_motor_dump}) mock_response = AsyncMock(return_value=RocketCreated(rocket_id='123')) mock_controller_instance.post_rocket = mock_response with patch.object( - MotorModel, 'set_motor_kind', side_effect=None + MotorModel, 'set_motor_kind', side_effect=None ) as mock_set_motor_kind: response = client.post( '/rockets/', json=stub_rocket_dump, params={'motor_kind': 'LIQUID'} @@ -195,21 +220,26 @@ def test_create_liquid_motor_mass_flow_tank_rocket( assert response.status_code == 200 assert response.json() == { 'rocket_id': '123', - 'message': 'rocket successfully created', + 'message': 'Rocket successfully created', } mock_set_motor_kind.assert_called_once_with(MotorKinds.LIQUID) - mock_controller_instance.post_rocket.assert_called_once_with(RocketModel(**stub_rocket_dump)) + mock_controller_instance.post_rocket.assert_called_once_with( + RocketModel(**stub_rocket_dump) + ) def test_create_liquid_motor_ullage_tank_rocket( - stub_rocket_dump, stub_motor_dump, stub_ullage_tank_dump, mock_controller_instance + stub_rocket_dump, + stub_motor_dump, + stub_ullage_tank_dump, + mock_controller_instance, ): stub_motor_dump.update({'tanks': [stub_ullage_tank_dump]}) stub_rocket_dump.update({'motor': stub_motor_dump}) mock_response = AsyncMock(return_value=RocketCreated(rocket_id='123')) mock_controller_instance.post_rocket = mock_response with patch.object( - MotorModel, 'set_motor_kind', side_effect=None + MotorModel, 'set_motor_kind', side_effect=None ) as mock_set_motor_kind: response = client.post( '/rockets/', json=stub_rocket_dump, params={'motor_kind': 'LIQUID'} @@ -217,21 +247,26 @@ def test_create_liquid_motor_ullage_tank_rocket( assert response.status_code == 200 assert response.json() == { 'rocket_id': '123', - 'message': 'rocket successfully created', + 'message': 'Rocket successfully created', } mock_set_motor_kind.assert_called_once_with(MotorKinds.LIQUID) - mock_controller_instance.post_rocket.assert_called_once_with(RocketModel(**stub_rocket_dump)) + mock_controller_instance.post_rocket.assert_called_once_with( + RocketModel(**stub_rocket_dump) + ) def test_create_liquid_motor_mass_tank_rocket( - stub_rocket_dump, stub_motor_dump, stub_mass_tank_dump, mock_controller_instance + stub_rocket_dump, + stub_motor_dump, + stub_mass_tank_dump, + mock_controller_instance, ): stub_motor_dump.update({'tanks': [stub_mass_tank_dump]}) stub_rocket_dump.update({'motor': stub_motor_dump}) mock_response = AsyncMock(return_value=RocketCreated(rocket_id='123')) mock_controller_instance.post_rocket = mock_response with patch.object( - MotorModel, 'set_motor_kind', side_effect=None + MotorModel, 'set_motor_kind', side_effect=None ) as mock_set_motor_kind: response = client.post( '/rockets/', json=stub_rocket_dump, params={'motor_kind': 'LIQUID'} @@ -239,13 +274,20 @@ def test_create_liquid_motor_mass_tank_rocket( assert response.status_code == 200 assert response.json() == { 'rocket_id': '123', - 'message': 'rocket successfully created', + 'message': 'Rocket successfully created', } mock_set_motor_kind.assert_called_once_with(MotorKinds.LIQUID) - mock_controller_instance.post_rocket.assert_called_once_with(RocketModel(**stub_rocket_dump)) + mock_controller_instance.post_rocket.assert_called_once_with( + RocketModel(**stub_rocket_dump) + ) -def test_create_hybrid_motor_rocket(stub_rocket_dump, stub_motor_dump, stub_level_tank_dump, mock_controller_instance): +def test_create_hybrid_motor_rocket( + stub_rocket_dump, + stub_motor_dump, + stub_level_tank_dump, + mock_controller_instance, +): stub_motor_dump.update( { 'grain_number': 0, @@ -263,7 +305,7 @@ def test_create_hybrid_motor_rocket(stub_rocket_dump, stub_motor_dump, stub_leve mock_response = AsyncMock(return_value=RocketCreated(rocket_id='123')) mock_controller_instance.post_rocket = mock_response with patch.object( - MotorModel, 'set_motor_kind', side_effect=None + MotorModel, 'set_motor_kind', side_effect=None ) as mock_set_motor_kind: response = client.post( '/rockets/', json=stub_rocket_dump, params={'motor_kind': 'HYBRID'} @@ -271,13 +313,17 @@ def test_create_hybrid_motor_rocket(stub_rocket_dump, stub_motor_dump, stub_leve assert response.status_code == 200 assert response.json() == { 'rocket_id': '123', - 'message': 'rocket successfully created', + 'message': 'Rocket successfully created', } mock_set_motor_kind.assert_called_once_with(MotorKinds.HYBRID) - mock_controller_instance.post_rocket.assert_called_once_with(RocketModel(**stub_rocket_dump)) + mock_controller_instance.post_rocket.assert_called_once_with( + RocketModel(**stub_rocket_dump) + ) -def test_create_solid_motor_rocket(stub_rocket_dump, stub_motor_dump, mock_controller_instance): +def test_create_solid_motor_rocket( + stub_rocket_dump, stub_motor_dump, mock_controller_instance +): stub_motor_dump.update( { 'grain_number': 0, @@ -293,7 +339,7 @@ def test_create_solid_motor_rocket(stub_rocket_dump, stub_motor_dump, mock_contr mock_response = AsyncMock(return_value=RocketCreated(rocket_id='123')) mock_controller_instance.post_rocket = mock_response with patch.object( - MotorModel, 'set_motor_kind', side_effect=None + MotorModel, 'set_motor_kind', side_effect=None ) as mock_set_motor_kind: response = client.post( '/rockets/', json=stub_rocket_dump, params={'motor_kind': 'SOLID'} @@ -301,10 +347,12 @@ def test_create_solid_motor_rocket(stub_rocket_dump, stub_motor_dump, mock_contr assert response.status_code == 200 assert response.json() == { 'rocket_id': '123', - 'message': 'rocket successfully created', + 'message': 'Rocket successfully created', } mock_set_motor_kind.assert_called_once_with(MotorKinds.SOLID) - mock_controller_instance.post_rocket.assert_called_once_with(RocketModel(**stub_rocket_dump)) + mock_controller_instance.post_rocket.assert_called_once_with( + RocketModel(**stub_rocket_dump) + ) def test_create_rocket_invalid_input(): @@ -312,7 +360,9 @@ def test_create_rocket_invalid_input(): assert response.status_code == 422 -def test_create_rocket_server_error(stub_rocket_dump, mock_controller_instance): +def test_create_rocket_server_error( + stub_rocket_dump, mock_controller_instance +): mock_controller_instance.post_rocket.side_effect = HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR ) @@ -323,16 +373,18 @@ def test_create_rocket_server_error(stub_rocket_dump, mock_controller_instance): assert response.json() == {'detail': 'Internal Server Error'} -def test_read_rocket(stub_rocket_dump, stub_motor_dump, mock_controller_instance): +def test_read_rocket( + stub_rocket_dump, stub_motor_dump, mock_controller_instance +): stub_rocket_dump.update({'motor': stub_motor_dump}) - stub_rocket_out = RocketView(rocket_id='123', **stub_rocket_dump) - mock_response = AsyncMock(return_value=RocketRetrieved(rocket=stub_rocket_out)) + rocket_view = RocketView(rocket_id='123', **stub_rocket_dump) + mock_response = AsyncMock(return_value=RocketRetrieved(rocket=rocket_view)) mock_controller_instance.get_rocket_by_id = mock_response response = client.get('/rockets/123') assert response.status_code == 200 assert response.json() == { - 'message': 'rocket successfully retrieved', - 'rocket': json.loads(stub_rocket_out.model_dump_json()), + 'message': 'Rocket successfully retrieved', + 'rocket': json.loads(rocket_view.model_dump_json()), } mock_controller_instance.get_rocket_by_id.assert_called_once_with('123') @@ -359,7 +411,7 @@ def test_update_rocket(stub_rocket_dump, mock_controller_instance): mock_response = AsyncMock(return_value=RocketUpdated(rocket_id='123')) mock_controller_instance.put_rocket_by_id = mock_response with patch.object( - MotorModel, 'set_motor_kind', side_effect=None + MotorModel, 'set_motor_kind', side_effect=None ) as mock_set_motor_kind: response = client.put( '/rockets/123', @@ -368,11 +420,12 @@ def test_update_rocket(stub_rocket_dump, mock_controller_instance): ) assert response.status_code == 200 assert response.json() == { - 'rocket_id': '123', 'message': 'Rocket successfully updated', } mock_set_motor_kind.assert_called_once_with(MotorKinds.HYBRID) - mock_controller_instance.put_rocket_by_id.assert_called_once_with('123', RocketModel(**stub_rocket_dump)) + mock_controller_instance.put_rocket_by_id.assert_called_once_with( + '123', RocketModel(**stub_rocket_dump) + ) def test_update_rocket_invalid_input(): @@ -397,7 +450,9 @@ def test_update_rocket_not_found(stub_rocket_dump, mock_controller_instance): assert response.json() == {'detail': 'Not Found'} -def test_update_rocket_server_error(stub_rocket_dump, mock_controller_instance): +def test_update_rocket_server_error( + stub_rocket_dump, mock_controller_instance +): mock_controller_instance.put_rocket_by_id.side_effect = HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR ) @@ -416,7 +471,6 @@ def test_delete_rocket(mock_controller_instance): response = client.delete('/rockets/123') assert response.status_code == 200 assert response.json() == { - 'rocket_id': '123', 'message': 'Rocket successfully deleted', } mock_controller_instance.delete_rocket_by_id.assert_called_once_with('123') @@ -431,45 +485,54 @@ def test_delete_rocket_server_error(mock_controller_instance): assert response.json() == {'detail': 'Internal Server Error'} -def test_simulate_rocket(stub_rocket_summary_dump, mock_controller_instance): - mock_response = AsyncMock(return_value=RocketSummary(**stub_rocket_summary_dump)) - mock_controller_instance.simulate_rocket = mock_response - response = client.get('/rockets/123/summary') +def test_get_rocket_simulation( + stub_rocket_simulation_dump, mock_controller_instance +): + mock_response = AsyncMock( + return_value=RocketSimulation(**stub_rocket_simulation_dump) + ) + mock_controller_instance.get_rocket_simulation = mock_response + response = client.get('/rockets/123/simulate') assert response.status_code == 200 - assert response.json() == stub_rocket_summary_dump - mock_controller_instance.simulate_rocket.assert_called_once_with('123') + assert response.json() == stub_rocket_simulation_dump + mock_controller_instance.get_rocket_simulation.assert_called_once_with( + '123' + ) -def test_simulate_rocket_not_found(mock_controller_instance): - mock_controller_instance.simulate_rocket.side_effect = HTTPException( +def test_get_rocket_simulation_not_found(mock_controller_instance): + mock_controller_instance.get_rocket_simulation.side_effect = HTTPException( status_code=status.HTTP_404_NOT_FOUND ) - response = client.get('/rockets/123/summary') + response = client.get('/rockets/123/simulate') assert response.status_code == 404 assert response.json() == {'detail': 'Not Found'} -def test_simulate_rocket_server_error(mock_controller_instance): - mock_controller_instance.simulate_rocket.side_effect = HTTPException( +def test_get_rocket_simulation_server_error(mock_controller_instance): + mock_controller_instance.get_rocket_simulation.side_effect = HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR ) - response = client.get('/rockets/123/summary') + response = client.get('/rockets/123/simulate') assert response.status_code == 500 assert response.json() == {'detail': 'Internal Server Error'} def test_read_rocketpy_rocket_binary(mock_controller_instance): - mock_controller_instance.get_rocketpy_rocket_binary.return_value = b'rocketpy' + mock_response = AsyncMock(return_value=b'rocketpy') + mock_controller_instance.get_rocketpy_rocket_binary = mock_response response = client.get('/rockets/123/rocketpy') assert response.status_code == 203 assert response.content == b'rocketpy' assert response.headers['content-type'] == 'application/octet-stream' - mock_controller_instance.get_rocketpy_rocket_binary.assert_called_once_with('123') + mock_controller_instance.get_rocketpy_rocket_binary.assert_called_once_with( + '123' + ) def test_read_rocketpy_rocket_binary_not_found(mock_controller_instance): - mock_controller_instance.get_rocketpy_rocket_binary.side_effect = HTTPException( - status_code=status.HTTP_404_NOT_FOUND + mock_controller_instance.get_rocketpy_rocket_binary.side_effect = ( + HTTPException(status_code=status.HTTP_404_NOT_FOUND) ) response = client.get('/rockets/123/rocketpy') assert response.status_code == 404 @@ -477,8 +540,8 @@ def test_read_rocketpy_rocket_binary_not_found(mock_controller_instance): def test_read_rocketpy_rocket_binary_server_error(mock_controller_instance): - mock_controller_instance.get_rocketpy_rocket_binary.side_effect = HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR + mock_controller_instance.get_rocketpy_rocket_binary.side_effect = ( + HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR) ) response = client.get('/rockets/123/rocketpy') assert response.status_code == 500 From 8ff0c8de41dfe055f25b9b2931426980208767cf Mon Sep 17 00:00:00 2001 From: Gabriel Barberini Date: Sun, 16 Feb 2025 17:52:07 -0300 Subject: [PATCH 11/34] improves conditional reading --- lib/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/utils.py b/lib/utils.py index 9fdd7f8..a6dd5a4 100644 --- a/lib/utils.py +++ b/lib/utils.py @@ -95,7 +95,7 @@ async def send_with_gzip(self, message: Message) -> None: self.started = True body = message.get("body", b"") more_body = message.get("more_body", False) - if len(body) < (self.minimum_size and not more_body) or any( + if (len(body) < (self.minimum_size and not more_body)) or any( value == b'application/octet-stream' for header, value in self.initial_message["headers"] ): From 44581ad880c3886f5b71d24bb9ac0d111aa8ca91 Mon Sep 17 00:00:00 2001 From: Gabriel Barberini Date: Sun, 16 Feb 2025 19:50:33 -0300 Subject: [PATCH 12/34] moves unit tests to unit dir --- README.md | 51 ++++--------------- .../test_controller_interface.py | 0 .../test_repository_interface.py | 0 tests/{ => unit}/test_routes/conftest.py | 0 .../test_routes/test_environments_route.py | 0 .../test_routes/test_flights_route.py | 0 .../test_routes/test_motors_route.py | 0 .../test_routes/test_rockets_route.py | 0 8 files changed, 9 insertions(+), 42 deletions(-) rename tests/{ => unit}/test_controllers/test_controller_interface.py (100%) rename tests/{ => unit}/test_repositories/test_repository_interface.py (100%) rename tests/{ => unit}/test_routes/conftest.py (100%) rename tests/{ => unit}/test_routes/test_environments_route.py (100%) rename tests/{ => unit}/test_routes/test_flights_route.py (100%) rename tests/{ => unit}/test_routes/test_motors_route.py (100%) rename tests/{ => unit}/test_routes/test_rockets_route.py (100%) diff --git a/README.md b/README.md index 7513edb..6839402 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,7 @@ $ touch .env && echo MONGODB_CONNECTION_STRING="$ConnectionString" > .env │ │── secrets.py │   │   │   ├── controllers +│   │   ├── interface.py │   │   ├── environment.py │   │   ├── flight.py │   │   ├── motor.py @@ -57,65 +58,31 @@ $ touch .env && echo MONGODB_CONNECTION_STRING="$ConnectionString" > .env │   │   └── rocket.py │   │   │   ├── repositories -│   │   ├── repo.py +│   │   ├── interface.py │   │   ├── environment.py │   │   ├── flight.py │   │   ├── motor.py │   │   └── rocket.py │   │   │   ├── models -│   │   ├── aerosurfaces.py +│   │   ├── interface.py │   │   ├── environment.py │   │   ├── flight.py │   │   ├── motor.py -│   │   └── rocket.py +│   │   ├── rocket.py +│   │   │   +│   │   └── sub +│   │ ├── aerosurfaces.py +│ │ └── tanks.py │   │   │   └── views +│   ├── interface.py │   ├── environment.py │   ├── flight.py │   ├── motor.py │   └── rocket.py │   └── tests - ├── integration - │   ├── test_environment_integration.py - │   ├── test_motor_integration.py - │   ├── test_rocket_integration.py - │   └── test_flight_integration.py - │   - └── unit - ├── test_secrets.py - ├── test_api.py - │   - ├── test_controllers - │   ├── test_environment_controller.py - │   ├── test_flight_controller.py - │   ├── test_motor_controller.py - │   └── test_rocket_controller.py - │   - ├── test_services - │   ├── test_environment_service.py - │   ├── test_flight_service.py - │   ├── test_motor_service.py - │   └── test_rocket_serice.py - │ - ├── test_repositories - │   ├── test_environment_repo.py - │   ├── test_flight_repo.py - │   ├── test_motor_repo.py - │   └── test_rocket_repo.py - │ - ├── test_models - │   ├── test_environment_model.py - │   ├── test_flight_model.py - │   ├── test_motor_model.py - │   └── test_rocket_model.py - │   - └── test_views -    ├── test_environment_view.py -    ├── test_flight_view.py -    ├── test_motor_view.py -    └── test_rocket_view.py ``` ## DOCS diff --git a/tests/test_controllers/test_controller_interface.py b/tests/unit/test_controllers/test_controller_interface.py similarity index 100% rename from tests/test_controllers/test_controller_interface.py rename to tests/unit/test_controllers/test_controller_interface.py diff --git a/tests/test_repositories/test_repository_interface.py b/tests/unit/test_repositories/test_repository_interface.py similarity index 100% rename from tests/test_repositories/test_repository_interface.py rename to tests/unit/test_repositories/test_repository_interface.py diff --git a/tests/test_routes/conftest.py b/tests/unit/test_routes/conftest.py similarity index 100% rename from tests/test_routes/conftest.py rename to tests/unit/test_routes/conftest.py diff --git a/tests/test_routes/test_environments_route.py b/tests/unit/test_routes/test_environments_route.py similarity index 100% rename from tests/test_routes/test_environments_route.py rename to tests/unit/test_routes/test_environments_route.py diff --git a/tests/test_routes/test_flights_route.py b/tests/unit/test_routes/test_flights_route.py similarity index 100% rename from tests/test_routes/test_flights_route.py rename to tests/unit/test_routes/test_flights_route.py diff --git a/tests/test_routes/test_motors_route.py b/tests/unit/test_routes/test_motors_route.py similarity index 100% rename from tests/test_routes/test_motors_route.py rename to tests/unit/test_routes/test_motors_route.py diff --git a/tests/test_routes/test_rockets_route.py b/tests/unit/test_routes/test_rockets_route.py similarity index 100% rename from tests/test_routes/test_rockets_route.py rename to tests/unit/test_routes/test_rockets_route.py From 57f3142ce7c885ee7831a875834d48b1d897c679 Mon Sep 17 00:00:00 2001 From: Gabriel Barberini Date: Sun, 16 Feb 2025 20:11:09 -0300 Subject: [PATCH 13/34] removes unnecessary validator --- lib/models/interface.py | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/lib/models/interface.py b/lib/models/interface.py index ddebd9f..2b0cdad 100644 --- a/lib/models/interface.py +++ b/lib/models/interface.py @@ -4,13 +4,11 @@ BaseModel, PrivateAttr, ConfigDict, - model_validator, ) -from bson import ObjectId class ApiBaseModel(BaseModel, ABC): - _id: Optional[ObjectId] = PrivateAttr(default=None) + _id: Optional[str] = PrivateAttr(default=None) model_config = ConfigDict( extra="allow", use_enum_values=True, @@ -25,17 +23,6 @@ def set_id(self, value): def get_id(self): return self._id - @model_validator(mode='after') - def validate_computed_id(self): - """Validate _id after model instantiation""" - if self._id is not None: - if not isinstance(self._id, ObjectId): - try: - self._id = ObjectId(str(self._id)) - except Exception as e: - raise ValueError(f"Invalid ObjectId: {e}") - return self - @property @abstractmethod def NAME(): # pylint: disable=invalid-name, no-method-argument From 4417bee4a6acc576e3d4c066f061d2dc55ee77d5 Mon Sep 17 00:00:00 2001 From: Gabriel Barberini Date: Sun, 16 Feb 2025 20:48:39 -0300 Subject: [PATCH 14/34] fixes runtime issues --- lib/controllers/environment.py | 14 +++++++++----- lib/controllers/flight.py | 12 ++++++++---- lib/controllers/motor.py | 8 ++++---- lib/controllers/rocket.py | 12 ++++++++---- lib/models/interface.py | 1 - lib/views/environment.py | 1 + lib/views/flight.py | 1 + lib/views/motor.py | 1 + lib/views/rocket.py | 1 + 9 files changed, 33 insertions(+), 18 deletions(-) diff --git a/lib/controllers/environment.py b/lib/controllers/environment.py index c5a81e7..8a55e3a 100644 --- a/lib/controllers/environment.py +++ b/lib/controllers/environment.py @@ -20,7 +20,7 @@ def __init__(self): super().__init__(models=[EnvironmentModel]) @controller_exception_handler - async def get_rocketpy_env_binary( + async def get_rocketpy_environment_binary( self, env_id: str, ) -> bytes: @@ -36,8 +36,10 @@ async def get_rocketpy_env_binary( Raises: HTTP 404 Not Found: If the env is not found in the database. """ - env = await self.get_environment_by_id(env_id) - env_service = EnvironmentService.from_env_model(env) + env_retrieved = await self.get_environment_by_id(env_id) + env_service = EnvironmentService.from_env_model( + env_retrieved.environment + ) return env_service.get_environment_binary() @controller_exception_handler @@ -56,6 +58,8 @@ async def get_environment_simulation( Raises: HTTP 404 Not Found: If the env does not exist in the database. """ - env = await self.get_environment_by_id(env_id) - env_service = EnvironmentService.from_env_model(env) + env_retrieved = await self.get_environment_by_id(env_id) + env_service = EnvironmentService.from_env_model( + env_retrieved.environment + ) return env_service.get_environment_simulation() diff --git a/lib/controllers/flight.py b/lib/controllers/flight.py index 00f3c16..702d454 100644 --- a/lib/controllers/flight.py +++ b/lib/controllers/flight.py @@ -82,8 +82,10 @@ async def get_rocketpy_flight_binary( Raises: HTTP 404 Not Found: If the flight is not found in the database. """ - flight = await self.get_flight_by_id(flight_id) - flight_service = FlightService.from_flight_model(flight) + flight_retrieved = await self.get_flight_by_id(flight_id) + flight_service = FlightService.from_flight_model( + flight_retrieved.flight + ) return flight_service.get_flight_binary() @controller_exception_handler @@ -103,6 +105,8 @@ async def get_flight_simulation( Raises: HTTP 404 Not Found: If the flight does not exist in the database. """ - flight = await self.get_flight_by_id(flight_id=flight_id) - flight_service = FlightService.from_flight_model(flight) + flight_retrieved = await self.get_flight_by_id(flight_id=flight_id) + flight_service = FlightService.from_flight_model( + flight_retrieved.flight + ) return flight_service.get_flight_simmulation() diff --git a/lib/controllers/motor.py b/lib/controllers/motor.py index 73b2704..a030922 100644 --- a/lib/controllers/motor.py +++ b/lib/controllers/motor.py @@ -36,8 +36,8 @@ async def get_rocketpy_motor_binary( Raises: HTTP 404 Not Found: If the motor is not found in the database. """ - motor = await self.get_motor_by_id(motor_id) - motor_service = MotorService.from_motor_model(motor) + motor_retrieved = await self.get_motor_by_id(motor_id) + motor_service = MotorService.from_motor_model(motor_retrieved.motor) return motor_service.get_motor_binary() @controller_exception_handler @@ -54,6 +54,6 @@ async def get_motor_simulation(self, motor_id: str) -> MotorSimulation: Raises: HTTP 404 Not Found: If the motor does not exist in the database. """ - motor = await self.get_motor_by_id(motor_id) - motor_service = MotorService.from_motor_model(motor) + motor_retrieved = await self.get_motor_by_id(motor_id) + motor_service = MotorService.from_motor_model(motor_retrieved.motor) return motor_service.get_motor_simulation() diff --git a/lib/controllers/rocket.py b/lib/controllers/rocket.py index 4a1a131..6f305e6 100644 --- a/lib/controllers/rocket.py +++ b/lib/controllers/rocket.py @@ -33,8 +33,10 @@ async def get_rocketpy_rocket_binary(self, rocket_id: str) -> bytes: Raises: HTTP 404 Not Found: If the rocket is not found in the database. """ - rocket = await self.get_rocket_by_id(rocket_id) - rocket_service = RocketService.from_rocket_model(rocket) + rocket_retrieved = await self.get_rocket_by_id(rocket_id) + rocket_service = RocketService.from_rocket_model( + rocket_retrieved.rocket + ) return rocket_service.get_rocket_binary() @controller_exception_handler @@ -54,6 +56,8 @@ async def get_rocket_simulation( Raises: HTTP 404 Not Found: If the rocket does not exist in the database. """ - rocket = await self.get_rocket_by_id(rocket_id) - rocket_service = RocketService.from_rocket_model(rocket) + rocket_retrieved = await self.get_rocket_by_id(rocket_id) + rocket_service = RocketService.from_rocket_model( + rocket_retrieved.rocket + ) return rocket_service.get_rocket_simulation() diff --git a/lib/models/interface.py b/lib/models/interface.py index 2b0cdad..1703eab 100644 --- a/lib/models/interface.py +++ b/lib/models/interface.py @@ -10,7 +10,6 @@ class ApiBaseModel(BaseModel, ABC): _id: Optional[str] = PrivateAttr(default=None) model_config = ConfigDict( - extra="allow", use_enum_values=True, validate_default=True, validate_all_in_root=True, diff --git a/lib/views/environment.py b/lib/views/environment.py index d0ca44d..f3537b3 100644 --- a/lib/views/environment.py +++ b/lib/views/environment.py @@ -6,6 +6,7 @@ class EnvironmentSimulation(ApiBaseView): + message: str = "Environment successfully simulated" latitude: Optional[float] = None longitude: Optional[float] = None elevation: Optional[float] = 1 diff --git a/lib/views/flight.py b/lib/views/flight.py index ddcdf56..9cbcae1 100644 --- a/lib/views/flight.py +++ b/lib/views/flight.py @@ -7,6 +7,7 @@ class FlightSimulation(RocketSimulation, EnvironmentSimulation): + message: str = "Flight successfully simulated" name: Optional[str] = None max_time: Optional[int] = None min_time_step: Optional[int] = None diff --git a/lib/views/motor.py b/lib/views/motor.py index 7ab72bb..87759c4 100644 --- a/lib/views/motor.py +++ b/lib/views/motor.py @@ -6,6 +6,7 @@ class MotorSimulation(BaseModel): + message: str = "Motor successfully simulated" average_thrust: Optional[float] = None burn_duration: Optional[float] = None burn_out_time: Optional[float] = None diff --git a/lib/views/rocket.py b/lib/views/rocket.py index 4d9944c..a81b98e 100644 --- a/lib/views/rocket.py +++ b/lib/views/rocket.py @@ -6,6 +6,7 @@ class RocketSimulation(MotorSimulation): + message: str = "Rocket successfully simulated" area: Optional[float] = None coordinate_system_orientation: str = 'tail_to_nose' center_of_mass_without_motor: Optional[float] = None From a595acc9274277d0dcccb5b784be6dd6b52bd940 Mon Sep 17 00:00:00 2001 From: Gabriel Barberini Date: Mon, 17 Feb 2025 18:26:36 -0300 Subject: [PATCH 15/34] removes unnecessary response data from PUT/DELETE --- README.md | 30 ++++++++-------- lib/controllers/flight.py | 14 ++++---- lib/models/environment.py | 8 ++--- lib/models/flight.py | 8 ++--- lib/models/motor.py | 8 ++--- lib/models/rocket.py | 8 ++--- lib/routes/environment.py | 12 +++---- lib/routes/flight.py | 20 +++++------ lib/routes/motor.py | 12 +++---- lib/routes/rocket.py | 12 +++---- lib/views/environment.py | 8 ----- lib/views/flight.py | 8 ----- lib/views/motor.py | 8 ----- lib/views/rocket.py | 8 ----- .../test_routes/test_environments_route.py | 24 ++++--------- tests/unit/test_routes/test_flights_route.py | 34 ++++++------------- tests/unit/test_routes/test_motors_route.py | 34 +++++++------------ tests/unit/test_routes/test_rockets_route.py | 34 +++++++------------ 18 files changed, 96 insertions(+), 194 deletions(-) diff --git a/README.md b/README.md index 6839402..8772372 100644 --- a/README.md +++ b/README.md @@ -102,21 +102,21 @@ sequenceDiagram User ->> API: POST /model API ->> MongoDB: Persist API Model as a document MongoDB -->> API: Model ID - API -->> User: ModelCreated View + API -->> User: 201 ModelCreated View User ->> API: GET /model/:id API ->> MongoDB: Read API Model document MongoDB -->> API: API Model document - API -->> User: API ModelView + API -->> User: 200 API ModelView User ->> API: PUT /model/:id API ->> MongoDB: Update API Model document - API -->> User: ModelUpdated View + API -->> User: 204 User ->> API: DELETE /model/:id API ->> MongoDB: Delete API Model document MongoDB -->> API: Deletion Confirmation - API -->> User: ModelDeleted View + API -->> User: 204 ``` @@ -126,19 +126,19 @@ sequenceDiagram participant User participant API participant MongoDB - participant Rocketpy lib + participant RocketPy lib - User ->> API: GET /summary/model/:id - API -->> MongoDB: Retrieve Rocketpy native class - MongoDB -->> API: Rocketpy native class - API ->> Rocketpy lib: Simulate Rocketpy native class - Rocketpy lib -->> API: Simulation Results + User ->> API: GET model/:id/simulate/ + API -->> MongoDB: Retrieve API Model document + MongoDB -->> API: API Model document + API ->> RocketPy: Initialize RocketPy native class and simulate + RocketPy lib -->> API: Simulation Results API -->> User: Simulation Results User ->> API: GET /model/:id/rocketpy - API -->> MongoDB: Retrieve Rocketpy Model - MongoDB -->> API: Rocketpy Model - API ->> Rocketpy lib: Rocketpy Model - Rocketpy lib -->> API: Rocketpy native class - API -->> User: Rocketpy native class as .dill binary + API -->> MongoDB: Retrieve API Model document + MongoDB -->> API: API Model document + API ->> RocketPy: Initialize RocketPy native class + RocketPy lib -->> API: RocketPy native class + API -->> User: RocketPy native class as .dill binary (amd64) ``` diff --git a/lib/controllers/flight.py b/lib/controllers/flight.py index 702d454..181e38f 100644 --- a/lib/controllers/flight.py +++ b/lib/controllers/flight.py @@ -2,7 +2,7 @@ ControllerInterface, controller_exception_handler, ) -from lib.views.flight import FlightSimulation, FlightUpdated +from lib.views.flight import FlightSimulation from lib.models.flight import FlightModel from lib.models.environment import EnvironmentModel from lib.models.rocket import RocketModel @@ -24,7 +24,7 @@ def __init__(self): @controller_exception_handler async def update_environment_by_flight_id( self, flight_id: str, *, environment: EnvironmentModel - ) -> FlightUpdated: + ) -> None: """ Update a models.Flight.environment in the database. @@ -33,7 +33,7 @@ async def update_environment_by_flight_id( environment: models.Environment Returns: - views.FlightUpdated + None Raises: HTTP 404 Not Found: If the flight is not found in the database. @@ -41,12 +41,12 @@ async def update_environment_by_flight_id( flight = await self.get_flight_by_id(flight_id) flight.environment = environment self.update_flight_by_id(flight_id, flight) - return FlightUpdated(flight_id=flight_id) + return @controller_exception_handler async def update_rocket_by_flight_id( self, flight_id: str, *, rocket: RocketModel - ) -> FlightUpdated: + ) -> None: """ Update a models.Flight.rocket in the database. @@ -55,7 +55,7 @@ async def update_rocket_by_flight_id( rocket: models.Rocket Returns: - views.FlightUpdated + None Raises: HTTP 404 Not Found: If the flight is not found in the database. @@ -63,7 +63,7 @@ async def update_rocket_by_flight_id( flight = await self.get_flight_by_id(flight_id) flight.rocket = rocket self.update_flight_by_id(flight_id, flight) - return FlightUpdated(flight_id=flight_id) + return @controller_exception_handler async def get_rocketpy_flight_binary( diff --git a/lib/models/environment.py b/lib/models/environment.py index 58d31c4..e77bceb 100644 --- a/lib/models/environment.py +++ b/lib/models/environment.py @@ -26,15 +26,11 @@ class EnvironmentModel(ApiBaseModel): @staticmethod def UPDATED(): - from lib.views.environment import EnvironmentUpdated - - return EnvironmentUpdated() + return @staticmethod def DELETED(): - from lib.views.environment import EnvironmentDeleted - - return EnvironmentDeleted() + return @staticmethod def CREATED(model_id: str): diff --git a/lib/models/flight.py b/lib/models/flight.py index 473fef2..a31f377 100644 --- a/lib/models/flight.py +++ b/lib/models/flight.py @@ -46,15 +46,11 @@ def get_additional_parameters(self): @staticmethod def UPDATED(): - from lib.views.flight import FlightUpdated - - return FlightUpdated() + return @staticmethod def DELETED(): - from lib.views.flight import FlightDeleted - - return FlightDeleted() + return @staticmethod def CREATED(model_id: str): diff --git a/lib/models/motor.py b/lib/models/motor.py index 94a59ae..d5d6596 100644 --- a/lib/models/motor.py +++ b/lib/models/motor.py @@ -82,15 +82,11 @@ def set_motor_kind(self, motor_kind: MotorKinds): @staticmethod def UPDATED(): - from lib.views.motor import MotorUpdated - - return MotorUpdated() + return @staticmethod def DELETED(): - from lib.views.motor import MotorDeleted - - return MotorDeleted() + return @staticmethod def CREATED(model_id: str): diff --git a/lib/models/rocket.py b/lib/models/rocket.py index 872a80a..4947057 100644 --- a/lib/models/rocket.py +++ b/lib/models/rocket.py @@ -39,15 +39,11 @@ class RocketModel(ApiBaseModel): @staticmethod def UPDATED(): - from lib.views.rocket import RocketUpdated - - return RocketUpdated() + return @staticmethod def DELETED(): - from lib.views.rocket import RocketDeleted - - return RocketDeleted() + return @staticmethod def CREATED(model_id: str): diff --git a/lib/routes/environment.py b/lib/routes/environment.py index a0837e3..9249091 100644 --- a/lib/routes/environment.py +++ b/lib/routes/environment.py @@ -9,8 +9,6 @@ EnvironmentSimulation, EnvironmentCreated, EnvironmentRetrieved, - EnvironmentUpdated, - EnvironmentDeleted, ) from lib.models.environment import EnvironmentModel from lib.controllers.environment import EnvironmentController @@ -28,7 +26,7 @@ tracer = trace.get_tracer(__name__) -@router.post("/") +@router.post("/", status_code=201) async def create_environment( environment: EnvironmentModel, ) -> EnvironmentCreated: @@ -56,10 +54,10 @@ async def read_environment(environment_id: str) -> EnvironmentRetrieved: return await controller.get_environment_by_id(environment_id) -@router.put("/{environment_id}") +@router.put("/{environment_id}", status_code=204) async def update_environment( environment_id: str, environment: EnvironmentModel -) -> EnvironmentUpdated: +) -> None: """ Updates an existing environment @@ -76,8 +74,8 @@ async def update_environment( ) -@router.delete("/{environment_id}") -async def delete_environment(environment_id: str) -> EnvironmentDeleted: +@router.delete("/{environment_id}", status_code=204) +async def delete_environment(environment_id: str) -> None: """ Deletes an existing environment diff --git a/lib/routes/flight.py b/lib/routes/flight.py index 6b84684..36961c9 100644 --- a/lib/routes/flight.py +++ b/lib/routes/flight.py @@ -9,8 +9,6 @@ FlightSimulation, FlightCreated, FlightRetrieved, - FlightUpdated, - FlightDeleted, ) from lib.models.environment import EnvironmentModel from lib.models.flight import FlightModel @@ -31,7 +29,7 @@ tracer = trace.get_tracer(__name__) -@router.post("/") +@router.post("/", status_code=201) async def create_flight( flight: FlightModel, motor_kind: MotorKinds ) -> FlightCreated: @@ -60,10 +58,10 @@ async def read_flight(flight_id: str) -> FlightRetrieved: return await controller.get_flight_by_id(flight_id) -@router.put("/{flight_id}") +@router.put("/{flight_id}", status_code=204) async def update_flight( flight_id: str, flight: FlightModel, motor_kind: MotorKinds -) -> FlightUpdated: +) -> None: """ Updates an existing flight @@ -79,8 +77,8 @@ async def update_flight( return await controller.put_flight_by_id(flight_id, flight) -@router.delete("/{flight_id}") -async def delete_flight(flight_id: str) -> FlightDeleted: +@router.delete("/{flight_id}", status_code=204) +async def delete_flight(flight_id: str) -> None: """ Deletes an existing flight @@ -125,10 +123,10 @@ async def get_rocketpy_flight_binary(flight_id: str): ) -@router.put("/{flight_id}/environment") +@router.put("/{flight_id}/environment", status_code=204) async def update_flight_environment( flight_id: str, environment: EnvironmentModel -) -> FlightUpdated: +) -> None: """ Updates flight environment @@ -145,12 +143,12 @@ async def update_flight_environment( ) -@router.put("/{flight_id}/rocket") +@router.put("/{flight_id}/rocket", status_code=204) async def update_flight_rocket( flight_id: str, rocket: RocketModel, motor_kind: MotorKinds, -) -> FlightUpdated: +) -> None: """ Updates flight rocket. diff --git a/lib/routes/motor.py b/lib/routes/motor.py index b5525a3..7806d6f 100644 --- a/lib/routes/motor.py +++ b/lib/routes/motor.py @@ -9,8 +9,6 @@ MotorSimulation, MotorCreated, MotorRetrieved, - MotorUpdated, - MotorDeleted, ) from lib.models.motor import MotorModel, MotorKinds from lib.controllers.motor import MotorController @@ -28,7 +26,7 @@ tracer = trace.get_tracer(__name__) -@router.post("/") +@router.post("/", status_code=201) async def create_motor( motor: MotorModel, motor_kind: MotorKinds ) -> MotorCreated: @@ -57,10 +55,10 @@ async def read_motor(motor_id: str) -> MotorRetrieved: return await controller.get_motor_by_id(motor_id) -@router.put("/{motor_id}") +@router.put("/{motor_id}", status_code=204) async def update_motor( motor_id: str, motor: MotorModel, motor_kind: MotorKinds -) -> MotorUpdated: +) -> None: """ Updates an existing motor @@ -76,8 +74,8 @@ async def update_motor( return await controller.put_motor_by_id(motor_id, motor) -@router.delete("/{motor_id}") -async def delete_motor(motor_id: str) -> MotorDeleted: +@router.delete("/{motor_id}", status_code=204) +async def delete_motor(motor_id: str) -> None: """ Deletes an existing motor diff --git a/lib/routes/rocket.py b/lib/routes/rocket.py index 1fa0e23..1de5740 100644 --- a/lib/routes/rocket.py +++ b/lib/routes/rocket.py @@ -9,8 +9,6 @@ RocketSimulation, RocketCreated, RocketRetrieved, - RocketUpdated, - RocketDeleted, ) from lib.models.rocket import RocketModel from lib.models.motor import MotorKinds @@ -29,7 +27,7 @@ tracer = trace.get_tracer(__name__) -@router.post("/") +@router.post("/", status_code=201) async def create_rocket( rocket: RocketModel, motor_kind: MotorKinds ) -> RocketCreated: @@ -58,10 +56,10 @@ async def read_rocket(rocket_id: str) -> RocketRetrieved: return await controller.get_rocket_by_id(rocket_id) -@router.put("/{rocket_id}") +@router.put("/{rocket_id}", status_code=204) async def update_rocket( rocket_id: str, rocket: RocketModel, motor_kind: MotorKinds -) -> RocketUpdated: +) -> None: """ Updates an existing rocket @@ -77,8 +75,8 @@ async def update_rocket( return await controller.put_rocket_by_id(rocket_id, rocket) -@router.delete("/{rocket_id}") -async def delete_rocket(rocket_id: str) -> RocketDeleted: +@router.delete("/{rocket_id}", status_code=204) +async def delete_rocket(rocket_id: str) -> None: """ Deletes an existing rocket diff --git a/lib/views/environment.py b/lib/views/environment.py index f3537b3..583a212 100644 --- a/lib/views/environment.py +++ b/lib/views/environment.py @@ -61,11 +61,3 @@ class EnvironmentCreated(ApiBaseView): class EnvironmentRetrieved(ApiBaseView): message: str = "Environment successfully retrieved" environment: EnvironmentView - - -class EnvironmentUpdated(ApiBaseView): - message: str = "Environment successfully updated" - - -class EnvironmentDeleted(ApiBaseView): - message: str = "Environment successfully deleted" diff --git a/lib/views/flight.py b/lib/views/flight.py index 9cbcae1..15dd93c 100644 --- a/lib/views/flight.py +++ b/lib/views/flight.py @@ -166,11 +166,3 @@ class FlightCreated(ApiBaseView): class FlightRetrieved(ApiBaseView): message: str = "Flight successfully retrieved" flight: FlightView - - -class FlightUpdated(ApiBaseView): - message: str = "Flight successfully updated" - - -class FlightDeleted(ApiBaseView): - message: str = "Flight successfully deleted" diff --git a/lib/views/motor.py b/lib/views/motor.py index 87759c4..121dd3c 100644 --- a/lib/views/motor.py +++ b/lib/views/motor.py @@ -82,11 +82,3 @@ class MotorCreated(ApiBaseView): class MotorRetrieved(ApiBaseView): message: str = "Motor successfully retrieved" motor: MotorView - - -class MotorUpdated(ApiBaseView): - message: str = "Motor successfully updated" - - -class MotorDeleted(ApiBaseView): - message: str = "Motor successfully deleted" diff --git a/lib/views/rocket.py b/lib/views/rocket.py index a81b98e..c19bab6 100644 --- a/lib/views/rocket.py +++ b/lib/views/rocket.py @@ -51,11 +51,3 @@ class RocketCreated(ApiBaseView): class RocketRetrieved(ApiBaseView): message: str = "Rocket successfully retrieved" rocket: RocketView - - -class RocketUpdated(ApiBaseView): - message: str = "Rocket successfully updated" - - -class RocketDeleted(ApiBaseView): - message: str = "Rocket successfully deleted" diff --git a/tests/unit/test_routes/test_environments_route.py b/tests/unit/test_routes/test_environments_route.py index 3995168..0711bc3 100644 --- a/tests/unit/test_routes/test_environments_route.py +++ b/tests/unit/test_routes/test_environments_route.py @@ -7,9 +7,7 @@ from lib.views.environment import ( EnvironmentView, EnvironmentCreated, - EnvironmentUpdated, EnvironmentRetrieved, - EnvironmentDeleted, EnvironmentSimulation, ) from lib import app @@ -45,7 +43,7 @@ def test_create_environment(stub_environment_dump, mock_controller_instance): ) mock_controller_instance.post_environment = mock_response response = client.post('/environments/', json=stub_environment_dump) - assert response.status_code == 200 + assert response.status_code == 201 assert response.json() == { 'environment_id': '123', 'message': 'Environment successfully created', @@ -73,7 +71,7 @@ def test_create_environment_optional_params( ) mock_controller_instance.post_environment = mock_response response = client.post('/environments/', json=stub_environment_dump) - assert response.status_code == 200 + assert response.status_code == 201 assert response.json() == { 'environment_id': '123', 'message': 'Environment successfully created', @@ -136,15 +134,10 @@ def test_read_environment_server_error(mock_controller_instance): def test_update_environment(stub_environment_dump, mock_controller_instance): - mock_reponse = AsyncMock( - return_value=EnvironmentUpdated(environment_id='123') - ) + mock_reponse = AsyncMock(return_value=None) mock_controller_instance.put_environment_by_id = mock_reponse response = client.put('/environments/123', json=stub_environment_dump) - assert response.status_code == 200 - assert response.json() == { - 'message': 'Environment successfully updated', - } + assert response.status_code == 204 mock_controller_instance.put_environment_by_id.assert_called_once_with( '123', EnvironmentModel(**stub_environment_dump) ) @@ -181,15 +174,10 @@ def test_update_environment_server_error( def test_delete_environment(mock_controller_instance): - mock_reponse = AsyncMock( - return_value=EnvironmentDeleted(environment_id='123') - ) + mock_reponse = AsyncMock(return_value=None) mock_controller_instance.delete_environment_by_id = mock_reponse response = client.delete('/environments/123') - assert response.status_code == 200 - assert response.json() == { - 'message': 'Environment successfully deleted', - } + assert response.status_code == 204 mock_controller_instance.delete_environment_by_id.assert_called_once_with( '123' ) diff --git a/tests/unit/test_routes/test_flights_route.py b/tests/unit/test_routes/test_flights_route.py index bc112ca..8775b6e 100644 --- a/tests/unit/test_routes/test_flights_route.py +++ b/tests/unit/test_routes/test_flights_route.py @@ -11,9 +11,7 @@ from lib.views.rocket import RocketView from lib.views.flight import ( FlightCreated, - FlightUpdated, FlightRetrieved, - FlightDeleted, FlightSimulation, FlightView, ) @@ -69,7 +67,7 @@ def test_create_flight(stub_flight_dump, mock_controller_instance): response = client.post( '/flights/', json=stub_flight_dump, params={'motor_kind': 'HYBRID'} ) - assert response.status_code == 200 + assert response.status_code == 201 assert response.json() == { 'flight_id': '123', 'message': 'Flight successfully created', @@ -103,7 +101,7 @@ def test_create_flight_optional_params( response = client.post( '/flights/', json=stub_flight_dump, params={'motor_kind': 'HYBRID'} ) - assert response.status_code == 200 + assert response.status_code == 201 assert response.json() == { 'flight_id': '123', 'message': 'Flight successfully created', @@ -181,7 +179,7 @@ def test_read_flight_server_error(mock_controller_instance): def test_update_flight_by_id(stub_flight_dump, mock_controller_instance): - mock_response = AsyncMock(return_value=FlightUpdated(flight_id='123')) + mock_response = AsyncMock(return_value=None) mock_controller_instance.put_flight_by_id = mock_response with patch.object( MotorModel, 'set_motor_kind', side_effect=None @@ -191,10 +189,7 @@ def test_update_flight_by_id(stub_flight_dump, mock_controller_instance): json=stub_flight_dump, params={'motor_kind': 'HYBRID'}, ) - assert response.status_code == 200 - assert response.json() == { - 'message': 'Flight successfully updated', - } + assert response.status_code == 204 mock_set_motor_kind.assert_called_once_with(MotorKinds.HYBRID) mock_controller_instance.put_flight_by_id.assert_called_once_with( '123', FlightModel(**stub_flight_dump) @@ -204,15 +199,12 @@ def test_update_flight_by_id(stub_flight_dump, mock_controller_instance): def test_update_environment_by_flight_id( stub_environment_dump, mock_controller_instance ): - mock_response = AsyncMock(return_value=FlightUpdated(flight_id='123')) + mock_response = AsyncMock(return_value=None) mock_controller_instance.update_environment_by_flight_id = mock_response response = client.put( '/flights/123/environment', json=stub_environment_dump ) - assert response.status_code == 200 - assert response.json() == { - 'message': 'Flight successfully updated', - } + assert response.status_code == 204 mock_controller_instance.update_environment_by_flight_id.assert_called_once_with( '123', environment=EnvironmentModel(**stub_environment_dump) ) @@ -221,7 +213,7 @@ def test_update_environment_by_flight_id( def test_update_rocket_by_flight_id( stub_rocket_dump, mock_controller_instance ): - mock_response = AsyncMock(return_value=FlightUpdated(flight_id='123')) + mock_response = AsyncMock(return_value=None) mock_controller_instance.update_rocket_by_flight_id = mock_response with patch.object( MotorModel, 'set_motor_kind', side_effect=None @@ -231,10 +223,7 @@ def test_update_rocket_by_flight_id( json=stub_rocket_dump, params={'motor_kind': 'HYBRID'}, ) - assert response.status_code == 200 - assert response.json() == { - 'message': 'Flight successfully updated', - } + assert response.status_code == 204 mock_set_motor_kind.assert_called_once_with(MotorKinds.HYBRID) mock_controller_instance.update_rocket_by_flight_id.assert_called_once_with( '123', rocket=RocketModel(**stub_rocket_dump) @@ -345,13 +334,10 @@ def test_update_rocket_by_flight_id_server_error( def test_delete_flight(mock_controller_instance): - mock_response = AsyncMock(return_value=FlightDeleted(flight_id='123')) + mock_response = AsyncMock(return_value=None) mock_controller_instance.delete_flight_by_id = mock_response response = client.delete('/flights/123') - assert response.status_code == 200 - assert response.json() == { - 'message': 'Flight successfully deleted', - } + assert response.status_code == 204 mock_controller_instance.delete_flight_by_id.assert_called_once_with('123') diff --git a/tests/unit/test_routes/test_motors_route.py b/tests/unit/test_routes/test_motors_route.py index ee4abe2..79ca3b3 100644 --- a/tests/unit/test_routes/test_motors_route.py +++ b/tests/unit/test_routes/test_motors_route.py @@ -10,8 +10,6 @@ from lib.views.motor import ( MotorCreated, MotorRetrieved, - MotorUpdated, - MotorDeleted, MotorSimulation, MotorView, ) @@ -51,7 +49,7 @@ def test_create_motor(stub_motor_dump, mock_controller_instance): response = client.post( '/motors/', json=stub_motor_dump, params={'motor_kind': 'HYBRID'} ) - assert response.status_code == 200 + assert response.status_code == 201 assert response.json() == { 'motor_id': '123', 'message': 'Motor successfully created', @@ -80,7 +78,7 @@ def test_create_motor_optional_params( response = client.post( '/motors/', json=stub_motor_dump, params={'motor_kind': 'HYBRID'} ) - assert response.status_code == 200 + assert response.status_code == 201 assert response.json() == { 'motor_id': '123', 'message': 'Motor successfully created', @@ -109,7 +107,7 @@ def test_create_generic_motor(stub_motor_dump, mock_controller_instance): response = client.post( '/motors/', json=stub_motor_dump, params={'motor_kind': 'GENERIC'} ) - assert response.status_code == 200 + assert response.status_code == 201 assert response.json() == { 'motor_id': '123', 'message': 'Motor successfully created', @@ -132,7 +130,7 @@ def test_create_liquid_motor_level_tank( response = client.post( '/motors/', json=stub_motor_dump, params={'motor_kind': 'LIQUID'} ) - assert response.status_code == 200 + assert response.status_code == 201 assert response.json() == { 'motor_id': '123', 'message': 'Motor successfully created', @@ -155,7 +153,7 @@ def test_create_liquid_motor_mass_flow_tank( response = client.post( '/motors/', json=stub_motor_dump, params={'motor_kind': 'LIQUID'} ) - assert response.status_code == 200 + assert response.status_code == 201 assert response.json() == { 'motor_id': '123', 'message': 'Motor successfully created', @@ -178,7 +176,7 @@ def test_create_liquid_motor_ullage_tank( response = client.post( '/motors/', json=stub_motor_dump, params={'motor_kind': 'LIQUID'} ) - assert response.status_code == 200 + assert response.status_code == 201 assert response.json() == { 'motor_id': '123', 'message': 'Motor successfully created', @@ -201,7 +199,7 @@ def test_create_liquid_motor_mass_tank( response = client.post( '/motors/', json=stub_motor_dump, params={'motor_kind': 'LIQUID'} ) - assert response.status_code == 200 + assert response.status_code == 201 assert response.json() == { 'motor_id': '123', 'message': 'Motor successfully created', @@ -236,7 +234,7 @@ def test_create_hybrid_motor( response = client.post( '/motors/', json=stub_motor_dump, params={'motor_kind': 'HYBRID'} ) - assert response.status_code == 200 + assert response.status_code == 201 assert response.json() == { 'motor_id': '123', 'message': 'Motor successfully created', @@ -267,7 +265,7 @@ def test_create_solid_motor(stub_motor_dump, mock_controller_instance): response = client.post( '/motors/', json=stub_motor_dump, params={'motor_kind': 'SOLID'} ) - assert response.status_code == 200 + assert response.status_code == 201 assert response.json() == { 'motor_id': '123', 'message': 'Motor successfully created', @@ -333,7 +331,7 @@ def test_read_motor_server_error(mock_controller_instance): def test_update_motor(stub_motor_dump, mock_controller_instance): - mock_response = AsyncMock(return_value=MotorUpdated(motor_id='123')) + mock_response = AsyncMock(return_value=None) mock_controller_instance.put_motor_by_id = mock_response with patch.object( MotorModel, 'set_motor_kind', side_effect=None @@ -343,10 +341,7 @@ def test_update_motor(stub_motor_dump, mock_controller_instance): json=stub_motor_dump, params={'motor_kind': 'HYBRID'}, ) - assert response.status_code == 200 - assert response.json() == { - 'message': 'Motor successfully updated', - } + assert response.status_code == 204 mock_set_motor_kind.assert_called_once_with(MotorKinds.HYBRID) mock_controller_instance.put_motor_by_id.assert_called_once_with( '123', MotorModel(**stub_motor_dump) @@ -401,13 +396,10 @@ def test_update_motor_server_error(stub_motor_dump, mock_controller_instance): def test_delete_motor(mock_controller_instance): - mock_response = AsyncMock(return_value=MotorDeleted(motor_id='123')) + mock_response = AsyncMock(return_value=None) mock_controller_instance.delete_motor_by_id = mock_response response = client.delete('/motors/123') - assert response.status_code == 200 - assert response.json() == { - 'message': 'Motor successfully deleted', - } + assert response.status_code == 204 mock_controller_instance.delete_motor_by_id.assert_called_once_with('123') diff --git a/tests/unit/test_routes/test_rockets_route.py b/tests/unit/test_routes/test_rockets_route.py index ac99606..2e04481 100644 --- a/tests/unit/test_routes/test_rockets_route.py +++ b/tests/unit/test_routes/test_rockets_route.py @@ -15,9 +15,7 @@ ) from lib.views.rocket import ( RocketCreated, - RocketUpdated, RocketRetrieved, - RocketDeleted, RocketSimulation, RocketView, ) @@ -96,7 +94,7 @@ def test_create_rocket(stub_rocket_dump, mock_controller_instance): response = client.post( '/rockets/', json=stub_rocket_dump, params={'motor_kind': 'HYBRID'} ) - assert response.status_code == 200 + assert response.status_code == 201 assert response.json() == { 'rocket_id': '123', 'message': 'Rocket successfully created', @@ -129,7 +127,7 @@ def test_create_rocket_optional_params( response = client.post( '/rockets/', json=stub_rocket_dump, params={'motor_kind': 'HYBRID'} ) - assert response.status_code == 200 + assert response.status_code == 201 assert response.json() == { 'rocket_id': '123', 'message': 'Rocket successfully created', @@ -163,7 +161,7 @@ def test_create_generic_motor_rocket( json=stub_rocket_dump, params={'motor_kind': 'GENERIC'}, ) - assert response.status_code == 200 + assert response.status_code == 201 assert response.json() == { 'rocket_id': '123', 'message': 'Rocket successfully created', @@ -190,7 +188,7 @@ def test_create_liquid_motor_level_tank_rocket( response = client.post( '/rockets/', json=stub_rocket_dump, params={'motor_kind': 'LIQUID'} ) - assert response.status_code == 200 + assert response.status_code == 201 assert response.json() == { 'rocket_id': '123', 'message': 'Rocket successfully created', @@ -217,7 +215,7 @@ def test_create_liquid_motor_mass_flow_tank_rocket( response = client.post( '/rockets/', json=stub_rocket_dump, params={'motor_kind': 'LIQUID'} ) - assert response.status_code == 200 + assert response.status_code == 201 assert response.json() == { 'rocket_id': '123', 'message': 'Rocket successfully created', @@ -244,7 +242,7 @@ def test_create_liquid_motor_ullage_tank_rocket( response = client.post( '/rockets/', json=stub_rocket_dump, params={'motor_kind': 'LIQUID'} ) - assert response.status_code == 200 + assert response.status_code == 201 assert response.json() == { 'rocket_id': '123', 'message': 'Rocket successfully created', @@ -271,7 +269,7 @@ def test_create_liquid_motor_mass_tank_rocket( response = client.post( '/rockets/', json=stub_rocket_dump, params={'motor_kind': 'LIQUID'} ) - assert response.status_code == 200 + assert response.status_code == 201 assert response.json() == { 'rocket_id': '123', 'message': 'Rocket successfully created', @@ -310,7 +308,7 @@ def test_create_hybrid_motor_rocket( response = client.post( '/rockets/', json=stub_rocket_dump, params={'motor_kind': 'HYBRID'} ) - assert response.status_code == 200 + assert response.status_code == 201 assert response.json() == { 'rocket_id': '123', 'message': 'Rocket successfully created', @@ -344,7 +342,7 @@ def test_create_solid_motor_rocket( response = client.post( '/rockets/', json=stub_rocket_dump, params={'motor_kind': 'SOLID'} ) - assert response.status_code == 200 + assert response.status_code == 201 assert response.json() == { 'rocket_id': '123', 'message': 'Rocket successfully created', @@ -408,7 +406,7 @@ def test_read_rocket_server_error(mock_controller_instance): def test_update_rocket(stub_rocket_dump, mock_controller_instance): - mock_response = AsyncMock(return_value=RocketUpdated(rocket_id='123')) + mock_response = AsyncMock(return_value=None) mock_controller_instance.put_rocket_by_id = mock_response with patch.object( MotorModel, 'set_motor_kind', side_effect=None @@ -418,10 +416,7 @@ def test_update_rocket(stub_rocket_dump, mock_controller_instance): json=stub_rocket_dump, params={'motor_kind': 'HYBRID'}, ) - assert response.status_code == 200 - assert response.json() == { - 'message': 'Rocket successfully updated', - } + assert response.status_code == 204 mock_set_motor_kind.assert_called_once_with(MotorKinds.HYBRID) mock_controller_instance.put_rocket_by_id.assert_called_once_with( '123', RocketModel(**stub_rocket_dump) @@ -466,13 +461,10 @@ def test_update_rocket_server_error( def test_delete_rocket(mock_controller_instance): - mock_response = AsyncMock(return_value=RocketDeleted(rocket_id='123')) + mock_response = AsyncMock(return_value=None) mock_controller_instance.delete_rocket_by_id = mock_response response = client.delete('/rockets/123') - assert response.status_code == 200 - assert response.json() == { - 'message': 'Rocket successfully deleted', - } + assert response.status_code == 204 mock_controller_instance.delete_rocket_by_id.assert_called_once_with('123') From df6c4c2afd43d07914811f2402261be519db2e8f Mon Sep 17 00:00:00 2001 From: Gabriel Barberini Date: Mon, 17 Feb 2025 19:06:32 -0300 Subject: [PATCH 16/34] change lib to src --- Dockerfile | 4 +- Makefile | 10 ++--- README.md | 4 +- lib/controllers/__init__.py | 1 - lib/repositories/__init__.py | 1 - {lib => src}/__init__.py | 4 +- {lib => src}/__main__.py | 2 +- {lib => src}/api.py | 6 +-- src/controllers/__init__.py | 1 + {lib => src}/controllers/environment.py | 8 ++-- {lib => src}/controllers/flight.py | 12 +++--- {lib => src}/controllers/interface.py | 8 ++-- {lib => src}/controllers/motor.py | 8 ++-- {lib => src}/controllers/rocket.py | 8 ++-- {lib => src}/models/environment.py | 6 +-- {lib => src}/models/flight.py | 10 ++--- {lib => src}/models/interface.py | 0 {lib => src}/models/motor.py | 8 ++-- {lib => src}/models/rocket.py | 10 ++--- {lib => src}/models/sub/aerosurfaces.py | 0 {lib => src}/models/sub/tanks.py | 0 src/repositories/__init__.py | 1 + {lib => src}/repositories/environment.py | 4 +- {lib => src}/repositories/flight.py | 4 +- {lib => src}/repositories/interface.py | 6 +-- {lib => src}/repositories/motor.py | 4 +- {lib => src}/repositories/rocket.py | 4 +- {lib => src}/routes/environment.py | 6 +-- {lib => src}/routes/flight.py | 12 +++--- {lib => src}/routes/motor.py | 6 +-- {lib => src}/routes/rocket.py | 8 ++-- {lib => src}/secrets.py | 0 {lib => src}/services/environment.py | 4 +- {lib => src}/services/flight.py | 8 ++-- {lib => src}/services/motor.py | 6 +-- {lib => src}/services/rocket.py | 10 ++--- {lib => src}/settings/gunicorn.py | 4 +- {lib => src}/utils.py | 0 {lib => src}/views/environment.py | 6 +-- {lib => src}/views/flight.py | 10 ++--- {lib => src}/views/interface.py | 0 {lib => src}/views/motor.py | 6 +-- {lib => src}/views/rocket.py | 8 ++-- .../test_controller_interface.py | 38 +++++++++---------- .../test_repository_interface.py | 32 ++++++++-------- tests/unit/test_routes/conftest.py | 10 ++--- .../test_routes/test_environments_route.py | 8 ++-- tests/unit/test_routes/test_flights_route.py | 18 ++++----- tests/unit/test_routes/test_motors_route.py | 8 ++-- tests/unit/test_routes/test_rockets_route.py | 12 +++--- 50 files changed, 177 insertions(+), 177 deletions(-) delete mode 100644 lib/controllers/__init__.py delete mode 100644 lib/repositories/__init__.py rename {lib => src}/__init__.py (89%) rename {lib => src}/__main__.py (77%) rename {lib => src}/api.py (95%) create mode 100644 src/controllers/__init__.py rename {lib => src}/controllers/environment.py (88%) rename {lib => src}/controllers/flight.py (91%) rename {lib => src}/controllers/interface.py (95%) rename {lib => src}/controllers/motor.py (89%) rename {lib => src}/controllers/rocket.py (89%) rename {lib => src}/models/environment.py (87%) rename {lib => src}/models/flight.py (86%) rename {lib => src}/models/interface.py (100%) rename {lib => src}/models/motor.py (93%) rename {lib => src}/models/rocket.py (85%) rename {lib => src}/models/sub/aerosurfaces.py (100%) rename {lib => src}/models/sub/tanks.py (100%) create mode 100644 src/repositories/__init__.py rename {lib => src}/repositories/environment.py (92%) rename {lib => src}/repositories/flight.py (92%) rename {lib => src}/repositories/interface.py (98%) rename {lib => src}/repositories/motor.py (92%) rename {lib => src}/repositories/rocket.py (92%) rename {lib => src}/routes/environment.py (95%) rename {lib => src}/routes/flight.py (94%) rename {lib => src}/routes/motor.py (96%) rename {lib => src}/routes/rocket.py (95%) rename {lib => src}/secrets.py (100%) rename {lib => src}/services/environment.py (94%) rename {lib => src}/services/flight.py (90%) rename {lib => src}/services/motor.py (97%) rename {lib => src}/services/rocket.py (96%) rename {lib => src}/settings/gunicorn.py (82%) rename {lib => src}/utils.py (100%) rename {lib => src}/views/environment.py (94%) rename {lib => src}/views/flight.py (96%) rename {lib => src}/views/interface.py (100%) rename {lib => src}/views/motor.py (96%) rename {lib => src}/views/rocket.py (91%) diff --git a/Dockerfile b/Dockerfile index 191bffe..ce53e71 100644 --- a/Dockerfile +++ b/Dockerfile @@ -14,6 +14,6 @@ RUN apt-get update && \ apt-get purge -y --auto-remove && \ rm -rf /var/lib/apt/lists/* -COPY ./lib /app/lib +COPY ./src /app/src -CMD ["gunicorn", "-c", "lib/settings/gunicorn.py", "-w", "1", "--threads=2", "-k", "uvicorn.workers.UvicornWorker", "lib.api:app", "--log-level", "Debug", "-b", "0.0.0.0:3000", "--timeout", "60"] +CMD ["gunicorn", "-c", "src/settings/gunicorn.py", "-w", "1", "--threads=2", "-k", "uvicorn.workers.UvicornWorker", "src.api:app", "--log-level", "Debug", "-b", "0.0.0.0:3000", "--timeout", "60"] diff --git a/Makefile b/Makefile index 815840e..a2ee8d9 100644 --- a/Makefile +++ b/Makefile @@ -2,26 +2,26 @@ format: black ruff lint: flake8 pylint black: - black ./lib || true + black ./src || true black ./tests || true flake8: - flake8 --ignore E501,E402,F401,W503,C0414 ./lib || true + flake8 --ignore E501,E402,F401,W503,C0414 ./src || true flake8 --ignore E501,E402,F401,W503,C0414 ./tests || true pylint: - pylint ./lib || true + pylint ./src || true pylint ./tests || true ruff: - ruff check --fix ./lib || true + ruff check --fix ./src || true ruff check --fix ./tests || true test: python3 -m pytest . dev: - python3 -m uvicorn lib:app --reload --port 3000 + python3 -m uvicorn src.app --reload --port 3000 clean: docker stop infinity-api diff --git a/README.md b/README.md index 8772372..709dc50 100644 --- a/README.md +++ b/README.md @@ -25,8 +25,8 @@ $ touch .env && echo MONGODB_CONNECTION_STRING="$ConnectionString" > .env - run docker compose: `docker-compose up --build -d` ### Standalone -- Dev: `python3 -m uvicorn lib:app --reload --port 3000` -- Prod: `gunicorn -k uvicorn.workers.UvicornWorker lib:app -b 0.0.0.0:3000` +- Dev: `python3 -m uvicorn src:app --reload --port 3000` +- Prod: `gunicorn -k uvicorn.workers.UvicornWorker src:app -b 0.0.0.0:3000` ## Project structure ``` diff --git a/lib/controllers/__init__.py b/lib/controllers/__init__.py deleted file mode 100644 index c8add6a..0000000 --- a/lib/controllers/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# lib/controllers/__init__.py diff --git a/lib/repositories/__init__.py b/lib/repositories/__init__.py deleted file mode 100644 index c8add6a..0000000 --- a/lib/repositories/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# lib/controllers/__init__.py diff --git a/lib/__init__.py b/src/__init__.py similarity index 89% rename from lib/__init__.py rename to src/__init__.py index 4eecb2e..38835c1 100644 --- a/lib/__init__.py +++ b/src/__init__.py @@ -1,4 +1,4 @@ -# lib/__init__.py +# src.__init__.py import logging import sys @@ -25,6 +25,6 @@ def parse_error(error): return f"{exc_type}: {exc_obj}" -from lib.api import ( # pylint: disable=wrong-import-position,cyclic-import,useless-import-alias +from src.api import ( # pylint: disable=wrong-import-position,cyclic-import,useless-import-alias app as app, ) diff --git a/lib/__main__.py b/src/__main__.py similarity index 77% rename from lib/__main__.py rename to src/__main__.py index 38e9fef..a6937c4 100644 --- a/lib/__main__.py +++ b/src/__main__.py @@ -1,5 +1,5 @@ # __main__.py -from lib.api import app +from src.api import app if __name__ == '__main__': app.run() # pylint: disable=no-member diff --git a/lib/api.py b/src/api.py similarity index 95% rename from lib/api.py rename to src/api.py index 2ee22bf..5b97be0 100644 --- a/lib/api.py +++ b/src/api.py @@ -6,9 +6,9 @@ from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor from opentelemetry.instrumentation.requests import RequestsInstrumentor -from lib import logger, parse_error -from lib.routes import flight, environment, motor, rocket -from lib.utils import RocketPyGZipMiddleware +from src import logger, parse_error +from src.routes import flight, environment, motor, rocket +from src.utils import RocketPyGZipMiddleware app = FastAPI( swagger_ui_parameters={ diff --git a/src/controllers/__init__.py b/src/controllers/__init__.py new file mode 100644 index 0000000..9eaeadf --- /dev/null +++ b/src/controllers/__init__.py @@ -0,0 +1 @@ +# src.controllers/__init__.py diff --git a/lib/controllers/environment.py b/src/controllers/environment.py similarity index 88% rename from lib/controllers/environment.py rename to src/controllers/environment.py index 8a55e3a..8fb4bfe 100644 --- a/lib/controllers/environment.py +++ b/src/controllers/environment.py @@ -1,10 +1,10 @@ -from lib.controllers.interface import ( +from src.controllers.interface import ( ControllerInterface, controller_exception_handler, ) -from lib.views.environment import EnvironmentSimulation -from lib.models.environment import EnvironmentModel -from lib.services.environment import EnvironmentService +from src.views.environment import EnvironmentSimulation +from src.models.environment import EnvironmentModel +from src.services.environment import EnvironmentService class EnvironmentController(ControllerInterface): diff --git a/lib/controllers/flight.py b/src/controllers/flight.py similarity index 91% rename from lib/controllers/flight.py rename to src/controllers/flight.py index 181e38f..6f9dbbb 100644 --- a/lib/controllers/flight.py +++ b/src/controllers/flight.py @@ -1,12 +1,12 @@ -from lib.controllers.interface import ( +from src.controllers.interface import ( ControllerInterface, controller_exception_handler, ) -from lib.views.flight import FlightSimulation -from lib.models.flight import FlightModel -from lib.models.environment import EnvironmentModel -from lib.models.rocket import RocketModel -from lib.services.flight import FlightService +from src.views.flight import FlightSimulation +from src.models.flight import FlightModel +from src.models.environment import EnvironmentModel +from src.models.rocket import RocketModel +from src.services.flight import FlightService class FlightController(ControllerInterface): diff --git a/lib/controllers/interface.py b/src/controllers/interface.py similarity index 95% rename from lib/controllers/interface.py rename to src/controllers/interface.py index acd8105..83ad164 100644 --- a/lib/controllers/interface.py +++ b/src/controllers/interface.py @@ -3,10 +3,10 @@ from pymongo.errors import PyMongoError from fastapi import HTTPException, status -from lib import logger -from lib.models.interface import ApiBaseModel -from lib.views.interface import ApiBaseView -from lib.repositories.interface import RepositoryInterface +from src import logger +from src.models.interface import ApiBaseModel +from src.views.interface import ApiBaseView +from src.repositories.interface import RepositoryInterface def controller_exception_handler(method): diff --git a/lib/controllers/motor.py b/src/controllers/motor.py similarity index 89% rename from lib/controllers/motor.py rename to src/controllers/motor.py index a030922..dbd2980 100644 --- a/lib/controllers/motor.py +++ b/src/controllers/motor.py @@ -1,10 +1,10 @@ -from lib.controllers.interface import ( +from src.controllers.interface import ( ControllerInterface, controller_exception_handler, ) -from lib.views.motor import MotorSimulation -from lib.models.motor import MotorModel -from lib.services.motor import MotorService +from src.views.motor import MotorSimulation +from src.models.motor import MotorModel +from src.services.motor import MotorService class MotorController(ControllerInterface): diff --git a/lib/controllers/rocket.py b/src/controllers/rocket.py similarity index 89% rename from lib/controllers/rocket.py rename to src/controllers/rocket.py index 6f305e6..80c98d3 100644 --- a/lib/controllers/rocket.py +++ b/src/controllers/rocket.py @@ -1,10 +1,10 @@ -from lib.controllers.interface import ( +from src.controllers.interface import ( ControllerInterface, controller_exception_handler, ) -from lib.views.rocket import RocketSimulation -from lib.models.rocket import RocketModel -from lib.services.rocket import RocketService +from src.views.rocket import RocketSimulation +from src.models.rocket import RocketModel +from src.services.rocket import RocketService class RocketController(ControllerInterface): diff --git a/lib/models/environment.py b/src/models/environment.py similarity index 87% rename from lib/models/environment.py rename to src/models/environment.py index e77bceb..3682dd0 100644 --- a/lib/models/environment.py +++ b/src/models/environment.py @@ -1,6 +1,6 @@ import datetime from typing import Optional, ClassVar, Self, Literal -from lib.models.interface import ApiBaseModel +from src.models.interface import ApiBaseModel class EnvironmentModel(ApiBaseModel): @@ -34,13 +34,13 @@ def DELETED(): @staticmethod def CREATED(model_id: str): - from lib.views.environment import EnvironmentCreated + from src.views.environment import EnvironmentCreated return EnvironmentCreated(environment_id=model_id) @staticmethod def RETRIEVED(model_instance: type(Self)): - from lib.views.environment import EnvironmentRetrieved, EnvironmentView + from src.views.environment import EnvironmentRetrieved, EnvironmentView return EnvironmentRetrieved( environment=EnvironmentView( diff --git a/lib/models/flight.py b/src/models/flight.py similarity index 86% rename from lib/models/flight.py rename to src/models/flight.py index a31f377..8b17d47 100644 --- a/lib/models/flight.py +++ b/src/models/flight.py @@ -1,7 +1,7 @@ from typing import Optional, Self, ClassVar, Literal -from lib.models.interface import ApiBaseModel -from lib.models.rocket import RocketModel -from lib.models.environment import EnvironmentModel +from src.models.interface import ApiBaseModel +from src.models.rocket import RocketModel +from src.models.environment import EnvironmentModel class FlightModel(ApiBaseModel): @@ -54,13 +54,13 @@ def DELETED(): @staticmethod def CREATED(model_id: str): - from lib.views.flight import FlightCreated + from src.views.flight import FlightCreated return FlightCreated(flight_id=model_id) @staticmethod def RETRIEVED(model_instance: type(Self)): - from lib.views.flight import FlightRetrieved, FlightView + from src.views.flight import FlightRetrieved, FlightView return FlightRetrieved( flight=FlightView( diff --git a/lib/models/interface.py b/src/models/interface.py similarity index 100% rename from lib/models/interface.py rename to src/models/interface.py diff --git a/lib/models/motor.py b/src/models/motor.py similarity index 93% rename from lib/models/motor.py rename to src/models/motor.py index d5d6596..8718fb5 100644 --- a/lib/models/motor.py +++ b/src/models/motor.py @@ -2,8 +2,8 @@ from typing import Optional, Tuple, List, Union, Self, ClassVar, Literal from pydantic import PrivateAttr, model_validator, computed_field -from lib.models.interface import ApiBaseModel -from lib.models.sub.tanks import MotorTank +from src.models.interface import ApiBaseModel +from src.models.sub.tanks import MotorTank class MotorKinds(str, Enum): @@ -90,13 +90,13 @@ def DELETED(): @staticmethod def CREATED(model_id: str): - from lib.views.motor import MotorCreated + from src.views.motor import MotorCreated return MotorCreated(motor_id=model_id) @staticmethod def RETRIEVED(model_instance: type(Self)): - from lib.views.motor import MotorRetrieved, MotorView + from src.views.motor import MotorRetrieved, MotorView return MotorRetrieved( motor=MotorView( diff --git a/lib/models/rocket.py b/src/models/rocket.py similarity index 85% rename from lib/models/rocket.py rename to src/models/rocket.py index 4947057..c2f53a3 100644 --- a/lib/models/rocket.py +++ b/src/models/rocket.py @@ -1,7 +1,7 @@ from typing import Optional, Tuple, List, Union, Self, ClassVar, Literal -from lib.models.interface import ApiBaseModel -from lib.models.motor import MotorModel -from lib.models.sub.aerosurfaces import ( +from src.models.interface import ApiBaseModel +from src.models.motor import MotorModel +from src.models.sub.aerosurfaces import ( Fins, NoseCone, Tail, @@ -47,13 +47,13 @@ def DELETED(): @staticmethod def CREATED(model_id: str): - from lib.views.rocket import RocketCreated + from src.views.rocket import RocketCreated return RocketCreated(rocket_id=model_id) @staticmethod def RETRIEVED(model_instance: type(Self)): - from lib.views.rocket import RocketRetrieved, RocketView + from src.views.rocket import RocketRetrieved, RocketView return RocketRetrieved( rocket=RocketView( diff --git a/lib/models/sub/aerosurfaces.py b/src/models/sub/aerosurfaces.py similarity index 100% rename from lib/models/sub/aerosurfaces.py rename to src/models/sub/aerosurfaces.py diff --git a/lib/models/sub/tanks.py b/src/models/sub/tanks.py similarity index 100% rename from lib/models/sub/tanks.py rename to src/models/sub/tanks.py diff --git a/src/repositories/__init__.py b/src/repositories/__init__.py new file mode 100644 index 0000000..41576ce --- /dev/null +++ b/src/repositories/__init__.py @@ -0,0 +1 @@ +# src/controllers/__init__.py diff --git a/lib/repositories/environment.py b/src/repositories/environment.py similarity index 92% rename from lib/repositories/environment.py rename to src/repositories/environment.py index 5f30911..6f483b3 100644 --- a/lib/repositories/environment.py +++ b/src/repositories/environment.py @@ -1,6 +1,6 @@ from typing import Optional -from lib.models.environment import EnvironmentModel -from lib.repositories.interface import ( +from src.models.environment import EnvironmentModel +from src.repositories.interface import ( RepositoryInterface, repository_exception_handler, ) diff --git a/lib/repositories/flight.py b/src/repositories/flight.py similarity index 92% rename from lib/repositories/flight.py rename to src/repositories/flight.py index fbe6fc5..3b3448b 100644 --- a/lib/repositories/flight.py +++ b/src/repositories/flight.py @@ -1,6 +1,6 @@ from typing import Optional -from lib.models.flight import FlightModel -from lib.repositories.interface import ( +from src.models.flight import FlightModel +from src.repositories.interface import ( RepositoryInterface, repository_exception_handler, ) diff --git a/lib/repositories/interface.py b/src/repositories/interface.py similarity index 98% rename from lib/repositories/interface.py rename to src/repositories/interface.py index 4242da6..dcfa619 100644 --- a/lib/repositories/interface.py +++ b/src/repositories/interface.py @@ -16,9 +16,9 @@ from fastapi import HTTPException, status from bson import ObjectId -from lib import logger -from lib.secrets import Secrets -from lib.models.interface import ApiBaseModel +from src import logger +from src.secrets import Secrets +from src.models.interface import ApiBaseModel def not_implemented(*args, **kwargs): diff --git a/lib/repositories/motor.py b/src/repositories/motor.py similarity index 92% rename from lib/repositories/motor.py rename to src/repositories/motor.py index 452940d..026655f 100644 --- a/lib/repositories/motor.py +++ b/src/repositories/motor.py @@ -1,6 +1,6 @@ from typing import Optional -from lib.models.motor import MotorModel -from lib.repositories.interface import ( +from src.models.motor import MotorModel +from src.repositories.interface import ( RepositoryInterface, repository_exception_handler, ) diff --git a/lib/repositories/rocket.py b/src/repositories/rocket.py similarity index 92% rename from lib/repositories/rocket.py rename to src/repositories/rocket.py index 0b7923c..f4b6f38 100644 --- a/lib/repositories/rocket.py +++ b/src/repositories/rocket.py @@ -1,6 +1,6 @@ from typing import Optional -from lib.models.rocket import RocketModel -from lib.repositories.interface import ( +from src.models.rocket import RocketModel +from src.repositories.interface import ( RepositoryInterface, repository_exception_handler, ) diff --git a/lib/routes/environment.py b/src/routes/environment.py similarity index 95% rename from lib/routes/environment.py rename to src/routes/environment.py index 9249091..a343df8 100644 --- a/lib/routes/environment.py +++ b/src/routes/environment.py @@ -5,13 +5,13 @@ from fastapi import APIRouter, Response from opentelemetry import trace -from lib.views.environment import ( +from src.views.environment import ( EnvironmentSimulation, EnvironmentCreated, EnvironmentRetrieved, ) -from lib.models.environment import EnvironmentModel -from lib.controllers.environment import EnvironmentController +from src.models.environment import EnvironmentModel +from src.controllers.environment import EnvironmentController router = APIRouter( prefix="/environments", diff --git a/lib/routes/flight.py b/src/routes/flight.py similarity index 94% rename from lib/routes/flight.py rename to src/routes/flight.py index 36961c9..5bb6af0 100644 --- a/lib/routes/flight.py +++ b/src/routes/flight.py @@ -5,16 +5,16 @@ from fastapi import APIRouter, Response from opentelemetry import trace -from lib.views.flight import ( +from src.views.flight import ( FlightSimulation, FlightCreated, FlightRetrieved, ) -from lib.models.environment import EnvironmentModel -from lib.models.flight import FlightModel -from lib.models.rocket import RocketModel -from lib.models.motor import MotorKinds -from lib.controllers.flight import FlightController +from src.models.environment import EnvironmentModel +from src.models.flight import FlightModel +from src.models.rocket import RocketModel +from src.models.motor import MotorKinds +from src.controllers.flight import FlightController router = APIRouter( prefix="/flights", diff --git a/lib/routes/motor.py b/src/routes/motor.py similarity index 96% rename from lib/routes/motor.py rename to src/routes/motor.py index 7806d6f..32254f4 100644 --- a/lib/routes/motor.py +++ b/src/routes/motor.py @@ -5,13 +5,13 @@ from fastapi import APIRouter, Response from opentelemetry import trace -from lib.views.motor import ( +from src.views.motor import ( MotorSimulation, MotorCreated, MotorRetrieved, ) -from lib.models.motor import MotorModel, MotorKinds -from lib.controllers.motor import MotorController +from src.models.motor import MotorModel, MotorKinds +from src.controllers.motor import MotorController router = APIRouter( prefix="/motors", diff --git a/lib/routes/rocket.py b/src/routes/rocket.py similarity index 95% rename from lib/routes/rocket.py rename to src/routes/rocket.py index 1de5740..67457d2 100644 --- a/lib/routes/rocket.py +++ b/src/routes/rocket.py @@ -5,14 +5,14 @@ from fastapi import APIRouter, Response from opentelemetry import trace -from lib.views.rocket import ( +from src.views.rocket import ( RocketSimulation, RocketCreated, RocketRetrieved, ) -from lib.models.rocket import RocketModel -from lib.models.motor import MotorKinds -from lib.controllers.rocket import RocketController +from src.models.rocket import RocketModel +from src.models.motor import MotorKinds +from src.controllers.rocket import RocketController router = APIRouter( prefix="/rockets", diff --git a/lib/secrets.py b/src/secrets.py similarity index 100% rename from lib/secrets.py rename to src/secrets.py diff --git a/lib/services/environment.py b/src/services/environment.py similarity index 94% rename from lib/services/environment.py rename to src/services/environment.py index 2391b5b..4f1137c 100644 --- a/lib/services/environment.py +++ b/src/services/environment.py @@ -4,8 +4,8 @@ from rocketpy.environment.environment import Environment as RocketPyEnvironment from rocketpy.utilities import get_instance_attributes -from lib.models.environment import EnvironmentModel -from lib.views.environment import EnvironmentSimulation +from src.models.environment import EnvironmentModel +from src.views.environment import EnvironmentSimulation class EnvironmentService: diff --git a/lib/services/flight.py b/src/services/flight.py similarity index 90% rename from lib/services/flight.py rename to src/services/flight.py index 47e50da..fdce768 100644 --- a/lib/services/flight.py +++ b/src/services/flight.py @@ -5,10 +5,10 @@ from rocketpy.simulation.flight import Flight as RocketPyFlight from rocketpy.utilities import get_instance_attributes -from lib.services.environment import EnvironmentService -from lib.services.rocket import RocketService -from lib.models.flight import FlightModel -from lib.views.flight import FlightSimulation +from src.services.environment import EnvironmentService +from src.services.rocket import RocketService +from src.models.flight import FlightModel +from src.views.flight import FlightSimulation class FlightService: diff --git a/lib/services/motor.py b/src/services/motor.py similarity index 97% rename from lib/services/motor.py rename to src/services/motor.py index df04302..1ecae4b 100644 --- a/lib/services/motor.py +++ b/src/services/motor.py @@ -15,9 +15,9 @@ TankGeometry, ) -from lib.models.sub.tanks import TankKinds -from lib.models.motor import MotorKinds, MotorModel -from lib.views.motor import MotorSimulation +from src.models.sub.tanks import TankKinds +from src.models.motor import MotorKinds, MotorModel +from src.views.motor import MotorSimulation class MotorService: diff --git a/lib/services/rocket.py b/src/services/rocket.py similarity index 96% rename from lib/services/rocket.py rename to src/services/rocket.py index 77c7533..65bbfe3 100644 --- a/lib/services/rocket.py +++ b/src/services/rocket.py @@ -13,11 +13,11 @@ ) from rocketpy.utilities import get_instance_attributes -from lib import logger -from lib.models.rocket import RocketModel, Parachute -from lib.models.sub.aerosurfaces import NoseCone, Tail, Fins -from lib.services.motor import MotorService -from lib.views.rocket import RocketSimulation +from src import logger +from src.models.rocket import RocketModel, Parachute +from src.models.sub.aerosurfaces import NoseCone, Tail, Fins +from src.services.motor import MotorService +from src.views.rocket import RocketSimulation class RocketService: diff --git a/lib/settings/gunicorn.py b/src/settings/gunicorn.py similarity index 82% rename from lib/settings/gunicorn.py rename to src/settings/gunicorn.py index a563567..f2b92cd 100644 --- a/lib/settings/gunicorn.py +++ b/src/settings/gunicorn.py @@ -1,6 +1,6 @@ import uptrace from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor -from lib.secrets import Secrets +from src.secrets import Secrets def post_fork(server, worker): # pylint: disable=unused-argument @@ -10,7 +10,7 @@ def post_fork(server, worker): # pylint: disable=unused-argument service_version="2.2.0", deployment_environment="production", ) - from lib.api import ( # pylint: disable=import-outside-toplevel + from src.api import ( # pylint: disable=import-outside-toplevel app as fastapi_server, ) diff --git a/lib/utils.py b/src/utils.py similarity index 100% rename from lib/utils.py rename to src/utils.py diff --git a/lib/views/environment.py b/src/views/environment.py similarity index 94% rename from lib/views/environment.py rename to src/views/environment.py index 583a212..4283d52 100644 --- a/lib/views/environment.py +++ b/src/views/environment.py @@ -1,8 +1,8 @@ from typing import Optional from datetime import datetime, timedelta -from lib.views.interface import ApiBaseView -from lib.models.environment import EnvironmentModel -from lib.utils import AnyToPrimitive +from src.views.interface import ApiBaseView +from src.models.environment import EnvironmentModel +from src.utils import AnyToPrimitive class EnvironmentSimulation(ApiBaseView): diff --git a/lib/views/flight.py b/src/views/flight.py similarity index 96% rename from lib/views/flight.py rename to src/views/flight.py index 15dd93c..1a82f98 100644 --- a/lib/views/flight.py +++ b/src/views/flight.py @@ -1,9 +1,9 @@ from typing import Optional -from lib.models.flight import FlightModel -from lib.views.interface import ApiBaseView -from lib.views.rocket import RocketView, RocketSimulation -from lib.views.environment import EnvironmentSimulation -from lib.utils import AnyToPrimitive +from src.models.flight import FlightModel +from src.views.interface import ApiBaseView +from src.views.rocket import RocketView, RocketSimulation +from src.views.environment import EnvironmentSimulation +from src.utils import AnyToPrimitive class FlightSimulation(RocketSimulation, EnvironmentSimulation): diff --git a/lib/views/interface.py b/src/views/interface.py similarity index 100% rename from lib/views/interface.py rename to src/views/interface.py diff --git a/lib/views/motor.py b/src/views/motor.py similarity index 96% rename from lib/views/motor.py rename to src/views/motor.py index 121dd3c..9f73a17 100644 --- a/lib/views/motor.py +++ b/src/views/motor.py @@ -1,8 +1,8 @@ from typing import List, Optional from pydantic import BaseModel -from lib.views.interface import ApiBaseView -from lib.models.motor import MotorModel -from lib.utils import AnyToPrimitive +from src.views.interface import ApiBaseView +from src.models.motor import MotorModel +from src.utils import AnyToPrimitive class MotorSimulation(BaseModel): diff --git a/lib/views/rocket.py b/src/views/rocket.py similarity index 91% rename from lib/views/rocket.py rename to src/views/rocket.py index c19bab6..becee4a 100644 --- a/lib/views/rocket.py +++ b/src/views/rocket.py @@ -1,8 +1,8 @@ from typing import Optional -from lib.models.rocket import RocketModel -from lib.views.interface import ApiBaseView -from lib.views.motor import MotorView, MotorSimulation -from lib.utils import AnyToPrimitive +from src.models.rocket import RocketModel +from src.views.interface import ApiBaseView +from src.views.motor import MotorView, MotorSimulation +from src.utils import AnyToPrimitive class RocketSimulation(MotorSimulation): diff --git a/tests/unit/test_controllers/test_controller_interface.py b/tests/unit/test_controllers/test_controller_interface.py index 9b190bf..1dff129 100644 --- a/tests/unit/test_controllers/test_controller_interface.py +++ b/tests/unit/test_controllers/test_controller_interface.py @@ -2,7 +2,7 @@ import pytest from pymongo.errors import PyMongoError from fastapi import HTTPException, status -from lib.controllers.interface import ( +from src.controllers.interface import ( ControllerInterface, controller_exception_handler, ) @@ -37,7 +37,7 @@ async def method( mock_kwargs = {'foo': 'bar'} mock_args = ('foo', 'bar') wrapped_method = controller_exception_handler(method) - with patch('lib.controllers.interface.logger') as mock_logger: + with patch('src.controllers.interface.logger') as mock_logger: assert await wrapped_method( test_controller, stub_model, *mock_args, **mock_kwargs ) == (stub_model, mock_args, mock_kwargs) @@ -53,7 +53,7 @@ async def method(self, model, *args, **kwargs): raise PyMongoError wrapped_method = controller_exception_handler(method) - with patch('lib.controllers.interface.logger') as mock_logger: + with patch('src.controllers.interface.logger') as mock_logger: with pytest.raises(HTTPException) as exc: await wrapped_method(None, stub_model) assert exc.value.status_code == status.HTTP_503_SERVICE_UNAVAILABLE @@ -71,7 +71,7 @@ async def method(self, model, *args, **kwargs): ) wrapped_method = controller_exception_handler(method) - with patch('lib.controllers.interface.logger') as mock_logger: + with patch('src.controllers.interface.logger') as mock_logger: with pytest.raises(HTTPException) as exc: await wrapped_method(None, stub_model) assert exc.value.status_code == status.HTTP_404_NOT_FOUND @@ -85,7 +85,7 @@ async def method(self, model, *args, **kwargs): raise ValueError('Test Error') wrapped_method = controller_exception_handler(method) - with patch('lib.controllers.interface.logger') as mock_logger: + with patch('src.controllers.interface.logger') as mock_logger: with pytest.raises(HTTPException) as exc: await wrapped_method(None, stub_model) assert exc.value.status_code == status.HTTP_500_INTERNAL_SERVER_ERROR @@ -97,7 +97,7 @@ async def method(self, model, *args, **kwargs): def test_controller_interface_init(stub_model): with patch( - 'lib.controllers.interface.ControllerInterface._generate_method' + 'src.controllers.interface.ControllerInterface._generate_method' ) as mock_gen: mock_gen.return_value = lambda *args, **kwargs: True stub_controller = ControllerInterface([stub_model]) @@ -118,9 +118,9 @@ def test_controller_interface_init(stub_model): async def test_controller_interface_generate_available_method( stub_controller, stub_model ): - with patch('lib.controllers.interface.RepositoryInterface') as mock_repo: + with patch('src.controllers.interface.RepositoryInterface') as mock_repo: with patch( - 'lib.controllers.interface.ControllerInterface._get_model' + 'src.controllers.interface.ControllerInterface._get_model' ) as mock_get: mock_get.return_value = stub_model method = stub_controller._generate_method('get', stub_model) @@ -134,7 +134,7 @@ async def test_controller_interface_generate_available_method( async def test_controller_interface_generate_unavailable_method( stub_controller, stub_model ): - with patch('lib.controllers.interface.RepositoryInterface') as mock_repo: + with patch('src.controllers.interface.RepositoryInterface') as mock_repo: with pytest.raises(NotImplementedError): method = stub_controller._generate_method('foo', stub_model) await method(None, stub_model, mock_repo, 'arg', key='bar') @@ -142,9 +142,9 @@ async def test_controller_interface_generate_unavailable_method( @pytest.mark.asyncio async def test_controller_interface_post_model(stub_controller, stub_model): - with patch('lib.controllers.interface.RepositoryInterface') as mock_repo: + with patch('src.controllers.interface.RepositoryInterface') as mock_repo: with patch( - 'lib.controllers.interface.RepositoryInterface.get_model_repo' + 'src.controllers.interface.RepositoryInterface.get_model_repo' ) as mock_get_repo: mock_get_repo.return_value = mock_repo assert ( @@ -159,9 +159,9 @@ async def test_controller_interface_post_model(stub_controller, stub_model): async def test_controller_interface_get_model_found( stub_controller, stub_model ): - with patch('lib.controllers.interface.RepositoryInterface') as mock_repo: + with patch('src.controllers.interface.RepositoryInterface') as mock_repo: with patch( - 'lib.controllers.interface.RepositoryInterface.get_model_repo' + 'src.controllers.interface.RepositoryInterface.get_model_repo' ) as mock_get_repo: mock_get_repo.return_value = mock_repo mock_repo.return_value.__aenter__.return_value.read_test_model_by_id.return_value = ( @@ -177,9 +177,9 @@ async def test_controller_interface_get_model_found( async def test_controller_interface_get_model_not_found( stub_controller, stub_model ): - with patch('lib.controllers.interface.RepositoryInterface') as mock_repo: + with patch('src.controllers.interface.RepositoryInterface') as mock_repo: with patch( - 'lib.controllers.interface.RepositoryInterface.get_model_repo' + 'src.controllers.interface.RepositoryInterface.get_model_repo' ) as mock_get_repo: mock_get_repo.return_value = mock_repo mock_repo.return_value.__aenter__.return_value.read_test_model_by_id.return_value = ( @@ -193,9 +193,9 @@ async def test_controller_interface_get_model_not_found( @pytest.mark.asyncio async def test_controller_interface_update_model(stub_controller, stub_model): - with patch('lib.controllers.interface.RepositoryInterface') as mock_repo: + with patch('src.controllers.interface.RepositoryInterface') as mock_repo: with patch( - 'lib.controllers.interface.RepositoryInterface.get_model_repo' + 'src.controllers.interface.RepositoryInterface.get_model_repo' ) as mock_get_repo: mock_get_repo.return_value = mock_repo mock_repo.return_value.__aenter__.return_value.update_test_model_by_id.return_value = ( @@ -211,9 +211,9 @@ async def test_controller_interface_update_model(stub_controller, stub_model): @pytest.mark.asyncio async def test_controller_interface_delete_model(stub_controller, stub_model): - with patch('lib.controllers.interface.RepositoryInterface') as mock_repo: + with patch('src.controllers.interface.RepositoryInterface') as mock_repo: with patch( - 'lib.controllers.interface.RepositoryInterface.get_model_repo' + 'src.controllers.interface.RepositoryInterface.get_model_repo' ) as mock_get_repo: mock_get_repo.return_value = mock_repo mock_repo.return_value.__aenter__.return_value.delete_test_model_by_id.return_value = ( diff --git a/tests/unit/test_repositories/test_repository_interface.py b/tests/unit/test_repositories/test_repository_interface.py index 6ea6001..f4989dc 100644 --- a/tests/unit/test_repositories/test_repository_interface.py +++ b/tests/unit/test_repositories/test_repository_interface.py @@ -3,7 +3,7 @@ import pytest_asyncio from pymongo.errors import PyMongoError from fastapi import HTTPException, status -from lib.repositories.interface import ( +from src.repositories.interface import ( RepositoryInterface, repository_exception_handler, RepositoryNotInitializedException, @@ -81,7 +81,7 @@ async def method(self, *args, **kwargs): # pylint: disable=unused-argument mock_args = ('foo', 'bar') mock_repo = Mock(model=Mock(NAME='mock_model')) wrapped_method = repository_exception_handler(method) - with patch('lib.repositories.interface.logger') as mock_logger: + with patch('src.repositories.interface.logger') as mock_logger: assert await wrapped_method(mock_repo, *mock_args, **mock_kwargs) == ( mock_args, mock_kwargs, @@ -136,7 +136,7 @@ async def test_repository_interface_init(stub_repository): def test_get_model_repo(stub_repository): with patch( - 'lib.repositories.interface.importlib.import_module' + 'src.repositories.interface.importlib.import_module' ) as mock_import_module: mock_import_module.return_value = Mock(MockmodelRepository='mock_repo') assert ( @@ -148,7 +148,7 @@ def test_get_model_repo(stub_repository): @pytest.mark.asyncio async def test_repository_insert_data(stub_repository, mock_db_interface): with patch( - 'lib.repositories.interface.RepositoryInterface.get_collection', + 'src.repositories.interface.RepositoryInterface.get_collection', return_value=mock_db_interface, ): assert await stub_repository.insert('mock_data') == 'mock_id' @@ -158,7 +158,7 @@ async def test_repository_insert_data(stub_repository, mock_db_interface): @pytest.mark.asyncio async def test_repository_insert_invalid_data(stub_repository_invalid_model): with patch( - 'lib.repositories.interface.RepositoryInterface.get_collection', + 'src.repositories.interface.RepositoryInterface.get_collection', return_value=mock_db_interface, ): with pytest.raises(HTTPException): @@ -168,11 +168,11 @@ async def test_repository_insert_invalid_data(stub_repository_invalid_model): @pytest.mark.asyncio async def test_repository_update_data(stub_repository, mock_db_interface): with patch( - 'lib.repositories.interface.RepositoryInterface.get_collection', + 'src.repositories.interface.RepositoryInterface.get_collection', return_value=mock_db_interface, ): with patch( - 'lib.repositories.interface.ObjectId', side_effect=lambda x: x + 'src.repositories.interface.ObjectId', side_effect=lambda x: x ): assert ( await stub_repository.update_by_id( @@ -188,7 +188,7 @@ async def test_repository_update_data(stub_repository, mock_db_interface): @pytest.mark.asyncio async def test_repository_update_invalid_data(stub_repository_invalid_model): with patch( - 'lib.repositories.interface.RepositoryInterface.get_collection', + 'src.repositories.interface.RepositoryInterface.get_collection', return_value=mock_db_interface, ): with pytest.raises(HTTPException): @@ -202,11 +202,11 @@ async def test_repository_find_data_found( stub_repository, mock_db_interface, stub_loaded_model ): with patch( - 'lib.repositories.interface.RepositoryInterface.get_collection', + 'src.repositories.interface.RepositoryInterface.get_collection', return_value=mock_db_interface, ): with patch( - 'lib.repositories.interface.ObjectId', side_effect=lambda x: x + 'src.repositories.interface.ObjectId', side_effect=lambda x: x ): assert ( await stub_repository.find_by_id(data_id='mock_id') @@ -226,11 +226,11 @@ async def test_repository_find_data_not_found( ): mock_db_interface.find_one.return_value = None with patch( - 'lib.repositories.interface.RepositoryInterface.get_collection', + 'src.repositories.interface.RepositoryInterface.get_collection', return_value=mock_db_interface, ): with patch( - 'lib.repositories.interface.ObjectId', side_effect=lambda x: x + 'src.repositories.interface.ObjectId', side_effect=lambda x: x ): assert await stub_repository.find_by_id(data_id='mock_id') is None mock_db_interface.find_one.assert_called_once_with( @@ -241,11 +241,11 @@ async def test_repository_find_data_not_found( @pytest.mark.asyncio async def test_repository_delete_data(stub_repository, mock_db_interface): with patch( - 'lib.repositories.interface.RepositoryInterface.get_collection', + 'src.repositories.interface.RepositoryInterface.get_collection', return_value=mock_db_interface, ): with patch( - 'lib.repositories.interface.ObjectId', side_effect=lambda x: x + 'src.repositories.interface.ObjectId', side_effect=lambda x: x ): assert ( await stub_repository.delete_by_id(data_id='mock_id') @@ -261,7 +261,7 @@ async def test_repository_find_by_query_found( stub_repository, mock_db_interface, stub_loaded_model ): with patch( - 'lib.repositories.interface.RepositoryInterface.get_collection', + 'src.repositories.interface.RepositoryInterface.get_collection', return_value=mock_db_interface, ): assert await stub_repository.find_by_query('mock_query') == [ @@ -279,7 +279,7 @@ async def test_repository_find_by_query_not_found( stub_repository, mock_db_interface_empty_find ): with patch( - 'lib.repositories.interface.RepositoryInterface.get_collection', + 'src.repositories.interface.RepositoryInterface.get_collection', return_value=mock_db_interface_empty_find, ): assert await stub_repository.find_by_query('mock_query') == [] diff --git a/tests/unit/test_routes/conftest.py b/tests/unit/test_routes/conftest.py index 260a907..bb88a64 100644 --- a/tests/unit/test_routes/conftest.py +++ b/tests/unit/test_routes/conftest.py @@ -1,11 +1,11 @@ import json import pytest -from lib.models.rocket import RocketModel -from lib.models.sub.tanks import MotorTank, TankFluids, TankKinds -from lib.models.motor import MotorModel -from lib.models.environment import EnvironmentModel -from lib.models.sub.aerosurfaces import Fins, NoseCone +from src.models.rocket import RocketModel +from src.models.sub.tanks import MotorTank, TankFluids, TankKinds +from src.models.motor import MotorModel +from src.models.environment import EnvironmentModel +from src.models.sub.aerosurfaces import Fins, NoseCone @pytest.fixture diff --git a/tests/unit/test_routes/test_environments_route.py b/tests/unit/test_routes/test_environments_route.py index 0711bc3..6ee7136 100644 --- a/tests/unit/test_routes/test_environments_route.py +++ b/tests/unit/test_routes/test_environments_route.py @@ -3,14 +3,14 @@ import pytest from fastapi.testclient import TestClient from fastapi import HTTPException -from lib.models.environment import EnvironmentModel -from lib.views.environment import ( +from src.models.environment import EnvironmentModel +from src.views.environment import ( EnvironmentView, EnvironmentCreated, EnvironmentRetrieved, EnvironmentSimulation, ) -from lib import app +from src import app client = TestClient(app) @@ -25,7 +25,7 @@ def stub_environment_simulation_dump(): @pytest.fixture(autouse=True) def mock_controller_instance(): with patch( - "lib.routes.environment.EnvironmentController", autospec=True + "src.routes.environment.EnvironmentController", autospec=True ) as mock_controller: mock_controller_instance = mock_controller.return_value mock_controller_instance.post_environment = Mock() diff --git a/tests/unit/test_routes/test_flights_route.py b/tests/unit/test_routes/test_flights_route.py index 8775b6e..9cf28a0 100644 --- a/tests/unit/test_routes/test_flights_route.py +++ b/tests/unit/test_routes/test_flights_route.py @@ -3,19 +3,19 @@ import pytest from fastapi.testclient import TestClient from fastapi import HTTPException, status -from lib.models.environment import EnvironmentModel -from lib.models.flight import FlightModel -from lib.models.motor import MotorModel, MotorKinds -from lib.models.rocket import RocketModel -from lib.views.motor import MotorView -from lib.views.rocket import RocketView -from lib.views.flight import ( +from src.models.environment import EnvironmentModel +from src.models.flight import FlightModel +from src.models.motor import MotorModel, MotorKinds +from src.models.rocket import RocketModel +from src.views.motor import MotorView +from src.views.rocket import RocketView +from src.views.flight import ( FlightCreated, FlightRetrieved, FlightSimulation, FlightView, ) -from lib import app +from src import app client = TestClient(app) @@ -44,7 +44,7 @@ def stub_flight_simulate_dump(): @pytest.fixture(autouse=True) def mock_controller_instance(): with patch( - "lib.routes.flight.FlightController", autospec=True + "src.routes.flight.FlightController", autospec=True ) as mock_controller: mock_controller_instance = mock_controller.return_value mock_controller_instance.post_flight = Mock() diff --git a/tests/unit/test_routes/test_motors_route.py b/tests/unit/test_routes/test_motors_route.py index 79ca3b3..1bacc91 100644 --- a/tests/unit/test_routes/test_motors_route.py +++ b/tests/unit/test_routes/test_motors_route.py @@ -3,17 +3,17 @@ import pytest from fastapi.testclient import TestClient from fastapi import HTTPException -from lib.models.motor import ( +from src.models.motor import ( MotorModel, MotorKinds, ) -from lib.views.motor import ( +from src.views.motor import ( MotorCreated, MotorRetrieved, MotorSimulation, MotorView, ) -from lib import app +from src import app client = TestClient(app) @@ -28,7 +28,7 @@ def stub_motor_dump_simulation(): @pytest.fixture(autouse=True) def mock_controller_instance(): with patch( - "lib.routes.motor.MotorController", autospec=True + "src.routes.motor.MotorController", autospec=True ) as mock_controller: mock_controller_instance = mock_controller.return_value mock_controller_instance.post_motor = Mock() diff --git a/tests/unit/test_routes/test_rockets_route.py b/tests/unit/test_routes/test_rockets_route.py index 2e04481..8c15771 100644 --- a/tests/unit/test_routes/test_rockets_route.py +++ b/tests/unit/test_routes/test_rockets_route.py @@ -3,23 +3,23 @@ import pytest from fastapi.testclient import TestClient from fastapi import HTTPException, status -from lib.models.sub.aerosurfaces import ( +from src.models.sub.aerosurfaces import ( Tail, RailButtons, Parachute, ) -from lib.models.rocket import RocketModel -from lib.models.motor import ( +from src.models.rocket import RocketModel +from src.models.motor import ( MotorModel, MotorKinds, ) -from lib.views.rocket import ( +from src.views.rocket import ( RocketCreated, RocketRetrieved, RocketSimulation, RocketView, ) -from lib import app +from src import app client = TestClient(app) @@ -73,7 +73,7 @@ def stub_parachute_dump(): @pytest.fixture(autouse=True) def mock_controller_instance(): with patch( - "lib.routes.rocket.RocketController", autospec=True + "src.routes.rocket.RocketController", autospec=True ) as mock_controller: mock_controller_instance = mock_controller.return_value mock_controller_instance.post_rocket = Mock() From e68f7486eaf56c32a86f66816cd01091e951abfb Mon Sep 17 00:00:00 2001 From: Gabriel Barberini Date: Sat, 22 Feb 2025 18:26:10 -0300 Subject: [PATCH 17/34] adapts motor model --- src/models/motor.py | 17 +- src/routes/flight.py | 18 +- src/routes/motor.py | 12 +- src/routes/rocket.py | 11 +- src/services/motor.py | 2 +- tests/unit/test_routes/conftest.py | 1 + tests/unit/test_routes/test_flights_route.py | 119 ++----- tests/unit/test_routes/test_motors_route.py | 330 ++++++++----------- tests/unit/test_routes/test_rockets_route.py | 280 ++++++---------- 9 files changed, 285 insertions(+), 505 deletions(-) diff --git a/src/models/motor.py b/src/models/motor.py index 8718fb5..4d1cdf5 100644 --- a/src/models/motor.py +++ b/src/models/motor.py @@ -1,6 +1,6 @@ from enum import Enum from typing import Optional, Tuple, List, Union, Self, ClassVar, Literal -from pydantic import PrivateAttr, model_validator, computed_field +from pydantic import model_validator from src.models.interface import ApiBaseModel from src.models.sub.tanks import MotorTank @@ -24,6 +24,7 @@ class MotorModel(ApiBaseModel): dry_mass: float dry_inertia: Tuple[float, float, float] = (0, 0, 0) center_of_dry_mass_position: float + motor_kind: MotorKinds # Generic motor parameters chamber_radius: Optional[float] = None @@ -56,14 +57,11 @@ class MotorModel(ApiBaseModel): ] = 'nozzle_to_combustion_chamber' reshape_thrust_curve: Union[bool, tuple] = False - # Computed parameters - _motor_kind: MotorKinds = PrivateAttr(default=MotorKinds.SOLID) - @model_validator(mode='after') # TODO: extend guard to check motor kinds and tank kinds specifics def validate_motor_kind(self): if ( - self._motor_kind not in (MotorKinds.SOLID, MotorKinds.GENERIC) + self.motor_kind not in (MotorKinds.SOLID, MotorKinds.GENERIC) and self.tanks is None ): raise ValueError( @@ -71,15 +69,6 @@ def validate_motor_kind(self): ) return self - @computed_field - @property - def selected_motor_kind(self) -> str: - return self._motor_kind.value - - def set_motor_kind(self, motor_kind: MotorKinds): - self._motor_kind = motor_kind - return self - @staticmethod def UPDATED(): return diff --git a/src/routes/flight.py b/src/routes/flight.py index 5bb6af0..11e1ba2 100644 --- a/src/routes/flight.py +++ b/src/routes/flight.py @@ -13,7 +13,6 @@ from src.models.environment import EnvironmentModel from src.models.flight import FlightModel from src.models.rocket import RocketModel -from src.models.motor import MotorKinds from src.controllers.flight import FlightController router = APIRouter( @@ -30,9 +29,7 @@ @router.post("/", status_code=201) -async def create_flight( - flight: FlightModel, motor_kind: MotorKinds -) -> FlightCreated: +async def create_flight(flight: FlightModel) -> FlightCreated: """ Creates a new flight @@ -41,7 +38,6 @@ async def create_flight( """ with tracer.start_as_current_span("create_flight"): controller = FlightController() - flight.rocket.motor.set_motor_kind(motor_kind) return await controller.post_flight(flight) @@ -59,9 +55,7 @@ async def read_flight(flight_id: str) -> FlightRetrieved: @router.put("/{flight_id}", status_code=204) -async def update_flight( - flight_id: str, flight: FlightModel, motor_kind: MotorKinds -) -> None: +async def update_flight(flight_id: str, flight: FlightModel) -> None: """ Updates an existing flight @@ -73,7 +67,6 @@ async def update_flight( """ with tracer.start_as_current_span("update_flight"): controller = FlightController() - flight.rocket.motor.set_motor_kind(motor_kind) return await controller.put_flight_by_id(flight_id, flight) @@ -144,11 +137,7 @@ async def update_flight_environment( @router.put("/{flight_id}/rocket", status_code=204) -async def update_flight_rocket( - flight_id: str, - rocket: RocketModel, - motor_kind: MotorKinds, -) -> None: +async def update_flight_rocket(flight_id: str, rocket: RocketModel) -> None: """ Updates flight rocket. @@ -160,7 +149,6 @@ async def update_flight_rocket( """ with tracer.start_as_current_span("update_flight_rocket"): controller = FlightController() - rocket.motor.set_motor_kind(motor_kind) return await controller.update_rocket_by_flight_id( flight_id, rocket=rocket, diff --git a/src/routes/motor.py b/src/routes/motor.py index 32254f4..0227d51 100644 --- a/src/routes/motor.py +++ b/src/routes/motor.py @@ -10,7 +10,7 @@ MotorCreated, MotorRetrieved, ) -from src.models.motor import MotorModel, MotorKinds +from src.models.motor import MotorModel from src.controllers.motor import MotorController router = APIRouter( @@ -27,9 +27,7 @@ @router.post("/", status_code=201) -async def create_motor( - motor: MotorModel, motor_kind: MotorKinds -) -> MotorCreated: +async def create_motor(motor: MotorModel) -> MotorCreated: """ Creates a new motor @@ -38,7 +36,6 @@ async def create_motor( """ with tracer.start_as_current_span("create_motor"): controller = MotorController() - motor.set_motor_kind(motor_kind) return await controller.post_motor(motor) @@ -56,9 +53,7 @@ async def read_motor(motor_id: str) -> MotorRetrieved: @router.put("/{motor_id}", status_code=204) -async def update_motor( - motor_id: str, motor: MotorModel, motor_kind: MotorKinds -) -> None: +async def update_motor(motor_id: str, motor: MotorModel) -> None: """ Updates an existing motor @@ -70,7 +65,6 @@ async def update_motor( """ with tracer.start_as_current_span("update_motor"): controller = MotorController() - motor.set_motor_kind(motor_kind) return await controller.put_motor_by_id(motor_id, motor) diff --git a/src/routes/rocket.py b/src/routes/rocket.py index 67457d2..13cbb8c 100644 --- a/src/routes/rocket.py +++ b/src/routes/rocket.py @@ -11,7 +11,6 @@ RocketRetrieved, ) from src.models.rocket import RocketModel -from src.models.motor import MotorKinds from src.controllers.rocket import RocketController router = APIRouter( @@ -28,9 +27,7 @@ @router.post("/", status_code=201) -async def create_rocket( - rocket: RocketModel, motor_kind: MotorKinds -) -> RocketCreated: +async def create_rocket(rocket: RocketModel) -> RocketCreated: """ Creates a new rocket @@ -39,7 +36,6 @@ async def create_rocket( """ with tracer.start_as_current_span("create_rocket"): controller = RocketController() - rocket.motor.set_motor_kind(motor_kind) return await controller.post_rocket(rocket) @@ -57,9 +53,7 @@ async def read_rocket(rocket_id: str) -> RocketRetrieved: @router.put("/{rocket_id}", status_code=204) -async def update_rocket( - rocket_id: str, rocket: RocketModel, motor_kind: MotorKinds -) -> None: +async def update_rocket(rocket_id: str, rocket: RocketModel) -> None: """ Updates an existing rocket @@ -71,7 +65,6 @@ async def update_rocket( """ with tracer.start_as_current_span("update_rocket"): controller = RocketController() - rocket.motor.set_motor_kind(motor_kind) return await controller.put_rocket_by_id(rocket_id, rocket) diff --git a/src/services/motor.py b/src/services/motor.py index 1ecae4b..5b2acde 100644 --- a/src/services/motor.py +++ b/src/services/motor.py @@ -47,7 +47,7 @@ def from_motor_model(cls, motor: MotorModel) -> Self: "reshape_thrust_curve": False or motor.reshape_thrust_curve, } - match MotorKinds(motor.selected_motor_kind): + match MotorKinds(motor.motor_kind): case MotorKinds.LIQUID: rocketpy_motor = LiquidMotor(**motor_core) case MotorKinds.HYBRID: diff --git a/tests/unit/test_routes/conftest.py b/tests/unit/test_routes/conftest.py index bb88a64..74c63f9 100644 --- a/tests/unit/test_routes/conftest.py +++ b/tests/unit/test_routes/conftest.py @@ -24,6 +24,7 @@ def stub_motor_dump(): dry_mass=0, dry_inertia=[0, 0, 0], center_of_dry_mass_position=0, + motor_kind='GENERIC', ) motor_json = motor.model_dump_json() return json.loads(motor_json) diff --git a/tests/unit/test_routes/test_flights_route.py b/tests/unit/test_routes/test_flights_route.py index 9cf28a0..f860156 100644 --- a/tests/unit/test_routes/test_flights_route.py +++ b/tests/unit/test_routes/test_flights_route.py @@ -5,7 +5,6 @@ from fastapi import HTTPException, status from src.models.environment import EnvironmentModel from src.models.flight import FlightModel -from src.models.motor import MotorModel, MotorKinds from src.models.rocket import RocketModel from src.views.motor import MotorView from src.views.rocket import RocketView @@ -61,21 +60,15 @@ def mock_controller_instance(): def test_create_flight(stub_flight_dump, mock_controller_instance): mock_response = AsyncMock(return_value=FlightCreated(flight_id='123')) mock_controller_instance.post_flight = mock_response - with patch.object( - MotorModel, 'set_motor_kind', side_effect=None - ) as mock_set_motor_kind: - response = client.post( - '/flights/', json=stub_flight_dump, params={'motor_kind': 'HYBRID'} - ) - assert response.status_code == 201 - assert response.json() == { - 'flight_id': '123', - 'message': 'Flight successfully created', - } - mock_set_motor_kind.assert_called_once_with(MotorKinds.HYBRID) - mock_controller_instance.post_flight.assert_called_once_with( - FlightModel(**stub_flight_dump) - ) + response = client.post('/flights/', json=stub_flight_dump) + assert response.status_code == 201 + assert response.json() == { + 'flight_id': '123', + 'message': 'Flight successfully created', + } + mock_controller_instance.post_flight.assert_called_once_with( + FlightModel(**stub_flight_dump) + ) def test_create_flight_optional_params( @@ -95,21 +88,15 @@ def test_create_flight_optional_params( ) mock_response = AsyncMock(return_value=FlightCreated(flight_id='123')) mock_controller_instance.post_flight = mock_response - with patch.object( - MotorModel, 'set_motor_kind', side_effect=None - ) as mock_set_motor_kind: - response = client.post( - '/flights/', json=stub_flight_dump, params={'motor_kind': 'HYBRID'} - ) - assert response.status_code == 201 - assert response.json() == { - 'flight_id': '123', - 'message': 'Flight successfully created', - } - mock_set_motor_kind.assert_called_once_with(MotorKinds.HYBRID) - mock_controller_instance.post_flight.assert_called_once_with( - FlightModel(**stub_flight_dump) - ) + response = client.post('/flights/', json=stub_flight_dump) + assert response.status_code == 201 + assert response.json() == { + 'flight_id': '123', + 'message': 'Flight successfully created', + } + mock_controller_instance.post_flight.assert_called_once_with( + FlightModel(**stub_flight_dump) + ) def test_create_flight_invalid_input(): @@ -128,9 +115,7 @@ def test_create_flight_server_error( ) ) mock_controller_instance.post_flight = mock_response - response = client.post( - '/flights/', json=stub_flight_dump, params={'motor_kind': 'HYBRID'} - ) + response = client.post('/flights/', json=stub_flight_dump) assert response.status_code == 500 assert response.json() == {'detail': 'Internal Server Error'} @@ -143,7 +128,7 @@ def test_read_flight( ): del stub_rocket_dump['motor'] del stub_flight_dump['rocket'] - stub_motor_dump.update({'selected_motor_kind': 'HYBRID'}) + stub_motor_dump.update({'motor_kind': 'SOLID'}) motor_view = MotorView(**stub_motor_dump) rocket_view = RocketView(**stub_rocket_dump, motor=motor_view) flight_view = FlightView( @@ -181,19 +166,11 @@ def test_read_flight_server_error(mock_controller_instance): def test_update_flight_by_id(stub_flight_dump, mock_controller_instance): mock_response = AsyncMock(return_value=None) mock_controller_instance.put_flight_by_id = mock_response - with patch.object( - MotorModel, 'set_motor_kind', side_effect=None - ) as mock_set_motor_kind: - response = client.put( - '/flights/123', - json=stub_flight_dump, - params={'motor_kind': 'HYBRID'}, - ) - assert response.status_code == 204 - mock_set_motor_kind.assert_called_once_with(MotorKinds.HYBRID) - mock_controller_instance.put_flight_by_id.assert_called_once_with( - '123', FlightModel(**stub_flight_dump) - ) + response = client.put('/flights/123', json=stub_flight_dump) + assert response.status_code == 204 + mock_controller_instance.put_flight_by_id.assert_called_once_with( + '123', FlightModel(**stub_flight_dump) + ) def test_update_environment_by_flight_id( @@ -215,19 +192,11 @@ def test_update_rocket_by_flight_id( ): mock_response = AsyncMock(return_value=None) mock_controller_instance.update_rocket_by_flight_id = mock_response - with patch.object( - MotorModel, 'set_motor_kind', side_effect=None - ) as mock_set_motor_kind: - response = client.put( - '/flights/123/rocket', - json=stub_rocket_dump, - params={'motor_kind': 'HYBRID'}, - ) - assert response.status_code == 204 - mock_set_motor_kind.assert_called_once_with(MotorKinds.HYBRID) - mock_controller_instance.update_rocket_by_flight_id.assert_called_once_with( - '123', rocket=RocketModel(**stub_rocket_dump) - ) + response = client.put('/flights/123/rocket', json=stub_rocket_dump) + assert response.status_code == 204 + mock_controller_instance.update_rocket_by_flight_id.assert_called_once_with( + '123', rocket=RocketModel(**stub_rocket_dump) + ) def test_update_environment_by_flight_id_invalid_input(): @@ -242,9 +211,7 @@ def test_update_rocket_by_flight_id_invalid_input(): def test_update_flight_invalid_input(): response = client.put( - '/flights/123', - json={'environment': 'foo', 'rocket': 'bar'}, - params={'motor_kind': 'GENERIC'}, + '/flights/123', json={'environment': 'foo', 'rocket': 'bar'} ) assert response.status_code == 422 @@ -253,11 +220,7 @@ def test_update_flight_not_found(stub_flight_dump, mock_controller_instance): mock_controller_instance.put_flight_by_id.side_effect = HTTPException( status_code=status.HTTP_404_NOT_FOUND ) - response = client.put( - '/flights/123', - json=stub_flight_dump, - params={'motor_kind': 'HYBRID'}, - ) + response = client.put('/flights/123', json=stub_flight_dump) assert response.status_code == 404 assert response.json() == {'detail': 'Not Found'} @@ -268,11 +231,7 @@ def test_update_flight_server_error( mock_controller_instance.put_flight_by_id.side_effect = HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR ) - response = client.put( - '/flights/123', - json=stub_flight_dump, - params={'motor_kind': 'HYBRID'}, - ) + response = client.put('/flights/123', json=stub_flight_dump) assert response.status_code == 500 assert response.json() == {'detail': 'Internal Server Error'} @@ -309,11 +268,7 @@ def test_update_rocket_by_flight_id_not_found( mock_controller_instance.update_rocket_by_flight_id.side_effect = ( HTTPException(status_code=status.HTTP_404_NOT_FOUND) ) - response = client.put( - '/flights/123/rocket', - json=stub_rocket_dump, - params={'motor_kind': 'HYBRID'}, - ) + response = client.put('/flights/123/rocket', json=stub_rocket_dump) assert response.status_code == 404 assert response.json() == {'detail': 'Not Found'} @@ -324,11 +279,7 @@ def test_update_rocket_by_flight_id_server_error( mock_controller_instance.update_rocket_by_flight_id.side_effect = ( HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR) ) - response = client.put( - '/flights/123/rocket', - json=stub_rocket_dump, - params={'motor_kind': 'HYBRID'}, - ) + response = client.put('/flights/123/rocket', json=stub_rocket_dump) assert response.status_code == 500 assert response.json() == {'detail': 'Internal Server Error'} diff --git a/tests/unit/test_routes/test_motors_route.py b/tests/unit/test_routes/test_motors_route.py index 1bacc91..88a7291 100644 --- a/tests/unit/test_routes/test_motors_route.py +++ b/tests/unit/test_routes/test_motors_route.py @@ -3,10 +3,7 @@ import pytest from fastapi.testclient import TestClient from fastapi import HTTPException -from src.models.motor import ( - MotorModel, - MotorKinds, -) +from src.models.motor import MotorModel from src.views.motor import ( MotorCreated, MotorRetrieved, @@ -43,21 +40,15 @@ def mock_controller_instance(): def test_create_motor(stub_motor_dump, mock_controller_instance): mock_response = AsyncMock(return_value=MotorCreated(motor_id='123')) mock_controller_instance.post_motor = mock_response - with patch.object( - MotorModel, 'set_motor_kind', side_effect=None - ) as mock_set_motor_kind: - response = client.post( - '/motors/', json=stub_motor_dump, params={'motor_kind': 'HYBRID'} - ) - assert response.status_code == 201 - assert response.json() == { - 'motor_id': '123', - 'message': 'Motor successfully created', - } - mock_set_motor_kind.assert_called_once_with(MotorKinds.HYBRID) - mock_controller_instance.post_motor.assert_called_once_with( - MotorModel(**stub_motor_dump) - ) + response = client.post('/motors/', json=stub_motor_dump) + assert response.status_code == 201 + assert response.json() == { + 'motor_id': '123', + 'message': 'Motor successfully created', + } + mock_controller_instance.post_motor.assert_called_once_with( + MotorModel(**stub_motor_dump) + ) def test_create_motor_optional_params( @@ -72,21 +63,15 @@ def test_create_motor_optional_params( ) mock_response = AsyncMock(return_value=MotorCreated(motor_id='123')) mock_controller_instance.post_motor = mock_response - with patch.object( - MotorModel, 'set_motor_kind', side_effect=None - ) as mock_set_motor_kind: - response = client.post( - '/motors/', json=stub_motor_dump, params={'motor_kind': 'HYBRID'} - ) - assert response.status_code == 201 - assert response.json() == { - 'motor_id': '123', - 'message': 'Motor successfully created', - } - mock_set_motor_kind.assert_called_once_with(MotorKinds.HYBRID) - mock_controller_instance.post_motor.assert_called_once_with( - MotorModel(**stub_motor_dump) - ) + response = client.post('/motors/', json=stub_motor_dump) + assert response.status_code == 201 + assert response.json() == { + 'motor_id': '123', + 'message': 'Motor successfully created', + } + mock_controller_instance.post_motor.assert_called_once_with( + MotorModel(**stub_motor_dump) + ) def test_create_generic_motor(stub_motor_dump, mock_controller_instance): @@ -97,117 +82,96 @@ def test_create_generic_motor(stub_motor_dump, mock_controller_instance): 'chamber_position': 0, 'propellant_initial_mass': 0, 'nozzle_position': 0, + 'motor_kind': 'GENERIC', } ) mock_response = AsyncMock(return_value=MotorCreated(motor_id='123')) mock_controller_instance.post_motor = mock_response - with patch.object( - MotorModel, 'set_motor_kind', side_effect=None - ) as mock_set_motor_kind: - response = client.post( - '/motors/', json=stub_motor_dump, params={'motor_kind': 'GENERIC'} - ) - assert response.status_code == 201 - assert response.json() == { - 'motor_id': '123', - 'message': 'Motor successfully created', - } - mock_set_motor_kind.assert_called_once_with(MotorKinds.GENERIC) - mock_controller_instance.post_motor.assert_called_once_with( - MotorModel(**stub_motor_dump) - ) + response = client.post('/motors/', json=stub_motor_dump) + assert response.status_code == 201 + assert response.json() == { + 'motor_id': '123', + 'message': 'Motor successfully created', + } + mock_controller_instance.post_motor.assert_called_once_with( + MotorModel(**stub_motor_dump) + ) def test_create_liquid_motor_level_tank( stub_motor_dump, stub_level_tank_dump, mock_controller_instance ): - stub_motor_dump.update({'tanks': [stub_level_tank_dump]}) + stub_motor_dump.update( + {'tanks': [stub_level_tank_dump], 'motor_kind': 'LIQUID'} + ) mock_response = AsyncMock(return_value=MotorCreated(motor_id='123')) mock_controller_instance.post_motor = mock_response - with patch.object( - MotorModel, 'set_motor_kind', side_effect=None - ) as mock_set_motor_kind: - response = client.post( - '/motors/', json=stub_motor_dump, params={'motor_kind': 'LIQUID'} - ) - assert response.status_code == 201 - assert response.json() == { - 'motor_id': '123', - 'message': 'Motor successfully created', - } - mock_set_motor_kind.assert_called_once_with(MotorKinds.LIQUID) - mock_controller_instance.post_motor.assert_called_once_with( - MotorModel(**stub_motor_dump) - ) + response = client.post('/motors/', json=stub_motor_dump) + assert response.status_code == 201 + assert response.json() == { + 'motor_id': '123', + 'message': 'Motor successfully created', + } + mock_controller_instance.post_motor.assert_called_once_with( + MotorModel(**stub_motor_dump) + ) def test_create_liquid_motor_mass_flow_tank( stub_motor_dump, stub_mass_flow_tank_dump, mock_controller_instance ): - stub_motor_dump.update({'tanks': [stub_mass_flow_tank_dump]}) + stub_motor_dump.update( + {'tanks': [stub_mass_flow_tank_dump], 'motor_kind': 'LIQUID'} + ) mock_response = AsyncMock(return_value=MotorCreated(motor_id='123')) mock_controller_instance.post_motor = mock_response - with patch.object( - MotorModel, 'set_motor_kind', side_effect=None - ) as mock_set_motor_kind: - response = client.post( - '/motors/', json=stub_motor_dump, params={'motor_kind': 'LIQUID'} - ) - assert response.status_code == 201 - assert response.json() == { - 'motor_id': '123', - 'message': 'Motor successfully created', - } - mock_set_motor_kind.assert_called_once_with(MotorKinds.LIQUID) - mock_controller_instance.post_motor.assert_called_once_with( - MotorModel(**stub_motor_dump) - ) + response = client.post('/motors/', json=stub_motor_dump) + assert response.status_code == 201 + assert response.json() == { + 'motor_id': '123', + 'message': 'Motor successfully created', + } + mock_controller_instance.post_motor.assert_called_once_with( + MotorModel(**stub_motor_dump) + ) def test_create_liquid_motor_ullage_tank( stub_motor_dump, stub_ullage_tank_dump, mock_controller_instance ): - stub_motor_dump.update({'tanks': [stub_ullage_tank_dump]}) + stub_motor_dump.update( + {'tanks': [stub_ullage_tank_dump], 'motor_kind': 'LIQUID'} + ) mock_response = AsyncMock(return_value=MotorCreated(motor_id='123')) mock_controller_instance.post_motor = mock_response - with patch.object( - MotorModel, 'set_motor_kind', side_effect=None - ) as mock_set_motor_kind: - response = client.post( - '/motors/', json=stub_motor_dump, params={'motor_kind': 'LIQUID'} - ) - assert response.status_code == 201 - assert response.json() == { - 'motor_id': '123', - 'message': 'Motor successfully created', - } - mock_set_motor_kind.assert_called_once_with(MotorKinds.LIQUID) - mock_controller_instance.post_motor.assert_called_once_with( - MotorModel(**stub_motor_dump) - ) + response = client.post('/motors/', json=stub_motor_dump) + assert response.status_code == 201 + assert response.json() == { + 'motor_id': '123', + 'message': 'Motor successfully created', + } + mock_controller_instance.post_motor.assert_called_once_with( + MotorModel(**stub_motor_dump) + ) def test_create_liquid_motor_mass_tank( stub_motor_dump, stub_mass_tank_dump, mock_controller_instance ): - stub_motor_dump.update({'tanks': [stub_mass_tank_dump]}) + stub_motor_dump.update( + {'tanks': [stub_mass_tank_dump], 'motor_kind': 'LIQUID'} + ) mock_response = AsyncMock(return_value=MotorCreated(motor_id='123')) mock_controller_instance.post_motor = mock_response - with patch.object( - MotorModel, 'set_motor_kind', side_effect=None - ) as mock_set_motor_kind: - response = client.post( - '/motors/', json=stub_motor_dump, params={'motor_kind': 'LIQUID'} - ) - assert response.status_code == 201 - assert response.json() == { - 'motor_id': '123', - 'message': 'Motor successfully created', - } - mock_set_motor_kind.assert_called_once_with(MotorKinds.LIQUID) - mock_controller_instance.post_motor.assert_called_once_with( - MotorModel(**stub_motor_dump) - ) + response = client.post('/motors/', json=stub_motor_dump) + assert response.status_code == 201 + assert response.json() == { + 'motor_id': '123', + 'message': 'Motor successfully created', + } + mock_controller_instance.post_motor.assert_called_once_with( + MotorModel(**stub_motor_dump) + ) def test_create_hybrid_motor( @@ -223,26 +187,21 @@ def test_create_hybrid_motor( 'grains_center_of_mass_position': 0, 'grain_separation': 0, 'throat_radius': 0, + 'motor_kind': 'HYBRID', 'tanks': [stub_level_tank_dump], } ) mock_response = AsyncMock(return_value=MotorCreated(motor_id='123')) mock_controller_instance.post_motor = mock_response - with patch.object( - MotorModel, 'set_motor_kind', side_effect=None - ) as mock_set_motor_kind: - response = client.post( - '/motors/', json=stub_motor_dump, params={'motor_kind': 'HYBRID'} - ) - assert response.status_code == 201 - assert response.json() == { - 'motor_id': '123', - 'message': 'Motor successfully created', - } - mock_set_motor_kind.assert_called_once_with(MotorKinds.HYBRID) - mock_controller_instance.post_motor.assert_called_once_with( - MotorModel(**stub_motor_dump) - ) + response = client.post('/motors/', json=stub_motor_dump) + assert response.status_code == 201 + assert response.json() == { + 'motor_id': '123', + 'message': 'Motor successfully created', + } + mock_controller_instance.post_motor.assert_called_once_with( + MotorModel(**stub_motor_dump) + ) def test_create_solid_motor(stub_motor_dump, mock_controller_instance): @@ -255,25 +214,20 @@ def test_create_solid_motor(stub_motor_dump, mock_controller_instance): 'grain_initial_height': 0, 'grains_center_of_mass_position': 0, 'grain_separation': 0, + 'motor_kind': 'SOLID', } ) mock_response = AsyncMock(return_value=MotorCreated(motor_id='123')) mock_controller_instance.post_motor = mock_response - with patch.object( - MotorModel, 'set_motor_kind', side_effect=None - ) as mock_set_motor_kind: - response = client.post( - '/motors/', json=stub_motor_dump, params={'motor_kind': 'SOLID'} - ) - assert response.status_code == 201 - assert response.json() == { - 'motor_id': '123', - 'message': 'Motor successfully created', - } - mock_set_motor_kind.assert_called_once_with(MotorKinds.SOLID) - mock_controller_instance.post_motor.assert_called_once_with( - MotorModel(**stub_motor_dump) - ) + response = client.post('/motors/', json=stub_motor_dump) + assert response.status_code == 201 + assert response.json() == { + 'motor_id': '123', + 'message': 'Motor successfully created', + } + mock_controller_instance.post_motor.assert_called_once_with( + MotorModel(**stub_motor_dump) + ) def test_create_motor_invalid_input(): @@ -283,21 +237,25 @@ def test_create_motor_invalid_input(): assert response.status_code == 422 +def test_create_motor_invalid_input_post_instantiation(stub_motor_dump): + stub_motor_dump.update( + { + 'motor_kind': 'HYBRID', + } + ) + response = client.post('/motors/', json=stub_motor_dump) + assert response.status_code == 422 + + def test_create_motor_server_error(stub_motor_dump, mock_controller_instance): mock_response = AsyncMock(side_effect=HTTPException(status_code=500)) mock_controller_instance.post_motor = mock_response - with patch.object( - MotorModel, 'set_motor_kind', side_effect=None - ) as mock_set_motor_kind: - response = client.post( - '/motors/', json=stub_motor_dump, params={'motor_kind': 'HYBRID'} - ) - assert response.status_code == 500 - assert response.json() == {'detail': 'Internal Server Error'} - mock_set_motor_kind.assert_called_once_with(MotorKinds.HYBRID) - mock_controller_instance.post_motor.assert_called_once_with( - MotorModel(**stub_motor_dump) - ) + response = client.post('/motors/', json=stub_motor_dump) + assert response.status_code == 500 + assert response.json() == {'detail': 'Internal Server Error'} + mock_controller_instance.post_motor.assert_called_once_with( + MotorModel(**stub_motor_dump) + ) def test_read_motor(stub_motor_dump, mock_controller_instance): @@ -333,26 +291,16 @@ def test_read_motor_server_error(mock_controller_instance): def test_update_motor(stub_motor_dump, mock_controller_instance): mock_response = AsyncMock(return_value=None) mock_controller_instance.put_motor_by_id = mock_response - with patch.object( - MotorModel, 'set_motor_kind', side_effect=None - ) as mock_set_motor_kind: - response = client.put( - '/motors/123', - json=stub_motor_dump, - params={'motor_kind': 'HYBRID'}, - ) - assert response.status_code == 204 - mock_set_motor_kind.assert_called_once_with(MotorKinds.HYBRID) - mock_controller_instance.put_motor_by_id.assert_called_once_with( - '123', MotorModel(**stub_motor_dump) - ) + response = client.put('/motors/123', json=stub_motor_dump) + assert response.status_code == 204 + mock_controller_instance.put_motor_by_id.assert_called_once_with( + '123', MotorModel(**stub_motor_dump) + ) def test_update_motor_invalid_input(): response = client.put( - '/motors/123', - json={'burn_time': 'foo', 'nozzle_radius': 'bar'}, - params={'motor_kind': 'HYBRID'}, + '/motors/123', json={'burn_time': 'foo', 'nozzle_radius': 'bar'} ) assert response.status_code == 422 @@ -360,39 +308,23 @@ def test_update_motor_invalid_input(): def test_update_motor_not_found(stub_motor_dump, mock_controller_instance): mock_response = AsyncMock(side_effect=HTTPException(status_code=404)) mock_controller_instance.put_motor_by_id = mock_response - with patch.object( - MotorModel, 'set_motor_kind', side_effect=None - ) as mock_set_motor_kind: - response = client.put( - '/motors/123', - json=stub_motor_dump, - params={'motor_kind': 'HYBRID'}, - ) - assert response.status_code == 404 - assert response.json() == {'detail': 'Not Found'} - mock_set_motor_kind.assert_called_once_with(MotorKinds.HYBRID) - mock_controller_instance.put_motor_by_id.assert_called_once_with( - '123', MotorModel(**stub_motor_dump) - ) + response = client.put('/motors/123', json=stub_motor_dump) + assert response.status_code == 404 + assert response.json() == {'detail': 'Not Found'} + mock_controller_instance.put_motor_by_id.assert_called_once_with( + '123', MotorModel(**stub_motor_dump) + ) def test_update_motor_server_error(stub_motor_dump, mock_controller_instance): mock_response = AsyncMock(side_effect=HTTPException(status_code=500)) mock_controller_instance.put_motor_by_id = mock_response - with patch.object( - MotorModel, 'set_motor_kind', side_effect=None - ) as mock_set_motor_kind: - response = client.put( - '/motors/123', - json=stub_motor_dump, - params={'motor_kind': 'HYBRID'}, - ) - assert response.status_code == 500 - assert response.json() == {'detail': 'Internal Server Error'} - mock_set_motor_kind.assert_called_once_with(MotorKinds.HYBRID) - mock_controller_instance.put_motor_by_id.assert_called_once_with( - '123', MotorModel(**stub_motor_dump) - ) + response = client.put('/motors/123', json=stub_motor_dump) + assert response.status_code == 500 + assert response.json() == {'detail': 'Internal Server Error'} + mock_controller_instance.put_motor_by_id.assert_called_once_with( + '123', MotorModel(**stub_motor_dump) + ) def test_delete_motor(mock_controller_instance): diff --git a/tests/unit/test_routes/test_rockets_route.py b/tests/unit/test_routes/test_rockets_route.py index 8c15771..3d53df9 100644 --- a/tests/unit/test_routes/test_rockets_route.py +++ b/tests/unit/test_routes/test_rockets_route.py @@ -9,10 +9,6 @@ Parachute, ) from src.models.rocket import RocketModel -from src.models.motor import ( - MotorModel, - MotorKinds, -) from src.views.rocket import ( RocketCreated, RocketRetrieved, @@ -88,21 +84,15 @@ def mock_controller_instance(): def test_create_rocket(stub_rocket_dump, mock_controller_instance): mock_response = AsyncMock(return_value=RocketCreated(rocket_id='123')) mock_controller_instance.post_rocket = mock_response - with patch.object( - MotorModel, 'set_motor_kind', side_effect=None - ) as mock_set_motor_kind: - response = client.post( - '/rockets/', json=stub_rocket_dump, params={'motor_kind': 'HYBRID'} - ) - assert response.status_code == 201 - assert response.json() == { - 'rocket_id': '123', - 'message': 'Rocket successfully created', - } - mock_set_motor_kind.assert_called_once_with(MotorKinds.HYBRID) - mock_controller_instance.post_rocket.assert_called_once_with( - RocketModel(**stub_rocket_dump) - ) + response = client.post('/rockets/', json=stub_rocket_dump) + assert response.status_code == 201 + assert response.json() == { + 'rocket_id': '123', + 'message': 'Rocket successfully created', + } + mock_controller_instance.post_rocket.assert_called_once_with( + RocketModel(**stub_rocket_dump) + ) def test_create_rocket_optional_params( @@ -121,21 +111,15 @@ def test_create_rocket_optional_params( ) mock_response = AsyncMock(return_value=RocketCreated(rocket_id='123')) mock_controller_instance.post_rocket = mock_response - with patch.object( - MotorModel, 'set_motor_kind', side_effect=None - ) as mock_set_motor_kind: - response = client.post( - '/rockets/', json=stub_rocket_dump, params={'motor_kind': 'HYBRID'} - ) - assert response.status_code == 201 - assert response.json() == { - 'rocket_id': '123', - 'message': 'Rocket successfully created', - } - mock_set_motor_kind.assert_called_once_with(MotorKinds.HYBRID) - mock_controller_instance.post_rocket.assert_called_once_with( - RocketModel(**stub_rocket_dump) - ) + response = client.post('/rockets/', json=stub_rocket_dump) + assert response.status_code == 201 + assert response.json() == { + 'rocket_id': '123', + 'message': 'Rocket successfully created', + } + mock_controller_instance.post_rocket.assert_called_once_with( + RocketModel(**stub_rocket_dump) + ) def test_create_generic_motor_rocket( @@ -148,28 +132,21 @@ def test_create_generic_motor_rocket( 'chamber_position': 0, 'propellant_initial_mass': 0, 'nozzle_position': 0, + 'motor_kind': 'GENERIC', } ) stub_rocket_dump.update({'motor': stub_motor_dump}) mock_response = AsyncMock(return_value=RocketCreated(rocket_id='123')) mock_controller_instance.post_rocket = mock_response - with patch.object( - MotorModel, 'set_motor_kind', side_effect=None - ) as mock_set_motor_kind: - response = client.post( - '/rockets/', - json=stub_rocket_dump, - params={'motor_kind': 'GENERIC'}, - ) - assert response.status_code == 201 - assert response.json() == { - 'rocket_id': '123', - 'message': 'Rocket successfully created', - } - mock_set_motor_kind.assert_called_once_with(MotorKinds.GENERIC) - mock_controller_instance.post_rocket.assert_called_once_with( - RocketModel(**stub_rocket_dump) - ) + response = client.post('/rockets/', json=stub_rocket_dump) + assert response.status_code == 201 + assert response.json() == { + 'rocket_id': '123', + 'message': 'Rocket successfully created', + } + mock_controller_instance.post_rocket.assert_called_once_with( + RocketModel(**stub_rocket_dump) + ) def test_create_liquid_motor_level_tank_rocket( @@ -178,25 +155,21 @@ def test_create_liquid_motor_level_tank_rocket( stub_level_tank_dump, mock_controller_instance, ): - stub_motor_dump.update({'tanks': [stub_level_tank_dump]}) + stub_motor_dump.update( + {'tanks': [stub_level_tank_dump], 'motor_kind': 'LIQUID'} + ) stub_rocket_dump.update({'motor': stub_motor_dump}) mock_response = AsyncMock(return_value=RocketCreated(rocket_id='123')) mock_controller_instance.post_rocket = mock_response - with patch.object( - MotorModel, 'set_motor_kind', side_effect=None - ) as mock_set_motor_kind: - response = client.post( - '/rockets/', json=stub_rocket_dump, params={'motor_kind': 'LIQUID'} - ) - assert response.status_code == 201 - assert response.json() == { - 'rocket_id': '123', - 'message': 'Rocket successfully created', - } - mock_set_motor_kind.assert_called_once_with(MotorKinds.LIQUID) - mock_controller_instance.post_rocket.assert_called_once_with( - RocketModel(**stub_rocket_dump) - ) + response = client.post('/rockets/', json=stub_rocket_dump) + assert response.status_code == 201 + assert response.json() == { + 'rocket_id': '123', + 'message': 'Rocket successfully created', + } + mock_controller_instance.post_rocket.assert_called_once_with( + RocketModel(**stub_rocket_dump) + ) def test_create_liquid_motor_mass_flow_tank_rocket( @@ -205,25 +178,21 @@ def test_create_liquid_motor_mass_flow_tank_rocket( stub_mass_flow_tank_dump, mock_controller_instance, ): - stub_motor_dump.update({'tanks': [stub_mass_flow_tank_dump]}) + stub_motor_dump.update( + {'tanks': [stub_mass_flow_tank_dump], 'motor_kind': 'LIQUID'} + ) stub_rocket_dump.update({'motor': stub_motor_dump}) mock_response = AsyncMock(return_value=RocketCreated(rocket_id='123')) mock_controller_instance.post_rocket = mock_response - with patch.object( - MotorModel, 'set_motor_kind', side_effect=None - ) as mock_set_motor_kind: - response = client.post( - '/rockets/', json=stub_rocket_dump, params={'motor_kind': 'LIQUID'} - ) - assert response.status_code == 201 - assert response.json() == { - 'rocket_id': '123', - 'message': 'Rocket successfully created', - } - mock_set_motor_kind.assert_called_once_with(MotorKinds.LIQUID) - mock_controller_instance.post_rocket.assert_called_once_with( - RocketModel(**stub_rocket_dump) - ) + response = client.post('/rockets/', json=stub_rocket_dump) + assert response.status_code == 201 + assert response.json() == { + 'rocket_id': '123', + 'message': 'Rocket successfully created', + } + mock_controller_instance.post_rocket.assert_called_once_with( + RocketModel(**stub_rocket_dump) + ) def test_create_liquid_motor_ullage_tank_rocket( @@ -232,25 +201,21 @@ def test_create_liquid_motor_ullage_tank_rocket( stub_ullage_tank_dump, mock_controller_instance, ): - stub_motor_dump.update({'tanks': [stub_ullage_tank_dump]}) + stub_motor_dump.update( + {'tanks': [stub_ullage_tank_dump], 'motor_kind': 'LIQUID'} + ) stub_rocket_dump.update({'motor': stub_motor_dump}) mock_response = AsyncMock(return_value=RocketCreated(rocket_id='123')) mock_controller_instance.post_rocket = mock_response - with patch.object( - MotorModel, 'set_motor_kind', side_effect=None - ) as mock_set_motor_kind: - response = client.post( - '/rockets/', json=stub_rocket_dump, params={'motor_kind': 'LIQUID'} - ) - assert response.status_code == 201 - assert response.json() == { - 'rocket_id': '123', - 'message': 'Rocket successfully created', - } - mock_set_motor_kind.assert_called_once_with(MotorKinds.LIQUID) - mock_controller_instance.post_rocket.assert_called_once_with( - RocketModel(**stub_rocket_dump) - ) + response = client.post('/rockets/', json=stub_rocket_dump) + assert response.status_code == 201 + assert response.json() == { + 'rocket_id': '123', + 'message': 'Rocket successfully created', + } + mock_controller_instance.post_rocket.assert_called_once_with( + RocketModel(**stub_rocket_dump) + ) def test_create_liquid_motor_mass_tank_rocket( @@ -259,25 +224,21 @@ def test_create_liquid_motor_mass_tank_rocket( stub_mass_tank_dump, mock_controller_instance, ): - stub_motor_dump.update({'tanks': [stub_mass_tank_dump]}) + stub_motor_dump.update( + {'tanks': [stub_mass_tank_dump], 'motor_kind': 'LIQUID'} + ) stub_rocket_dump.update({'motor': stub_motor_dump}) mock_response = AsyncMock(return_value=RocketCreated(rocket_id='123')) mock_controller_instance.post_rocket = mock_response - with patch.object( - MotorModel, 'set_motor_kind', side_effect=None - ) as mock_set_motor_kind: - response = client.post( - '/rockets/', json=stub_rocket_dump, params={'motor_kind': 'LIQUID'} - ) - assert response.status_code == 201 - assert response.json() == { - 'rocket_id': '123', - 'message': 'Rocket successfully created', - } - mock_set_motor_kind.assert_called_once_with(MotorKinds.LIQUID) - mock_controller_instance.post_rocket.assert_called_once_with( - RocketModel(**stub_rocket_dump) - ) + response = client.post('/rockets/', json=stub_rocket_dump) + assert response.status_code == 201 + assert response.json() == { + 'rocket_id': '123', + 'message': 'Rocket successfully created', + } + mock_controller_instance.post_rocket.assert_called_once_with( + RocketModel(**stub_rocket_dump) + ) def test_create_hybrid_motor_rocket( @@ -296,27 +257,22 @@ def test_create_hybrid_motor_rocket( 'grains_center_of_mass_position': 0, 'grain_separation': 0, 'throat_radius': 0, + 'motor_kind': 'HYBRID', 'tanks': [stub_level_tank_dump], } ) stub_rocket_dump.update({'motor': stub_motor_dump}) mock_response = AsyncMock(return_value=RocketCreated(rocket_id='123')) mock_controller_instance.post_rocket = mock_response - with patch.object( - MotorModel, 'set_motor_kind', side_effect=None - ) as mock_set_motor_kind: - response = client.post( - '/rockets/', json=stub_rocket_dump, params={'motor_kind': 'HYBRID'} - ) - assert response.status_code == 201 - assert response.json() == { - 'rocket_id': '123', - 'message': 'Rocket successfully created', - } - mock_set_motor_kind.assert_called_once_with(MotorKinds.HYBRID) - mock_controller_instance.post_rocket.assert_called_once_with( - RocketModel(**stub_rocket_dump) - ) + response = client.post('/rockets/', json=stub_rocket_dump) + assert response.status_code == 201 + assert response.json() == { + 'rocket_id': '123', + 'message': 'Rocket successfully created', + } + mock_controller_instance.post_rocket.assert_called_once_with( + RocketModel(**stub_rocket_dump) + ) def test_create_solid_motor_rocket( @@ -331,26 +287,21 @@ def test_create_solid_motor_rocket( 'grain_initial_height': 0, 'grains_center_of_mass_position': 0, 'grain_separation': 0, + 'motor_kind': 'SOLID', } ) stub_rocket_dump.update({'motor': stub_motor_dump}) mock_response = AsyncMock(return_value=RocketCreated(rocket_id='123')) mock_controller_instance.post_rocket = mock_response - with patch.object( - MotorModel, 'set_motor_kind', side_effect=None - ) as mock_set_motor_kind: - response = client.post( - '/rockets/', json=stub_rocket_dump, params={'motor_kind': 'SOLID'} - ) - assert response.status_code == 201 - assert response.json() == { - 'rocket_id': '123', - 'message': 'Rocket successfully created', - } - mock_set_motor_kind.assert_called_once_with(MotorKinds.SOLID) - mock_controller_instance.post_rocket.assert_called_once_with( - RocketModel(**stub_rocket_dump) - ) + response = client.post('/rockets/', json=stub_rocket_dump) + assert response.status_code == 201 + assert response.json() == { + 'rocket_id': '123', + 'message': 'Rocket successfully created', + } + mock_controller_instance.post_rocket.assert_called_once_with( + RocketModel(**stub_rocket_dump) + ) def test_create_rocket_invalid_input(): @@ -364,9 +315,7 @@ def test_create_rocket_server_error( mock_controller_instance.post_rocket.side_effect = HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR ) - response = client.post( - '/rockets/', json=stub_rocket_dump, params={'motor_kind': 'HYBRID'} - ) + response = client.post('/rockets/', json=stub_rocket_dump) assert response.status_code == 500 assert response.json() == {'detail': 'Internal Server Error'} @@ -406,28 +355,19 @@ def test_read_rocket_server_error(mock_controller_instance): def test_update_rocket(stub_rocket_dump, mock_controller_instance): + stub_rocket_dump['motor']['motor_kind'] = 'SOLID' mock_response = AsyncMock(return_value=None) mock_controller_instance.put_rocket_by_id = mock_response - with patch.object( - MotorModel, 'set_motor_kind', side_effect=None - ) as mock_set_motor_kind: - response = client.put( - '/rockets/123', - json=stub_rocket_dump, - params={'motor_kind': 'HYBRID'}, - ) - assert response.status_code == 204 - mock_set_motor_kind.assert_called_once_with(MotorKinds.HYBRID) - mock_controller_instance.put_rocket_by_id.assert_called_once_with( - '123', RocketModel(**stub_rocket_dump) - ) + response = client.put('/rockets/123', json=stub_rocket_dump) + assert response.status_code == 204 + mock_controller_instance.put_rocket_by_id.assert_called_once_with( + '123', RocketModel(**stub_rocket_dump) + ) def test_update_rocket_invalid_input(): response = client.put( - '/rockets/123', - json={'mass': 'foo', 'radius': 'bar'}, - params={'motor_kind': 'GENERIC'}, + '/rockets/123', json={'mass': 'foo', 'radius': 'bar'} ) assert response.status_code == 422 @@ -436,11 +376,7 @@ def test_update_rocket_not_found(stub_rocket_dump, mock_controller_instance): mock_controller_instance.put_rocket_by_id.side_effect = HTTPException( status_code=status.HTTP_404_NOT_FOUND ) - response = client.put( - '/rockets/123', - json=stub_rocket_dump, - params={'motor_kind': 'HYBRID'}, - ) + response = client.put('/rockets/123', json=stub_rocket_dump) assert response.status_code == 404 assert response.json() == {'detail': 'Not Found'} @@ -451,11 +387,7 @@ def test_update_rocket_server_error( mock_controller_instance.put_rocket_by_id.side_effect = HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR ) - response = client.put( - '/rockets/123', - json=stub_rocket_dump, - params={'motor_kind': 'HYBRID'}, - ) + response = client.put('/rockets/123', json=stub_rocket_dump) assert response.status_code == 500 assert response.json() == {'detail': 'Internal Server Error'} From a46116af9a5328dc7317c7032339f23245cd2d13 Mon Sep 17 00:00:00 2001 From: Gabriel Barberini Date: Sat, 22 Feb 2025 18:32:54 -0300 Subject: [PATCH 18/34] typo fix --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index a2ee8d9..1ae6f8e 100644 --- a/Makefile +++ b/Makefile @@ -21,7 +21,7 @@ test: python3 -m pytest . dev: - python3 -m uvicorn src.app --reload --port 3000 + python3 -m uvicorn src:app --reload --port 3000 clean: docker stop infinity-api From 469a13e52fd26f85cbe85d4eadbef6b02886de49 Mon Sep 17 00:00:00 2001 From: Gabriel Barberini Date: Sat, 22 Feb 2025 19:01:24 -0300 Subject: [PATCH 19/34] fixes pylint issues --- pyproject.toml | 1 + src/utils.py | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 45b11af..a88cbc7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -54,6 +54,7 @@ disable = """ too-many-positional-arguments, no-member, protected-access, + import-outside-toplevel, """ [tool.ruff] diff --git a/src/utils.py b/src/utils.py index a6dd5a4..eeda455 100644 --- a/src/utils.py +++ b/src/utils.py @@ -95,13 +95,13 @@ async def send_with_gzip(self, message: Message) -> None: self.started = True body = message.get("body", b"") more_body = message.get("more_body", False) - if (len(body) < (self.minimum_size and not more_body)) or any( + if ((len(body) < self.minimum_size) and not more_body) or any( value == b'application/octet-stream' for header, value in self.initial_message["headers"] ): # Don't apply GZip to small outgoing responses or octet-streams. await self.send(self.initial_message) - await self.send(message) + await self.send(message) # pylint: disable=unreachable elif not more_body: # Standard GZip response. self.gzip_file.write(body) @@ -115,7 +115,7 @@ async def send_with_gzip(self, message: Message) -> None: message["body"] = body await self.send(self.initial_message) - await self.send(message) + await self.send(message) # pylint: disable=unreachable else: # Initial body in streaming GZip response. headers = MutableHeaders(raw=self.initial_message["headers"]) @@ -129,7 +129,7 @@ async def send_with_gzip(self, message: Message) -> None: self.gzip_buffer.truncate() await self.send(self.initial_message) - await self.send(message) + await self.send(message) # pylint: disable=unreachable elif message_type == "http.response.body": # Remaining body in streaming GZip response. From f3074b7c213812dd44b649368ded4e2a07f0562b Mon Sep 17 00:00:00 2001 From: Gabriel Barberini Date: Sat, 22 Feb 2025 19:04:05 -0300 Subject: [PATCH 20/34] Fixes enum value inconsistency Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- src/models/sub/tanks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/models/sub/tanks.py b/src/models/sub/tanks.py index 92f39b8..0873264 100644 --- a/src/models/sub/tanks.py +++ b/src/models/sub/tanks.py @@ -6,7 +6,7 @@ class TankKinds(str, Enum): LEVEL: str = "LEVEL" MASS: str = "MASS" - MASS_FLOW: str = "MASSFLOW" + MASS_FLOW: str = "MASS_FLOW" ULLAGE: str = "ULLAGE" From 4a0408bc48f39fa1e377fc7890d12554ef163a73 Mon Sep 17 00:00:00 2001 From: Gabriel Barberini Date: Sat, 22 Feb 2025 19:04:29 -0300 Subject: [PATCH 21/34] Replaces assert with explicit validation Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- src/repositories/interface.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/repositories/interface.py b/src/repositories/interface.py index dcfa619..025d25c 100644 --- a/src/repositories/interface.py +++ b/src/repositories/interface.py @@ -215,10 +215,12 @@ def get_model_repo(cls, model: ApiBaseModel) -> Self: @repository_exception_handler async def insert(self, data: dict): collection = self.get_collection() - assert self.model.model_validate(data) + try: + self.model.model_validate(data) + except ValidationError as e: + raise HTTPException(status_code=422, detail=str(e)) result = await collection.insert_one(data) return str(result.inserted_id) - @repository_exception_handler async def update_by_id(self, data: dict, *, data_id: str): collection = self.get_collection() From e0acdc95df948bfcf448cca278e1fff03575f9b0 Mon Sep 17 00:00:00 2001 From: Gabriel Barberini Date: Sat, 22 Feb 2025 19:05:45 -0300 Subject: [PATCH 22/34] Fixes typo in method name Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- src/controllers/flight.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/controllers/flight.py b/src/controllers/flight.py index 6f9dbbb..a8afa57 100644 --- a/src/controllers/flight.py +++ b/src/controllers/flight.py @@ -109,4 +109,4 @@ async def get_flight_simulation( flight_service = FlightService.from_flight_model( flight_retrieved.flight ) - return flight_service.get_flight_simmulation() + return flight_service.get_flight_simulation() From ae84bbc62a4a62d2f6dbd1f9cac48c38ebdb54a5 Mon Sep 17 00:00:00 2001 From: Gabriel Barberini Date: Sat, 22 Feb 2025 19:06:29 -0300 Subject: [PATCH 23/34] adds await keyword for async method call Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- src/controllers/flight.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/controllers/flight.py b/src/controllers/flight.py index a8afa57..b066dda 100644 --- a/src/controllers/flight.py +++ b/src/controllers/flight.py @@ -40,7 +40,7 @@ async def update_environment_by_flight_id( """ flight = await self.get_flight_by_id(flight_id) flight.environment = environment - self.update_flight_by_id(flight_id, flight) + await self.update_flight_by_id(flight_id, flight) return @controller_exception_handler From d7877fdb39d63534080727e8a799511d2c8fc55f Mon Sep 17 00:00:00 2001 From: Gabriel Barberini Date: Sat, 22 Feb 2025 19:07:14 -0300 Subject: [PATCH 24/34] Adds await keyword for async method calls Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- src/controllers/flight.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/controllers/flight.py b/src/controllers/flight.py index b066dda..2845bbf 100644 --- a/src/controllers/flight.py +++ b/src/controllers/flight.py @@ -62,7 +62,7 @@ async def update_rocket_by_flight_id( """ flight = await self.get_flight_by_id(flight_id) flight.rocket = rocket - self.update_flight_by_id(flight_id, flight) + await self.update_flight_by_id(flight_id, flight) return @controller_exception_handler From 3f028cfc6bc0815025613ddb31797b8fff4f3fd1 Mon Sep 17 00:00:00 2001 From: Gabriel Barberini Date: Sat, 22 Feb 2025 19:09:10 -0300 Subject: [PATCH 25/34] removes snippet garbage --- src/routes/environment.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/routes/environment.py b/src/routes/environment.py index a343df8..b8d7f83 100644 --- a/src/routes/environment.py +++ b/src/routes/environment.py @@ -64,10 +64,10 @@ async def update_environment( ## Args ``` environment_id: str - becho: models.Becho JSON + environment: models.Environment JSON ``` """ - with tracer.start_as_current_span("update_becho"): + with tracer.start_as_current_span("update_environment"): controller = EnvironmentController() return await controller.put_environment_by_id( environment_id, environment @@ -82,7 +82,7 @@ async def delete_environment(environment_id: str) -> None: ## Args ``` environment_id: str ``` """ - with tracer.start_as_current_span("delete_becho"): + with tracer.start_as_current_span("delete_environment"): controller = EnvironmentController() return await controller.delete_environment_by_id(environment_id) From aaab1a5d74b6f2481a4953c2b43a4590efbe4f22 Mon Sep 17 00:00:00 2001 From: Gabriel Barberini Date: Sat, 22 Feb 2025 19:19:16 -0300 Subject: [PATCH 26/34] solves nitpick comments --- src/repositories/interface.py | 3 ++- src/utils.py | 17 ++++++++++++----- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/repositories/interface.py b/src/repositories/interface.py index 025d25c..6639e27 100644 --- a/src/repositories/interface.py +++ b/src/repositories/interface.py @@ -9,7 +9,7 @@ wait_fixed, retry, ) -from pydantic import BaseModel +from pydantic import BaseModel, ValidationError from pymongo.errors import PyMongoError from pymongo.server_api import ServerApi from motor.motor_asyncio import AsyncIOMotorClient @@ -221,6 +221,7 @@ async def insert(self, data: dict): raise HTTPException(status_code=422, detail=str(e)) result = await collection.insert_one(data) return str(result.inserted_id) + @repository_exception_handler async def update_by_id(self, data: dict, *, data_id: str): collection = self.get_collection() diff --git a/src/utils.py b/src/utils.py index eeda455..0a8ba45 100644 --- a/src/utils.py +++ b/src/utils.py @@ -5,12 +5,22 @@ from typing import Annotated, NoReturn, Any import numpy as np -from pydantic import BeforeValidator, PlainSerializer +from pydantic import PlainSerializer from starlette.datastructures import Headers, MutableHeaders from starlette.types import ASGIApp, Message, Receive, Scope, Send -def to_python_primitive(v): +def to_python_primitive(v: Any) -> Any: + """ + Convert complex types to Python primitives. + + Args: + v: Any value, particularly those with a 'source' attribute + containing numpy arrays or generic types. + + Returns: + The primitive representation of the input value. + """ if hasattr(v, "source"): if isinstance(v.source, np.ndarray): return v.source.tolist() @@ -19,13 +29,11 @@ def to_python_primitive(v): return v.source.item() return str(v.source) - return str(v) AnyToPrimitive = Annotated[ Any, - BeforeValidator(lambda v: v), PlainSerializer(to_python_primitive), ] @@ -145,7 +153,6 @@ async def send_with_gzip(self, message: Message) -> None: self.gzip_buffer.truncate() await self.send(message) - return async def unattached_send(message: Message) -> NoReturn: From 40c7a9144edc2a78792253db91bc8eb058837706 Mon Sep 17 00:00:00 2001 From: Gabriel Barberini Date: Sat, 22 Feb 2025 19:29:23 -0300 Subject: [PATCH 27/34] improves HTTP code semantics --- src/routes/environment.py | 6 +++--- src/routes/flight.py | 6 +++--- src/routes/motor.py | 6 +++--- src/routes/rocket.py | 6 +++--- tests/unit/test_routes/test_environments_route.py | 2 +- tests/unit/test_routes/test_flights_route.py | 2 +- tests/unit/test_routes/test_motors_route.py | 2 +- tests/unit/test_routes/test_rockets_route.py | 2 +- 8 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/routes/environment.py b/src/routes/environment.py index b8d7f83..6c5e8b2 100644 --- a/src/routes/environment.py +++ b/src/routes/environment.py @@ -90,12 +90,12 @@ async def delete_environment(environment_id: str) -> None: @router.get( "/{environment_id}/rocketpy", responses={ - 203: { + 200: { "description": "Binary file download", "content": {"application/octet-stream": {}}, } }, - status_code=203, + status_code=200, response_class=Response, ) async def get_rocketpy_environment_binary(environment_id: str): @@ -118,7 +118,7 @@ async def get_rocketpy_environment_binary(environment_id: str): content=binary, headers=headers, media_type="application/octet-stream", - status_code=203, + status_code=200, ) diff --git a/src/routes/flight.py b/src/routes/flight.py index 11e1ba2..9aff0db 100644 --- a/src/routes/flight.py +++ b/src/routes/flight.py @@ -86,12 +86,12 @@ async def delete_flight(flight_id: str) -> None: @router.get( "/{flight_id}/rocketpy", responses={ - 203: { + 200: { "description": "Binary file download", "content": {"application/octet-stream": {}}, } }, - status_code=203, + status_code=200, response_class=Response, ) async def get_rocketpy_flight_binary(flight_id: str): @@ -112,7 +112,7 @@ async def get_rocketpy_flight_binary(flight_id: str): content=binary, headers=headers, media_type="application/octet-stream", - status_code=203, + status_code=200, ) diff --git a/src/routes/motor.py b/src/routes/motor.py index 0227d51..3143c26 100644 --- a/src/routes/motor.py +++ b/src/routes/motor.py @@ -84,12 +84,12 @@ async def delete_motor(motor_id: str) -> None: @router.get( "/{motor_id}/rocketpy", responses={ - 203: { + 200: { "description": "Binary file download", "content": {"application/octet-stream": {}}, } }, - status_code=203, + status_code=200, response_class=Response, ) async def get_rocketpy_motor_binary(motor_id: str): @@ -110,7 +110,7 @@ async def get_rocketpy_motor_binary(motor_id: str): content=binary, headers=headers, media_type="application/octet-stream", - status_code=203, + status_code=200, ) diff --git a/src/routes/rocket.py b/src/routes/rocket.py index 13cbb8c..5346d9e 100644 --- a/src/routes/rocket.py +++ b/src/routes/rocket.py @@ -84,12 +84,12 @@ async def delete_rocket(rocket_id: str) -> None: @router.get( "/{rocket_id}/rocketpy", responses={ - 203: { + 200: { "description": "Binary file download", "content": {"application/octet-stream": {}}, } }, - status_code=203, + status_code=200, response_class=Response, ) async def get_rocketpy_rocket_binary(rocket_id: str): @@ -110,7 +110,7 @@ async def get_rocketpy_rocket_binary(rocket_id: str): content=binary, headers=headers, media_type="application/octet-stream", - status_code=203, + status_code=200, ) diff --git a/tests/unit/test_routes/test_environments_route.py b/tests/unit/test_routes/test_environments_route.py index 6ee7136..93ff4c3 100644 --- a/tests/unit/test_routes/test_environments_route.py +++ b/tests/unit/test_routes/test_environments_route.py @@ -229,7 +229,7 @@ def test_read_rocketpy_environment_binary(mock_controller_instance): mock_response = AsyncMock(return_value=b'rocketpy') mock_controller_instance.get_rocketpy_environment_binary = mock_response response = client.get('/environments/123/rocketpy') - assert response.status_code == 203 + assert response.status_code == 200 assert response.content == b'rocketpy' assert response.headers['content-type'] == 'application/octet-stream' mock_controller_instance.get_rocketpy_environment_binary.assert_called_once_with( diff --git a/tests/unit/test_routes/test_flights_route.py b/tests/unit/test_routes/test_flights_route.py index f860156..0b8ac30 100644 --- a/tests/unit/test_routes/test_flights_route.py +++ b/tests/unit/test_routes/test_flights_route.py @@ -339,7 +339,7 @@ def test_read_rocketpy_flight_binary(mock_controller_instance): return_value=b'rocketpy' ) response = client.get('/flights/123/rocketpy') - assert response.status_code == 203 + assert response.status_code == 200 assert response.content == b'rocketpy' assert response.headers['content-type'] == 'application/octet-stream' mock_controller_instance.get_rocketpy_flight_binary.assert_called_once_with( diff --git a/tests/unit/test_routes/test_motors_route.py b/tests/unit/test_routes/test_motors_route.py index 88a7291..e55c976 100644 --- a/tests/unit/test_routes/test_motors_route.py +++ b/tests/unit/test_routes/test_motors_route.py @@ -385,7 +385,7 @@ def test_read_rocketpy_motor_binary(mock_controller_instance): mock_response = AsyncMock(return_value=b'rocketpy') mock_controller_instance.get_rocketpy_motor_binary = mock_response response = client.get('/motors/123/rocketpy') - assert response.status_code == 203 + assert response.status_code == 200 assert response.content == b'rocketpy' assert response.headers['content-type'] == 'application/octet-stream' mock_controller_instance.get_rocketpy_motor_binary.assert_called_once_with( diff --git a/tests/unit/test_routes/test_rockets_route.py b/tests/unit/test_routes/test_rockets_route.py index 3d53df9..6b73d8c 100644 --- a/tests/unit/test_routes/test_rockets_route.py +++ b/tests/unit/test_routes/test_rockets_route.py @@ -446,7 +446,7 @@ def test_read_rocketpy_rocket_binary(mock_controller_instance): mock_response = AsyncMock(return_value=b'rocketpy') mock_controller_instance.get_rocketpy_rocket_binary = mock_response response = client.get('/rockets/123/rocketpy') - assert response.status_code == 203 + assert response.status_code == 200 assert response.content == b'rocketpy' assert response.headers['content-type'] == 'application/octet-stream' mock_controller_instance.get_rocketpy_rocket_binary.assert_called_once_with( From 7f539eef644b90e522598a063d10a35078058b09 Mon Sep 17 00:00:00 2001 From: Gabriel Barberini Date: Sat, 8 Mar 2025 10:21:01 -0300 Subject: [PATCH 28/34] Update src/models/flight.py Co-authored-by: Gui-FernandesBR <63590233+Gui-FernandesBR@users.noreply.github.com> --- src/models/flight.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/models/flight.py b/src/models/flight.py index 8b17d47..1f2c415 100644 --- a/src/models/flight.py +++ b/src/models/flight.py @@ -17,8 +17,8 @@ class FlightModel(ApiBaseModel): equations_of_motion: Literal['standard', 'solid_propulsion'] = 'standard' # Optional parameters - inclination: Optional[int] = None - heading: Optional[int] = None + inclination: float = 90.0 + heading: float = 0.0 # TODO: implement initial_solution max_time: Optional[int] = None max_time_step: Optional[float] = None From 871921de398af388be60c6e3883ddfbe97c46b03 Mon Sep 17 00:00:00 2001 From: Gabriel Barberini Date: Sat, 8 Mar 2025 10:21:14 -0300 Subject: [PATCH 29/34] Update src/models/flight.py Co-authored-by: Gui-FernandesBR <63590233+Gui-FernandesBR@users.noreply.github.com> --- src/models/flight.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/models/flight.py b/src/models/flight.py index 1f2c415..44ac16a 100644 --- a/src/models/flight.py +++ b/src/models/flight.py @@ -13,7 +13,7 @@ class FlightModel(ApiBaseModel): rocket: RocketModel rail_length: float = 1 time_overshoot: bool = True - terminate_on_apogee: bool = True + terminate_on_apogee: bool = False equations_of_motion: Literal['standard', 'solid_propulsion'] = 'standard' # Optional parameters From 54c8ce83b756a808eb5f6f186a176cd44f6ef7d9 Mon Sep 17 00:00:00 2001 From: Gabriel Barberini Date: Sat, 8 Mar 2025 10:22:43 -0300 Subject: [PATCH 30/34] Update src/models/environment.py Co-authored-by: Gui-FernandesBR <63590233+Gui-FernandesBR@users.noreply.github.com> --- src/models/environment.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/models/environment.py b/src/models/environment.py index 3682dd0..89315d2 100644 --- a/src/models/environment.py +++ b/src/models/environment.py @@ -8,7 +8,7 @@ class EnvironmentModel(ApiBaseModel): METHODS: ClassVar = ('POST', 'GET', 'PUT', 'DELETE') latitude: float longitude: float - elevation: Optional[int] = 1 + elevation: Optional[float] = 0.0 # Optional parameters atmospheric_model_type: Literal[ From 9ba7aa2c421f1bd78a4727322b5a941cca6e849b Mon Sep 17 00:00:00 2001 From: Gabriel Barberini Date: Sat, 8 Mar 2025 11:05:52 -0300 Subject: [PATCH 31/34] changes ControllerInterface to ControllerBase --- src/controllers/environment.py | 4 ++-- src/controllers/flight.py | 4 ++-- src/controllers/interface.py | 2 +- src/controllers/motor.py | 4 ++-- src/controllers/rocket.py | 4 ++-- .../unit/test_controllers/test_controller_interface.py | 10 +++++----- 6 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/controllers/environment.py b/src/controllers/environment.py index 8fb4bfe..3b5b075 100644 --- a/src/controllers/environment.py +++ b/src/controllers/environment.py @@ -1,5 +1,5 @@ from src.controllers.interface import ( - ControllerInterface, + ControllerBase, controller_exception_handler, ) from src.views.environment import EnvironmentSimulation @@ -7,7 +7,7 @@ from src.services.environment import EnvironmentService -class EnvironmentController(ControllerInterface): +class EnvironmentController(ControllerBase): """ Controller for the Environment model. diff --git a/src/controllers/flight.py b/src/controllers/flight.py index 2845bbf..ac8624a 100644 --- a/src/controllers/flight.py +++ b/src/controllers/flight.py @@ -1,5 +1,5 @@ from src.controllers.interface import ( - ControllerInterface, + ControllerBase, controller_exception_handler, ) from src.views.flight import FlightSimulation @@ -9,7 +9,7 @@ from src.services.flight import FlightService -class FlightController(ControllerInterface): +class FlightController(ControllerBase): """ Controller for the Flight model. diff --git a/src/controllers/interface.py b/src/controllers/interface.py index 83ad164..78d2e5a 100644 --- a/src/controllers/interface.py +++ b/src/controllers/interface.py @@ -36,7 +36,7 @@ async def wrapper(self, *args, **kwargs): return wrapper -class ControllerInterface: +class ControllerBase: def __init__(self, models: List[ApiBaseModel]): self._initialized_models = {} diff --git a/src/controllers/motor.py b/src/controllers/motor.py index dbd2980..e446cef 100644 --- a/src/controllers/motor.py +++ b/src/controllers/motor.py @@ -1,5 +1,5 @@ from src.controllers.interface import ( - ControllerInterface, + ControllerBase, controller_exception_handler, ) from src.views.motor import MotorSimulation @@ -7,7 +7,7 @@ from src.services.motor import MotorService -class MotorController(ControllerInterface): +class MotorController(ControllerBase): """ Controller for the motor model. diff --git a/src/controllers/rocket.py b/src/controllers/rocket.py index 80c98d3..a7dcb4d 100644 --- a/src/controllers/rocket.py +++ b/src/controllers/rocket.py @@ -1,5 +1,5 @@ from src.controllers.interface import ( - ControllerInterface, + ControllerBase, controller_exception_handler, ) from src.views.rocket import RocketSimulation @@ -7,7 +7,7 @@ from src.services.rocket import RocketService -class RocketController(ControllerInterface): +class RocketController(ControllerBase): """ Controller for the Rocket model. diff --git a/tests/unit/test_controllers/test_controller_interface.py b/tests/unit/test_controllers/test_controller_interface.py index 1dff129..9cfec3f 100644 --- a/tests/unit/test_controllers/test_controller_interface.py +++ b/tests/unit/test_controllers/test_controller_interface.py @@ -3,7 +3,7 @@ from pymongo.errors import PyMongoError from fastapi import HTTPException, status from src.controllers.interface import ( - ControllerInterface, + ControllerBase, controller_exception_handler, ) @@ -22,7 +22,7 @@ def stub_model(): @pytest.fixture def stub_controller(stub_model): - return ControllerInterface([stub_model]) + return ControllerBase([stub_model]) @pytest.mark.asyncio @@ -97,10 +97,10 @@ async def method(self, model, *args, **kwargs): def test_controller_interface_init(stub_model): with patch( - 'src.controllers.interface.ControllerInterface._generate_method' + 'src.controllers.interface.ControllerBase._generate_method' ) as mock_gen: mock_gen.return_value = lambda *args, **kwargs: True - stub_controller = ControllerInterface([stub_model]) + stub_controller = ControllerBase([stub_model]) assert stub_controller._initialized_models == { 'test_model': stub_model } @@ -120,7 +120,7 @@ async def test_controller_interface_generate_available_method( ): with patch('src.controllers.interface.RepositoryInterface') as mock_repo: with patch( - 'src.controllers.interface.ControllerInterface._get_model' + 'src.controllers.interface.ControllerBase._get_model' ) as mock_get: mock_get.return_value = stub_model method = stub_controller._generate_method('get', stub_model) From 499f505648b24ddb8f46fae09a83f775c5af4577 Mon Sep 17 00:00:00 2001 From: Gabriel Barberini Date: Sat, 8 Mar 2025 11:11:24 -0300 Subject: [PATCH 32/34] adds docstring to interfaces --- src/controllers/interface.py | 13 +++++++++++++ src/models/interface.py | 7 +++++++ src/repositories/interface.py | 9 +++++++++ 3 files changed, 29 insertions(+) diff --git a/src/controllers/interface.py b/src/controllers/interface.py index 78d2e5a..a3a6975 100644 --- a/src/controllers/interface.py +++ b/src/controllers/interface.py @@ -37,6 +37,19 @@ async def wrapper(self, *args, **kwargs): class ControllerBase: + """ + ControllerBase is a base class for all controllers. It provides a set of + methods to handle the CRUD operations for the models provided in the + constructor. CRUD methods are generated dynamically based on the model + methods and the model name. + + The methods are named as follows: + - post_{model_name} for POST method + - get_{model_name}_by_id for GET method + - put_{model_name}_by_id for PUT method + - delete_{model_name}_by_id for DELETE method + + """ def __init__(self, models: List[ApiBaseModel]): self._initialized_models = {} diff --git a/src/models/interface.py b/src/models/interface.py index 1703eab..bd5b99c 100644 --- a/src/models/interface.py +++ b/src/models/interface.py @@ -8,6 +8,13 @@ class ApiBaseModel(BaseModel, ABC): + """ + Base class for all models in the API. + + This class is used to define the common attributes and + methods that all models in the API should have. + """ + _id: Optional[str] = PrivateAttr(default=None) model_config = ConfigDict( use_enum_values=True, diff --git a/src/repositories/interface.py b/src/repositories/interface.py index 6639e27..f5dcc22 100644 --- a/src/repositories/interface.py +++ b/src/repositories/interface.py @@ -82,6 +82,15 @@ def get_instance(self): class RepositoryInterface: """ Interface class for all repositories (singleton) + + This class is used to define the common attributes and + methods that all repositories should have. + + The class is a singleton, meaning that only one instance + of the class is created and shared among all instances + of the class. This is done to ensure that only one + connection per collection in the database is created + and shared among all repositories. """ _global_instances = {} From 5bcd592f6af43bb821e23ae29b77feebd7461654 Mon Sep 17 00:00:00 2001 From: Gabriel Barberini Date: Sat, 8 Mar 2025 12:08:57 -0300 Subject: [PATCH 33/34] adds uvloop --- Dockerfile | 2 +- requirements.txt | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index ce53e71..2111d50 100644 --- a/Dockerfile +++ b/Dockerfile @@ -16,4 +16,4 @@ RUN apt-get update && \ COPY ./src /app/src -CMD ["gunicorn", "-c", "src/settings/gunicorn.py", "-w", "1", "--threads=2", "-k", "uvicorn.workers.UvicornWorker", "src.api:app", "--log-level", "Debug", "-b", "0.0.0.0:3000", "--timeout", "60"] +CMD ["gunicorn", "-c", "src/settings/gunicorn.py", "-w", "1", "--threads=2", "-k", "uvicorn.workers.UvicornWorker", "src.api:app", "--log-level", "Debug", "-b", "0.0.0.0:3000", "--timeout", "60", "--loop", "uvloop"] diff --git a/requirements.txt b/requirements.txt index 8d7b739..336559c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,6 +2,7 @@ motor dill python-dotenv fastapi +uvloop pydantic numpy==1.26.4 pymongo From b29ae1c87525c41038fd7184ef103a8823eebf7c Mon Sep 17 00:00:00 2001 From: Gabriel Barberini Date: Sat, 8 Mar 2025 12:13:31 -0300 Subject: [PATCH 34/34] adds uvloop to make dev --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 1ae6f8e..c65bc7f 100644 --- a/Makefile +++ b/Makefile @@ -21,7 +21,7 @@ test: python3 -m pytest . dev: - python3 -m uvicorn src:app --reload --port 3000 + python3 -m uvicorn src:app --reload --port 3000 --loop uvloop clean: docker stop infinity-api