diff --git a/lib/__init__.py b/lib/__init__.py index b328917..5213401 100644 --- a/lib/__init__.py +++ b/lib/__init__.py @@ -1,10 +1,28 @@ # lib/__init__.py import logging -from .api import app - -logging.basicConfig( - level=logging.INFO, - filename='app.log', - filemode='a', - format='%(asctime)s - %(levelname)s - %(message)s', -) +import sys + +logger = logging.getLogger(__name__) +logger.setLevel(logging.INFO) + +# Creates log handlers +file_handler = logging.FileHandler("app.log") +stdout_handler = logging.StreamHandler(sys.stdout) + +# Creates log formatter and add it to handlers +formatter = logging.Formatter("%(asctime)s - %(levelname)s - %(message)s") +file_handler.setFormatter(formatter) +stdout_handler.setFormatter(formatter) + +if not logger.hasHandlers(): + logger.addHandler(file_handler) + logger.addHandler(stdout_handler) + + +def parse_error(error): + exc_type = type(error).__name__ + exc_obj = f"{error}".replace("\n", " ").replace(" ", " ") + return f"{exc_type} exception: {exc_obj}" + + +from .api import app # noqa diff --git a/lib/api.py b/lib/api.py index 6896bed..332e575 100644 --- a/lib/api.py +++ b/lib/api.py @@ -8,11 +8,9 @@ from fastapi.openapi.utils import get_openapi from fastapi.responses import RedirectResponse, JSONResponse -from lib import logging +from lib import logger, parse_error from lib.routes import flight, environment, motor, rocket -logger = logging.getLogger(__name__) - app = FastAPI( swagger_ui_parameters={ "defaultModelsExpandDepth": 0, @@ -84,9 +82,8 @@ async def __perform_healthcheck(): async def validation_exception_handler( request: Request, exc: RequestValidationError ): - exc_str = f"{exc}".replace("\n", " ").replace(" ", " ") + exc_str = parse_error(exc) logger.error(f"{request}: {exc_str}") - content = {"status_code": 10422, "message": exc_str, "data": None} return JSONResponse( - content=content, status_code=status.HTTP_422_UNPROCESSABLE_ENTITY + content=exc_str, status_code=status.HTTP_422_UNPROCESSABLE_ENTITY ) diff --git a/lib/controllers/__init__.py b/lib/controllers/__init__.py index 87e5b46..c8add6a 100644 --- a/lib/controllers/__init__.py +++ b/lib/controllers/__init__.py @@ -1,6 +1 @@ # lib/controllers/__init__.py - - -def parse_error(e): - exc_str = f"{e}".replace("\n", " ").replace(" ", " ") - return exc_str diff --git a/lib/controllers/environment.py b/lib/controllers/environment.py index fc3b6f7..628c96d 100644 --- a/lib/controllers/environment.py +++ b/lib/controllers/environment.py @@ -4,8 +4,7 @@ from rocketpy.environment.environment import Environment as RocketPyEnvironment from fastapi import HTTPException, status -from lib import logging -from lib.controllers import parse_error +from lib import logger, parse_error from lib.models.environment import Env from lib.repositories.environment import EnvRepository from lib.views.environment import ( @@ -18,8 +17,6 @@ EnvPickle, ) -logger = logging.getLogger(__name__) - class EnvController: """ @@ -30,7 +27,7 @@ class EnvController: Enables: - Simulation of RocketPyEnvironment from models.Env - - CRUD operations over modeols.Env on the database + - CRUD operations over models.Env on the database """ def __init__(self, env: Env): @@ -45,7 +42,7 @@ def env(self, env: Env): self._env = env @staticmethod - async def get_rocketpy_env(env: Env) -> RocketPyEnvironment: + def get_rocketpy_env(env: Env) -> RocketPyEnvironment: """ Get the rocketpy env object. @@ -63,7 +60,7 @@ async def get_rocketpy_env(env: Env) -> RocketPyEnvironment: ) return rocketpy_env - async def create_env(self) -> "Union[EnvCreated, HTTPException]": + async def create_env(self) -> Union[EnvCreated, HTTPException]: """ Create a env in the database. @@ -71,30 +68,30 @@ async def create_env(self) -> "Union[EnvCreated, HTTPException]": views.EnvCreated """ try: - created_env = await EnvRepository( - environment=self.env - ).create_env() + async with EnvRepository() as env_repo: + env_repo.fetch_env(self.env) + await env_repo.create_env() 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: {e}", + detail=f"Failed to create environment: {exc_str}", ) from e else: - return EnvCreated(env_id=created_env.env_id) + return EnvCreated(env_id=self.env.env_id) finally: logger.info( - f"Call to controllers.environment.create_env completed; params: Env {hash(self.env)}" + f"Call to controllers.environment.create_env completed for Env {hash(self.env)}" ) @staticmethod - async def get_env_by_id(env_id: int) -> "Union[Env, HTTPException]": + async def get_env_by_id(env_id: str) -> Union[Env, HTTPException]: """ Get a env from the database. Args: - env_id: int + env_id: str Returns: models.Env @@ -103,13 +100,15 @@ async def get_env_by_id(env_id: int) -> "Union[Env, HTTPException]": HTTP 404 Not Found: If the env is not found in the database. """ try: - read_env = await EnvRepository(env_id=env_id).get_env() + async with EnvRepository() as env_repo: + await env_repo.get_env_by_id(env_id) + read_env = env_repo.env 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: {e}", + detail=f"Failed to read environment: {exc_str}", ) from e else: if read_env: @@ -120,19 +119,19 @@ async def get_env_by_id(env_id: int) -> "Union[Env, HTTPException]": ) finally: logger.info( - f"Call to controllers.environment.get_env_by_id completed; params: EnvID {env_id}" + f"Call to controllers.environment.get_env_by_id completed for Env {env_id}" ) @classmethod async def get_rocketpy_env_as_jsonpickle( cls, - env_id: int, - ) -> "Union[EnvPickle, HTTPException]": + env_id: str, + ) -> Union[EnvPickle, HTTPException]: """ Get rocketpy.Environmnet as jsonpickle string. Args: - env_id: int + env_id: str Returns: views.EnvPickle @@ -142,6 +141,7 @@ async def get_rocketpy_env_as_jsonpickle( """ try: read_env = await cls.get_env_by_id(env_id) + rocketpy_env = cls.get_rocketpy_env(read_env) except HTTPException as e: raise e from e except Exception as e: @@ -151,26 +151,25 @@ async def get_rocketpy_env_as_jsonpickle( ) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail=f"Failed to read environment: {e}", + detail=f"Failed to read environment: {exc_str}", ) from e else: - rocketpy_env = await cls.get_rocketpy_env(read_env) return EnvPickle( jsonpickle_rocketpy_env=jsonpickle.encode(rocketpy_env) ) finally: logger.info( - f"Call to controllers.environment.get_rocketpy_env_as_jsonpickle completed; params: EnvID {env_id}" + f"Call to controllers.environment.get_rocketpy_env_as_jsonpickle completed for Env {env_id}" ) - async def update_env( - self, env_id: int - ) -> "Union[EnvUpdated, HTTPException]": + async def update_env_by_id( + self, env_id: str + ) -> Union[EnvUpdated, HTTPException]: """ - Update a env in the database. + Update a models.Env in the database. Args: - env_id: int + env_id: str Returns: views.EnvUpdated @@ -179,33 +178,33 @@ async def update_env( HTTP 404 Not Found: If the env is not found in the database. """ try: - await EnvController.get_env_by_id(env_id) - updated_env = await EnvRepository( - environment=self.env, env_id=env_id - ).update_env() - except HTTPException as e: - raise e from e + async with EnvRepository() as env_repo: + env_repo.fetch_env(self.env) + await env_repo.create_env() + await env_repo.delete_env_by_id(env_id) 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: {e}", + detail=f"Failed to update environment: {exc_str}", ) from e else: - return EnvUpdated(new_env_id=updated_env.env_id) + return EnvUpdated(new_env_id=self.env.env_id) finally: logger.info( - f"Call to controllers.environment.update_env completed; params: EnvID {env_id}, Env {hash(self.env)}" + f"Call to controllers.environment.update_env completed for Env {env_id}; Env {hash(self.env)}" ) @staticmethod - async def delete_env(env_id: str) -> "Union[EnvDeleted, HTTPException]": + async def delete_env_by_id( + env_id: str, + ) -> Union[EnvDeleted, HTTPException]: """ - Delete a env from the database. + Delete a models.Env from the database. Args: - env_id: int + env_id: str Returns: views.EnvDeleted @@ -214,28 +213,31 @@ async def delete_env(env_id: str) -> "Union[EnvDeleted, HTTPException]": HTTP 404 Not Found: If the env is not found in the database. """ try: - await EnvRepository(env_id=env_id).delete_env() + async with EnvRepository() as env_repo: + await env_repo.delete_env_by_id(env_id) 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: {e}", + detail=f"Failed to delete environment: {exc_str}", ) from e else: return EnvDeleted(deleted_env_id=env_id) finally: logger.info( - f"Call to controllers.environment.delete_env completed; params: EnvID {env_id}" + f"Call to controllers.environment.delete_env completed for Env {env_id}" ) @classmethod - async def simulate(cls, env_id: int) -> "Union[EnvSummary, HTTPException]": + async def simulate_env( + cls, env_id: str + ) -> Union[EnvSummary, HTTPException]: """ Simulate a rocket environment. Args: - env_id: int. + env_id: str. Returns: views.EnvSummary @@ -245,7 +247,8 @@ async def simulate(cls, env_id: int) -> "Union[EnvSummary, HTTPException]": """ try: read_env = await cls.get_env_by_id(env_id) - rocketpy_env = await cls.get_rocketpy_env(read_env) + rocketpy_env = cls.get_rocketpy_env(read_env) + env_simulation_numbers = EnvData.parse_obj( rocketpy_env.all_info_returned() ) @@ -262,11 +265,11 @@ async def simulate(cls, env_id: int) -> "Union[EnvSummary, HTTPException]": logger.error(f"controllers.environment.simulate: {exc_str}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail=f"Failed to simulate environment: {e}", + detail=f"Failed to simulate environment: {exc_str}", ) from e else: return env_summary finally: logger.info( - f"Call to controllers.environment.simulate completed; params: EnvID {env_id}" + f"Call to controllers.environment.simulate completed for Env {env_id}" ) diff --git a/lib/controllers/flight.py b/lib/controllers/flight.py index 38d9ee8..d396595 100644 --- a/lib/controllers/flight.py +++ b/lib/controllers/flight.py @@ -1,10 +1,11 @@ from typing import Union from fastapi import HTTPException, status -from rocketpy.simulation.flight import Flight as RocketpyFlight +from rocketpy.simulation.flight import Flight as RocketPyFlight import jsonpickle +from lib import logger, parse_error from lib.models.rocket import Rocket, RocketOptions from lib.models.motor import MotorKinds from lib.models.flight import Flight @@ -40,291 +41,339 @@ class FlightController: flight (models.Flight): Flight model object. 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. + - 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. + - Read a RocketPyFlight object from the database. """ def __init__( self, flight: Flight, + *, rocket_option: RocketOptions, motor_kind: MotorKinds, ): - rocketpy_rocket = RocketController( - flight.rocket, rocket_option=rocket_option, motor_kind=motor_kind - ).rocketpy_rocket - rocketpy_env = EnvController(flight.environment).rocketpy_env + self._flight = flight + self._rocket_option = rocket_option + self._motor_kind = motor_kind - rocketpy_flight = RocketpyFlight( + @property + def flight(self) -> Flight: + return self._flight + + @property + def rocket_option(self) -> RocketOptions: + return self._rocket_option + + @property + def motor_kind(self) -> MotorKinds: + return self._motor_kind + + @flight.setter + def flight(self, flight: Flight): + self._flight = flight + + @staticmethod + def get_rocketpy_flight(flight: Flight) -> RocketPyFlight: + """ + Get the rocketpy flight object. + + Returns: + RocketPyFlight + """ + rocketpy_rocket = RocketController.get_rocketpy_rocket(flight.rocket) + rocketpy_env = EnvController.get_rocketpy_env(flight.environment) + rocketpy_flight = RocketPyFlight( rocket=rocketpy_rocket, inclination=flight.inclination, heading=flight.heading, environment=rocketpy_env, rail_length=flight.rail_length, ) + return rocketpy_flight - self.rocket_option = rocket_option - self.motor_kind = motor_kind - self.rocketpy_flight = rocketpy_flight - self.flight = flight - - async def create_flight(self) -> "Union[FlightCreated, HTTPException]": + async def create_flight(self) -> Union[FlightCreated, HTTPException]: """ Create a flight in the database. Returns: - FlightCreated: Flight id. + views.FlightCreated """ - flight = FlightRepository(flight=self.flight) - successfully_created_flight = await flight.create_flight( - motor_kind=self.motor_kind, rocket_option=self.rocket_option - ) - - if not successfully_created_flight: + try: + async with FlightRepository() as flight_repo: + flight_repo.fetch_flight(self.flight) + await flight_repo.create_flight( + motor_kind=self.motor_kind, + rocket_option=self.rocket_option, + ) + 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="Failed to create flight.", + detail=f"Failed to create flight: {exc_str}", + ) from e + else: + return FlightCreated(flight_id=self.flight.flight_id) + finally: + logger.info( + f"Call to controllers.flight.create_flight completed for Flight {hash(self.flight)}" ) - return FlightCreated(flight_id=str(flight.flight_id)) - @staticmethod - async def get_flight(flight_id: int) -> "Union[Flight, HTTPException]": + async def get_flight_by_id(flight_id: str) -> Union[Flight, HTTPException]: """ Get a flight from the database. Args: - flight_id (int): Flight id. + flight_id: str Returns: - Flight model object + models.Flight Raises: HTTP 404 Not Found: If the flight is not found in the database. """ - successfully_read_flight = await FlightRepository( - flight_id=flight_id - ).get_flight() - if not successfully_read_flight: + try: + async with FlightRepository() as flight_repo: + await flight_repo.get_flight_by_id(flight_id) + read_flight = flight_repo.flight + 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 read_flight: + return read_flight 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}" + ) - return successfully_read_flight - - @staticmethod - async def get_rocketpy_flight( - flight_id: int, - ) -> "Union[FlightPickle, HTTPException]": + @classmethod + async def get_rocketpy_flight_as_jsonpickle( + cls, + flight_id: str, + ) -> Union[FlightPickle, HTTPException]: """ - Get a rocketpy flight object encoded as jsonpickle string from the database. + Get rocketpy.flight as jsonpickle string. Args: - flight_id (int): Flight id. + flight_id: str Returns: - str: jsonpickle string of the rocketpy flight. + views.FlightPickle Raises: HTTP 404 Not Found: If the flight is not found in the database. """ - successfully_read_flight = await FlightRepository( - flight_id=flight_id - ).get_flight() - if not successfully_read_flight: + try: + read_flight = await cls.get_flight_by_id(flight_id) + rocketpy_flight = cls.get_rocketpy_flight(read_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_as_jsonpickle: {exc_str}" + ) raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail="Flight not found.", + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Failed to read flight: {exc_str}", + ) from e + else: + return FlightPickle( + jsonpickle_rocketpy_flight=jsonpickle.encode(rocketpy_flight) ) - - successfully_read_rocketpy_flight = FlightController( - flight=successfully_read_flight, - rocket_option=RocketOptions( - successfully_read_flight.rocket._rocket_option - ), - motor_kind=MotorKinds( - successfully_read_flight.rocket.motor._motor_kind - ), - ).rocketpy_flight - - return FlightPickle( - jsonpickle_rocketpy_flight=jsonpickle.encode( - successfully_read_rocketpy_flight + finally: + logger.info( + f"Call to controllers.flight.get_rocketpy_flight_as_jsonpickle completed for Flight {flight_id}" ) - ) - async def update_flight( - self, flight_id: int - ) -> "Union[FlightUpdated, HTTPException]": + async def update_flight_by_id( + self, flight_id: str + ) -> Union[FlightUpdated, HTTPException]: """ - Update a flight in the database. + Update a models.Flight in the database. Args: - flight_id (int): Flight id. + flight_id: str Returns: - FlightUpdated: Flight id and message. + views.FlightUpdated Raises: HTTP 404 Not Found: If the flight is not found in the database. """ - successfully_read_flight = await FlightRepository( - flight_id=flight_id - ).get_flight() - if not successfully_read_flight: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail="Flight not found.", - ) - - successfully_updated_flight = await FlightRepository( - flight=self.flight, flight_id=flight_id - ).update_flight( - rocket_option=self.rocket_option, motor_kind=self.motor_kind - ) - if not successfully_updated_flight: + try: + async with FlightRepository() as flight_repo: + flight_repo.fetch_flight(self.flight) + await flight_repo.create_flight( + motor_kind=self.motor_kind, + rocket_option=self.rocket_option, + ) + await flight_repo.delete_flight_by_id(flight_id) + 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="Failed to update flight.", + detail=f"Failed to update flight: {exc_str}", + ) from e + else: + return FlightUpdated(new_flight_id=self.flight.flight_id) + finally: + logger.info( + f"Call to controllers.flight.update_flight completed for Flight {flight_id}" ) - return FlightUpdated(new_flight_id=str(successfully_updated_flight)) - - @staticmethod - async def update_env( - flight_id: int, env: Env - ) -> "Union[FlightUpdated, HTTPException]": + @classmethod + async def update_env_by_flight_id( + cls, flight_id: str, *, env: Env + ) -> Union[FlightUpdated, HTTPException]: """ - Update the environment of a flight in the database. + Update a models.Flight.env in the database. Args: - flight_id (int): Flight id. - env (models.Env): Environment model object. + flight_id: str + env: models.Env Returns: - FlightUpdated: Flight id and message. + views.FlightUpdated Raises: HTTP 404 Not Found: If the flight is not found in the database. """ - successfully_read_flight = await FlightRepository( - flight_id=flight_id - ).get_flight() - if not successfully_read_flight: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail="Flight not found.", - ) - - flight = successfully_read_flight.dict() - flight["environment"] = env - flight = Flight(**flight) - successfully_updated_flight = await FlightRepository( - flight=flight, flight_id=flight_id - ).update_flight() - if not successfully_updated_flight: + try: + read_flight = await cls.get_flight_by_id(flight_id) + new_flight = read_flight.dict() + new_flight["environment"] = env + new_flight = Flight(**new_flight) + async with FlightRepository() as flight_repo: + flight_repo.fetch_flight(new_flight) + await flight_repo.create_flight( + motor_kind=read_flight.rocket.motor.motor_kind, + rocket_option=read_flight.rocket.rocket_option, + ) + await flight_repo.delete_flight_by_id(flight_id) + 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="Failed to update flight.", + detail=f"Failed to update environment: {exc_str}", + ) from e + else: + return FlightUpdated(new_flight_id=new_flight.flight_id) + finally: + logger.info( + f"Call to controllers.flight.update_env completed for Flight {flight_id}; Env {hash(env)}" ) - return FlightUpdated(new_flight_id=str(successfully_updated_flight)) - - @staticmethod - async def update_rocket( - flight_id: int, rocket: Rocket, rocket_option, motor_kind - ) -> "Union[FlightUpdated, HTTPException]": + @classmethod + async def update_rocket_by_flight_id( + cls, flight_id: str, *, rocket: Rocket, rocket_option, motor_kind + ) -> Union[FlightUpdated, HTTPException]: """ - Update the rocket of a flight in the database. + Update a models.Flight.rocket in the database. Args: - flight_id (int): Flight id. - rocket (models.Rocket): Rocket model object. + flight_id: str + rocket: models.Rocket Returns: - FlightUpdated: Flight id and message. + views.FlightUpdated Raises: HTTP 404 Not Found: If the flight is not found in the database. """ - successfully_read_flight = await FlightRepository( - flight_id=flight_id - ).get_flight() - if not successfully_read_flight: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail="Flight not found.", - ) - - flight = successfully_read_flight.dict() - updated_rocket = rocket.dict() - updated_rocket["rocket_option"] = rocket_option - updated_rocket["motor"]["motor_kind"] = motor_kind - flight["rocket"] = Rocket(**updated_rocket) - flight = Flight(**flight) - successfully_updated_flight = await FlightRepository( - flight=flight, flight_id=flight_id - ).update_flight() - if not successfully_updated_flight: + try: + read_flight = await cls.get_flight_by_id(flight_id) + updated_rocket = rocket.dict() + updated_rocket["rocket_option"] = rocket_option + updated_rocket["motor"]["motor_kind"] = motor_kind + new_flight = read_flight.dict() + new_flight["rocket"] = updated_rocket + new_flight = Flight(**new_flight) + async with FlightRepository() as flight_repo: + flight_repo.fetch_flight(new_flight) + await flight_repo.create_flight( + motor_kind=motor_kind, rocket_option=rocket_option + ) + await flight_repo.delete_flight_by_id(flight_id) + 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="Failed to update flight.", + detail=f"Failed to update rocket: {exc_str}", + ) from e + else: + return FlightUpdated(new_flight_id=new_flight.flight_id) + finally: + logger.info( + f"Call to controllers.flight.update_rocket completed for Flight {flight_id}; Rocket {hash(rocket)}" ) - return FlightUpdated(new_flight_id=str(successfully_updated_flight)) - @staticmethod - async def delete_flight( - flight_id: int, - ) -> "Union[FlightDeleted, HTTPException]": + async def delete_flight_by_id( + flight_id: str, + ) -> Union[FlightDeleted, HTTPException]: """ - Delete a flight from the database. + Delete a models.Flight from the database. Args: - flight_id (int): Flight id. + flight_id: str Returns: - FlightDeleted: Flight id and message. + views.FlightDeleted Raises: HTTP 404 Not Found: If the flight is not found in the database. """ - successfully_read_flight = await FlightRepository( - flight_id=flight_id - ).get_flight() - if not successfully_read_flight: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail="Flight not found.", - ) - - successfully_deleted_flight = await FlightRepository( - flight_id=flight_id - ).delete_flight() - if not successfully_deleted_flight: + try: + async with FlightRepository() as flight_repo: + await flight_repo.delete_flight_by_id(flight_id) + 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="Failed to delete flight.", + detail=f"Failed to delete flight: {exc_str}", + ) from e + else: + return FlightDeleted(deleted_flight_id=flight_id) + finally: + logger.info( + f"Call to controllers.flight.delete_flight completed for Flight {flight_id}" ) - return FlightDeleted(deleted_flight_id=str(flight_id)) - - @staticmethod - async def simulate( - flight_id: int, - ) -> "Union[FlightSummary, HTTPException]": + @classmethod + async def simulate_flight( + cls, + flight_id: str, + ) -> Union[FlightSummary, HTTPException]: """ Simulate a rocket flight. Args: - flight_id (int): Flight id. + flight_id: str Returns: Flight summary view. @@ -332,25 +381,9 @@ async def simulate( Raises: HTTP 404 Not Found: If the flight does not exist in the database. """ - successfully_read_flight = await FlightRepository( - flight_id=flight_id - ).get_flight() - if not successfully_read_flight: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail="Flight not found.", - ) - try: - flight = FlightController( - flight=successfully_read_flight, - rocket_option=RocketOptions( - successfully_read_flight.rocket._rocket_option - ), - motor_kind=MotorKinds( - successfully_read_flight.rocket.motor._motor_kind - ), - ).rocketpy_flight + read_flight = await cls.get_flight_by_id(flight_id=flight_id) + flight = cls.get_rocketpy_flight(read_flight) _initial_conditions = InitialConditions( initial_altitude="Attitude - e0: {:.3f} | e1: {:.3f} | e2: {:.3f} | e3: {:.3f}".format( @@ -601,8 +634,18 @@ async def simulate( ) flight_summary = FlightSummary(flight_data=_flight_data) - return 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=500, detail=f"Failed to simulate flight: {e}" + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Failed to simulate flight: {exc_str}", ) from e + else: + return flight_summary + finally: + logger.info( + f"Call to controllers.flight.simulate_flight completed for Flight {flight_id}" + ) diff --git a/lib/controllers/motor.py b/lib/controllers/motor.py index eaa226c..5ab9744 100644 --- a/lib/controllers/motor.py +++ b/lib/controllers/motor.py @@ -5,6 +5,7 @@ from rocketpy.motors.hybrid_motor import HybridMotor import jsonpickle +from lib import logger, parse_error from lib.models.motor import Motor, MotorKinds from lib.repositories.motor import MotorRepository from lib.views.motor import ( @@ -28,8 +29,34 @@ class MotorController: - Create a rocketpy.Motor object from a Motor model object. """ - def __init__(self, motor: Motor, motor_kind): + def __init__(self, *, motor: Motor, motor_kind: MotorKinds): self.guard(motor, motor_kind) + self._motor = motor + self._motor_kind = motor_kind + + @property + def motor(self) -> Motor: + return self._motor + + @motor.setter + def motor(self, motor: Motor): + self._motor = motor + + @property + def motor_kind(self) -> MotorKinds: + return self._motor_kind + + @staticmethod + def get_rocketpy_motor( + motor: Motor, + ) -> Union[LiquidMotor, HybridMotor, SolidMotor]: + """ + Get the rocketpy motor object. + + Returns: + Union[LiquidMotor, HybridMotor, SolidMotor] + """ + motor_core = { "thrust_source": f"lib/data/engines/{motor.thrust_source.value}.eng", "burn_time": motor.burn_time, @@ -39,10 +66,10 @@ def __init__(self, motor: Motor, motor_kind): "center_of_dry_mass_position": motor.center_of_dry_mass_position, } - match motor_kind: - case MotorKinds.liquid: + match motor.motor_kind: + case MotorKinds.LIQUID: rocketpy_motor = LiquidMotor(**motor_core) - case MotorKinds.hybrid: + case MotorKinds.HYBRID: rocketpy_motor = HybridMotor( **motor_core, throat_radius=motor.throat_radius, @@ -68,200 +95,208 @@ def __init__(self, motor: Motor, motor_kind): interpolation_method=motor.interpolation_method, ) - if motor_kind != MotorKinds.solid: + if motor.motor_kind != MotorKinds.SOLID: for tank in motor.tanks: rocketpy_motor.add_tank(tank.tank, tank.position) - self.motor_kind = motor_kind # tracks motor kind state - self.rocketpy_motor = rocketpy_motor - self.motor = motor + return rocketpy_motor def guard(self, motor: Motor, motor_kind): - if motor_kind != MotorKinds.solid and motor.tanks is None: + if motor_kind != MotorKinds.SOLID and motor.tanks is None: raise HTTPException( status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail="Tanks must be provided for liquid and hybrid motors.", ) - async def create_motor(self) -> "Union[MotorCreated, HTTPException]": + async def create_motor(self) -> Union[MotorCreated, HTTPException]: """ - Create a motor in the database. + Create a models.Motor in the database. Returns: - MotorCreated: motor id. + views.MotorCreated """ - motor = MotorRepository(motor=self.motor) - successfully_created_motor = await motor.create_motor( - motor_kind=self.motor_kind - ) - if not successfully_created_motor: + try: + async with MotorRepository() as motor_repo: + motor_repo.fetch_motor(self.motor) + await motor_repo.create_motor(motor_kind=self.motor_kind) + 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="Failed to create motor.", + detail=f"Failed to create motor: {exc_str}", + ) from e + else: + return MotorCreated(motor_id=self.motor.motor_id) + finally: + logger.info( + f"Call to controllers.motor.create_motor completed for Motor {self.motor.motor_id}" ) - return MotorCreated(motor_id=str(motor.motor_id)) - @staticmethod - async def get_motor(motor_id: int) -> "Union[Motor, HTTPException]": + async def get_motor_by_id(motor_id: str) -> Union[Motor, HTTPException]: """ - Get a motor from the database. + Get a models.Motor from the database. Args: - motor_id (int): Motor id. + motor_id: str Returns: - Motor model object + models.Motor Raises: HTTP 404 Not Found: If the motor is not found in the database. """ - successfully_read_motor = await MotorRepository( - motor_id=motor_id - ).get_motor() - if not successfully_read_motor: + try: + async with MotorRepository() as motor_repo: + await motor_repo.get_motor_by_id(motor_id) + read_motor = motor_repo.motor + 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 read_motor: + return read_motor raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, - detail="Motor not found.", + detail="Motor not found", + ) + finally: + logger.info( + f"Call to controllers.motor.get_motor_by_id completed for Motor {motor_id}" ) - return successfully_read_motor - - @staticmethod - async def get_rocketpy_motor( - motor_id: int, - ) -> "Union[MotorPickle, HTTPException]": + @classmethod + async def get_rocketpy_motor_as_jsonpickle( + cls, + motor_id: str, + ) -> Union[MotorPickle, HTTPException]: """ - Get a rocketpy motor object encoded as jsonpickle string from the database. + Get a rocketpy.Motor object as a jsonpickle string. Args: - motor_id (int): Motor id. + motor_id: str Returns: - str: jsonpickle string of the rocketpy motor. + views.MotorPickle Raises: HTTP 404 Not Found: If the motor is not found in the database. """ - successfully_read_motor = await MotorRepository( - motor_id=motor_id - ).get_motor() - if not successfully_read_motor: + try: + read_motor = await cls.get_motor_by_id(motor_id) + rocketpy_motor = cls.get_rocketpy_motor(read_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_as_jsonpickle: {exc_str}" + ) raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail="Motor not found.", + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Failed to read motor: {exc_str}", + ) from e + else: + return MotorPickle( + jsonpickle_rocketpy_motor=jsonpickle.encode(rocketpy_motor) ) - - successfully_read_rocketpy_motor = MotorController( - motor=successfully_read_motor, - motor_kind=MotorKinds(successfully_read_motor._motor_kind), - ).rocketpy_motor - - return MotorPickle( - jsonpickle_rocketpy_motor=jsonpickle.encode( - successfully_read_rocketpy_motor + finally: + logger.info( + f"Call to controllers.motor.get_rocketpy_motor_as_jsonpickle completed for Motor {motor_id}" ) - ) - async def update_motor( - self, motor_id: int - ) -> "Union[MotorUpdated, HTTPException]": + async def update_motor_by_id( + self, motor_id: str + ) -> Union[MotorUpdated, HTTPException]: """ Update a motor in the database. Args: - motor_id (int): Motor id. + motor_id: str Returns: - MotorUpdated: motor id and message. + views.MotorUpdated Raises: HTTP 404 Not Found: If the motor is not found in the database. """ - successfully_read_motor = await MotorRepository( - motor_id=motor_id - ).get_motor() - if not successfully_read_motor: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail="Motor not found.", - ) - - successfully_updated_motor = await MotorRepository( - motor=self.motor, motor_id=motor_id - ).update_motor(motor_kind=self.motor_kind) - if not successfully_updated_motor: + try: + async with MotorRepository() as motor_repo: + motor_repo.fetch_motor(self.motor) + await motor_repo.create_motor(motor_kind=self.motor_kind) + await motor_repo.delete_motor_by_id(motor_id) + 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="Failed to update motor.", + detail=f"Failed to update motor: {exc_str}", + ) from e + else: + return MotorUpdated(new_motor_id=self.motor.motor_id) + finally: + logger.info( + f"Call to controllers.motor.update_motor completed for Motor {motor_id}" ) - return MotorUpdated(new_motor_id=str(successfully_updated_motor)) - @staticmethod - async def delete_motor( - motor_id: int, - ) -> "Union[MotorDeleted, HTTPException]": + async def delete_motor_by_id( + motor_id: str, + ) -> Union[MotorDeleted, HTTPException]: """ - Delete a motor from the database. + Delete a models.Motor from the database. Args: - motor_id (int): motor id. + motor_id: str Returns: - MotorDeleted: motor id and message. + views.MotorDeleted Raises: HTTP 404 Not Found: If the motor is not found in the database. """ - successfully_read_motor = await MotorRepository( - motor_id=motor_id - ).get_motor() - if not successfully_read_motor: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail="Motor not found.", - ) - - successfully_deleted_motor = await MotorRepository( - motor_id=motor_id - ).delete_motor() - if not successfully_deleted_motor: + try: + async with MotorRepository() as motor_repo: + await motor_repo.delete_motor_by_id(motor_id) + 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="Failed to delete motor.", + detail=f"Failed to delete motor: {exc_str}", + ) from e + else: + return MotorDeleted(deleted_motor_id=motor_id) + finally: + logger.info( + f"Call to controllers.motor.delete_motor completed for Motor {motor_id}" ) - return MotorDeleted(deleted_motor_id=str(motor_id)) - - @staticmethod - async def simulate(motor_id: int) -> "Union[MotorSummary, HTTPException]": + @classmethod + async def simulate_motor( + cls, motor_id: str + ) -> Union[MotorSummary, HTTPException]: """ Simulate a rocketpy motor. Args: - motor_id (int): Motor id. + motor_id: str Returns: - motor summary view. + views.MotorSummary Raises: HTTP 404 Not Found: If the motor does not exist in the database. """ - successfully_read_motor = await MotorRepository( - motor_id=motor_id - ).get_motor() - if not successfully_read_motor: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail="Motor not found.", - ) - try: - motor = MotorController( - motor=successfully_read_motor, - motor_kind=MotorKinds(successfully_read_motor._motor_kind), - ).rocketpy_motor + read_motor = await cls.get_motor_by_id(motor_id) + motor = cls.get_rocketpy_motor(read_motor) + motor_simulation_numbers = MotorData( total_burning_time="Total Burning Time: " + str(motor.burn_out_time) @@ -292,9 +327,18 @@ async def simulate(motor_id: int) -> "Union[MotorSummary, HTTPException]": motor_summary = MotorSummary( motor_data=motor_simulation_numbers ) # , plots=motor_simulation_plots ) - return 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: {e}", + detail=f"Failed to simulate motor: {exc_str}", ) from e + else: + return motor_summary + finally: + logger.info( + f"Call to controllers.motor.simulate_motor completed for Motor {motor_id}" + ) diff --git a/lib/controllers/rocket.py b/lib/controllers/rocket.py index c0e7210..d0719dd 100644 --- a/lib/controllers/rocket.py +++ b/lib/controllers/rocket.py @@ -1,4 +1,5 @@ from typing import Union +import os import ast import jsonpickle @@ -7,14 +8,15 @@ # TODO # from inspect import getsourcelines -from rocketpy.rocket.parachute import Parachute as RocketpyParachute -from rocketpy.rocket.rocket import Rocket as RocketpyRocket -from rocketpy.rocket.aero_surface import NoseCone as RocketpyNoseCone +from rocketpy.rocket.parachute import Parachute as RocketPyParachute +from rocketpy.rocket.rocket import Rocket as RocketPyRocket +from rocketpy.rocket.aero_surface import NoseCone as RocketPyNoseCone from rocketpy.rocket.aero_surface import ( - TrapezoidalFins as RocketpyTrapezoidalFins, + TrapezoidalFins as RocketPyTrapezoidalFins, ) -from rocketpy.rocket.aero_surface import Tail as RocketpyTail +from rocketpy.rocket.aero_surface import Tail as RocketPyTail +from lib import logger, parse_error from lib.controllers.motor import MotorController from lib.models.rocket import Rocket, RocketOptions from lib.models.motor import MotorKinds @@ -40,19 +42,62 @@ class RocketController: Controller for the Rocket model. Init Attributes: - rocket (models.Rocket): Rocket model object. + rocket: models.Rocket. Enables: - create a RocketpyRocket object from a Rocket model object. + - CRUD operations over models.Rocket on the database. """ - def __init__(self, rocket: Rocket, rocket_option, motor_kind): - rocketpy_rocket = RocketpyRocket( + def __init__( + self, + *, + rocket: Rocket, + rocket_option: RocketOptions, + motor_kind: MotorKinds, + ): + self._rocket = rocket + self._rocket_option = rocket_option + self._motor_kind = motor_kind + + @property + def rocket(self) -> Rocket: + return self._rocket + + @rocket.setter + def rocket(self, rocket: Rocket): + self._rocket = rocket + + @property + def rocket_option(self) -> RocketOptions: + return self._rocket_option + + @property + def motor_kind(self) -> MotorKinds: + return self._motor_kind + + @classmethod + def get_rocketpy_rocket(cls, rocket: Rocket) -> RocketPyRocket: + """ + Get a rocketpy rocket object. + + Returns: + RocketPyRocket + """ + + rocketpy_rocket = RocketPyRocket( radius=rocket.radius, mass=rocket.mass, inertia=rocket.inertia, - power_off_drag=f"lib/data/{rocket_option.value.lower()}/powerOffDragCurve.csv", - power_on_drag=f"lib/data/{rocket_option.value.lower()}/powerOnDragCurve.csv", + power_off_drag=os.path.join( + "lib/data", + f"{rocket.rocket_option.lower()}", + "powerOffDragCurve.csv", + ), + power_on_drag=os.path.join( + "lib/data", + f"{rocket.rocket_option.lower()}", + "powerOnDragCurve.csv", + ), center_of_mass_without_motor=rocket.center_of_mass_without_motor, coordinate_system_orientation=rocket.coordinate_system_orientation, ) @@ -64,393 +109,238 @@ def __init__(self, rocket: Rocket, rocket_option, motor_kind): angular_position=rocket.rail_buttons.angular_position, ) rocketpy_rocket.add_motor( - MotorController(rocket.motor, motor_kind).rocketpy_motor, + MotorController.get_rocketpy_motor(rocket.motor), rocket.motor_position, ) # NoseCone - nose = self.NoseConeController(rocket.nose).rocketpy_nose + nose = cls.get_rocketpy_nose(rocket.nose) rocketpy_rocket.aerodynamic_surfaces.add(nose, nose.position) rocketpy_rocket.evaluate_static_margin() # FinSet # TODO: re-write this to match overall fins not only TrapezoidalFins - finset = self.TrapezoidalFinsController(rocket.fins).rocketpy_finset + # Maybe a strategy with different factory methods? + finset = cls.get_rocketpy_finset(rocket.fins) rocketpy_rocket.aerodynamic_surfaces.add(finset, finset.position) rocketpy_rocket.evaluate_static_margin() # Tail - tail = self.TailController(rocket.tail).rocketpy_tail + tail = cls.get_rocketpy_tail(rocket.tail) rocketpy_rocket.aerodynamic_surfaces.add(tail, tail.position) rocketpy_rocket.evaluate_static_margin() # Parachutes for p in range(len(rocket.parachutes)): parachute_trigger = rocket.parachutes[p].triggers[0] - if self.ParachuteController.check_trigger(parachute_trigger): + if cls.check_parachute_trigger(parachute_trigger): rocket.parachutes[p].triggers[0] = compile( parachute_trigger, "", "eval" ) - parachute = self.ParachuteController( - rocket.parachutes, p - ).rocketpy_parachute + parachute = cls.get_rocketpy_parachute(rocket.parachutes, p) rocketpy_rocket.parachutes.append(parachute) else: print("Parachute trigger not valid. Skipping parachute.") continue - self.rocket_option = rocket_option # tracks rocket option state - self.rocketpy_rocket = rocketpy_rocket - self.rocket = rocket - - class NoseConeController: - """ - Controller for the NoseCone model. - - Init Attributes: - nose (models.NoseCone): NoseCone model object. - - Enables: - - Create a rocketpy.AeroSurface.NoseCone object from a NoseCone model object. - """ - - def __init__(self, nose: NoseCone): - rocketpy_nose = RocketpyNoseCone( - length=nose.length, - kind=nose.kind, - base_radius=nose.base_radius, - rocket_radius=nose.rocket_radius, - ) - rocketpy_nose.position = nose.position - self.rocketpy_nose = rocketpy_nose - self.nose = nose - - class TrapezoidalFinsController: - """ - Controller for the TrapezoidalFins model. - - Init Attributes: - trapezoidal_fins (models.TrapezoidalFins): TrapezoidalFins model object. - - Enables: - - Create a rocketpy.AeroSurface.TrapezoidalFins object from a TrapezoidalFins model object. - """ - - def __init__(self, trapezoidal_fins: TrapezoidalFins): - rocketpy_finset = RocketpyTrapezoidalFins( - n=trapezoidal_fins.n, - root_chord=trapezoidal_fins.root_chord, - tip_chord=trapezoidal_fins.tip_chord, - span=trapezoidal_fins.span, - cant_angle=trapezoidal_fins.cant_angle, - rocket_radius=trapezoidal_fins.radius, - airfoil=trapezoidal_fins.airfoil, - ) - rocketpy_finset.position = trapezoidal_fins.position - self.rocketpy_finset = rocketpy_finset - self.trapezoidal_fins = trapezoidal_fins - - class TailController: - """ - Controller for the Tail model. - - Init Attributes: - tail (models.Tail): Tail model object. + return rocketpy_rocket - Enables: - - Create a rocketpy.AeroSurface.Tail object from a Tail model object. + async def create_rocket(self) -> Union[RocketCreated, HTTPException]: """ - - def __init__(self, tail: Tail): - rocketpy_tail = RocketpyTail( - top_radius=tail.top_radius, - bottom_radius=tail.bottom_radius, - length=tail.length, - rocket_radius=tail.radius, - ) - rocketpy_tail.position = tail.position - self.rocketpy_tail = rocketpy_tail - self.tail = tail - - class ParachuteController: - """ - Controller for the Parachute model. - - Init Attributes: - parachute (models.Parachute): Parachute model object. - - Enables: - - Create a RocketpyParachute.Parachute object from a Parachute model object. - """ - - def __init__(self, parachute: Parachute, p: int): - rocketpy_parachute = RocketpyParachute( - name=parachute[p].name[0], - cd_s=parachute[p].cd_s[0], - trigger=eval(parachute[p].triggers[0]), - sampling_rate=parachute[p].sampling_rate[0], - lag=parachute[p].lag[0], - noise=parachute[p].noise[0], - ) - self.rocketpy_parachute = rocketpy_parachute - self.parachute = parachute - - @staticmethod - def check_trigger(expression: str) -> bool: - """ - Check if the trigger expression is valid. - - Args: - expression (str): Trigger expression. - - Returns: - bool: True if the expression is valid, False otherwise. - """ - - # Parsing the expression into an AST - try: - parsed_expression = ast.parse(expression, mode="eval") - except SyntaxError: - print("Invalid syntax.") - return False - - # Constant case (supported after beta v1) - if isinstance(parsed_expression.body, ast.Constant): - return True - # Name case (supported after beta v1) - if ( - isinstance(parsed_expression.body, ast.Name) - and parsed_expression.body.id == "apogee" - ): - global apogee - apogee = "apogee" - return True - - # Validating the expression structure - if not isinstance(parsed_expression.body, ast.Lambda): - print("Invalid expression structure (not a Lambda).") - return False - - lambda_node = parsed_expression.body - if len(lambda_node.args.args) != 3: - print("Invalid expression structure (invalid arity).") - return False - - if not isinstance(lambda_node.body, ast.Compare): - try: - for operand in lambda_node.body.values: - if not isinstance(operand, ast.Compare): - print( - "Invalid expression structure (not a Compare)." - ) - return False - except AttributeError: - print("Invalid expression structure (not a Compare).") - return False - - # Restricting access to functions or attributes - for node in ast.walk(lambda_node): - if isinstance(node, ast.Call): - print( - "Calling functions is not allowed in the expression." - ) - return False - if isinstance(node, ast.Attribute): - print( - "Accessing attributes is not allowed in the expression." - ) - return False - return True - - async def create_rocket(self) -> "Union[RocketCreated, HTTPException]": - """ - Create a rocket in the database. + Create a models.Rocket in the database. Returns: - RocketCreated: Rocket id. + views.RocketCreated """ - rocket = RocketRepository(rocket=self.rocket) - successfully_created_rocket = await rocket.create_rocket( - rocket_option=self.rocket_option - ) - if not successfully_created_rocket: + try: + async with RocketRepository() as rocket_repo: + rocket_repo.fetch_rocket(self.rocket) + await rocket_repo.create_rocket( + rocket_option=self.rocket_option, + motor_kind=self.motor_kind, + ) + 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="Failed to create rocket.", + detail=f"Failed to create rocket: {exc_str}", + ) from e + else: + return RocketCreated(rocket_id=self.rocket.rocket_id) + finally: + logger.info( + f"Call to controllers.rocket.create_rocket completed for Rocket {hash(self.rocket)}" ) - return RocketCreated(rocket_id=str(rocket.rocket_id)) - @staticmethod - async def get_rocket(rocket_id: int) -> "Union[Rocket, HTTPException]": + async def get_rocket_by_id( + rocket_id: str, + ) -> Union[Rocket, HTTPException]: """ Get a rocket from the database. Args: - rocket_id (int): Rocket id. + rocket_id: str Returns: - rocket model object + models.Rocket Raises: HTTP 404 Not Found: If the rocket is not found in the database. """ - successfully_read_rocket = await RocketRepository( - rocket_id=rocket_id - ).get_rocket() - if not successfully_read_rocket: + try: + async with RocketRepository() as rocket_repo: + await rocket_repo.get_rocket_by_id(rocket_id) + read_rocket = rocket_repo.rocket + 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 read_rocket: + return read_rocket raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, - detail="Rocket not found.", + detail="Rocket not found", + ) + finally: + logger.info( + f"Call to controllers.rocket.get_rocket_by_id completed for Rocket {rocket_id}" ) - return successfully_read_rocket - - @staticmethod - async def get_rocketpy_rocket( - rocket_id: int, - ) -> "Union[RocketPickle, HTTPException]": + @classmethod + async def get_rocketpy_rocket_as_jsonpickle( + cls, rocket_id: str + ) -> Union[RocketPickle, HTTPException]: """ - Get a rocketpy rocket object encoded as jsonpickle string from the database. + Get a rocketpy.Rocket object as jsonpickle string. Args: - rocket_id (int): rocket id. + rocket_id: str Returns: - str: jsonpickle string of the rocketpy rocket. + views.RocketPickle Raises: HTTP 404 Not Found: If the rocket is not found in the database. """ - successfully_read_rocket = await RocketRepository( - rocket_id=rocket_id - ).get_rocket() - if not successfully_read_rocket: + try: + read_rocket = await cls.get_rocket_by_id(rocket_id) + rocketpy_rocket = cls.get_rocketpy_rocket(read_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_as_jsonpickle: {exc_str}" + ) raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail="Rocket not found.", + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Failed to read rocket: {exc_str}", + ) from e + else: + return RocketPickle( + jsonpickle_rocketpy_rocket=jsonpickle.encode(rocketpy_rocket) ) - - successfully_read_rocketpy_rocket = RocketController( - rocket=successfully_read_rocket, - rocket_option=RocketOptions( - successfully_read_rocket._rocket_option - ), - motor_kind=MotorKinds(successfully_read_rocket.motor._motor_kind), - ).rocketpy_rocket - - return RocketPickle( - jsonpickle_rocketpy_rocket=jsonpickle.encode( - successfully_read_rocketpy_rocket + finally: + logger.info( + f"Call to controllers.rocket.get_rocketpy_rocket_as_jsonpickle completed for Rocket {rocket_id}" ) - ) - async def update_rocket( - self, rocket_id: int - ) -> "Union[RocketUpdated, HTTPException]": + async def update_rocket_by_id( + self, rocket_id: str + ) -> Union[RocketUpdated, HTTPException]: """ - Update a rocket in the database. + Update a models.Rocket in the database. Args: - rocket_id (int): rocket id. + rocket_id: str Returns: - RocketUpdated: rocket id and message. + views.RocketUpdated Raises: HTTP 404 Not Found: If the rocket is not found in the database. """ - successfully_read_rocket = await RocketRepository( - rocket_id=rocket_id - ).get_rocket() - if not successfully_read_rocket: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail="Rocket not found.", - ) - - successfully_updated_rocket = await RocketRepository( - rocket=self.rocket, rocket_id=rocket_id - ).update_rocket(rocket_option=self.rocket_option) - if not successfully_updated_rocket: + try: + async with RocketRepository() as rocket_repo: + rocket_repo.fetch_rocket(self.rocket) + await rocket_repo.create_rocket( + rocket_option=self.rocket_option, + motor_kind=self.motor_kind, + ) + await rocket_repo.delete_rocket_by_id(rocket_id) + 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="Failed to update rocket.", + detail=f"Failed to update rocket: {exc_str}", + ) from e + else: + return RocketUpdated(new_rocket_id=self.rocket.rocket_id) + finally: + logger.info( + f"Call to controllers.rocket.update_rocket completed for Rocket {rocket_id}" ) - return RocketUpdated(new_rocket_id=str(successfully_updated_rocket)) - @staticmethod - async def delete_rocket( - rocket_id: int, - ) -> "Union[RocketDeleted, HTTPException]": + async def delete_rocket_by_id( + rocket_id: str, + ) -> Union[RocketDeleted, HTTPException]: """ - Delete a rocket from the database. + Delete a models.Rocket from the database. Args: - rocket_id (int): Rocket id. + rocket_id: str Returns: - RocketDeleted: Rocket id and message. + views.RocketDeleted Raises: HTTP 404 Not Found: If the rocket is not found in the database. """ - successfully_read_rocket = await RocketRepository( - rocket_id=rocket_id - ).get_rocket() - if not successfully_read_rocket: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail="Rocket not found.", - ) - - successfully_deleted_rocket = await RocketRepository( - rocket_id=rocket_id - ).delete_rocket() - if not successfully_deleted_rocket: + try: + async with RocketRepository() as rocket_repo: + await rocket_repo.delete_rocket_by_id(rocket_id) + 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="Failed to delete rocket.", + detail=f"Failed to delete rocket: {exc_str}", + ) from e + else: + return RocketDeleted(deleted_rocket_id=rocket_id) + finally: + logger.info( + f"Call to controllers.rocket.delete_rocket completed for Rocket {rocket_id}" ) - return RocketDeleted(deleted_rocket_id=str(rocket_id)) - - @staticmethod - async def simulate( - rocket_id: int, - ) -> "Union[RocketSummary, HTTPException]": + @classmethod + async def simulate_rocket( + cls, + rocket_id: str, + ) -> Union[RocketSummary, HTTPException]: """ - Simulate a rocket rocket. + Simulate a rocketpy rocket. Args: - rocket_id (int): Rocket id. + rocket_id: str Returns: - Rocket summary view. + views.RocketSummary Raises: HTTP 404 Not Found: If the rocket does not exist in the database. """ - successfully_read_rocket = await RocketRepository( - rocket_id=rocket_id - ).get_rocket() - if not successfully_read_rocket: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail="Rocket not found.", - ) - try: - rocket = RocketController( - rocket=successfully_read_rocket, - rocket_option=RocketOptions( - successfully_read_rocket._rocket_option - ), - motor_kind=MotorKinds( - successfully_read_rocket.motor._motor_kind - ), - ).rocketpy_rocket + read_rocket = await cls.get_rocket_by_id(rocket_id) + rocket = cls.get_rocketpy_rocket(read_rocket) _inertia_details = InertiaDetails( rocket_mass_without_propellant="Rocket Mass: {:.3f} kg (No Propellant)".format( @@ -539,7 +429,7 @@ async def simulate( aerodynamics_center_of_pressure=_aerodynamics_center_of_pressure, distance_cop_to_codm="Distance from Center of Pressure to Center of Dry Mass: " + "{:.3f}".format( - rocket.center_of_mass(0) - rocket.cp_position + rocket.center_of_mass(0) - rocket.cp_position(0) ) + " m", initial_static_margin="Initial Static Margin: " @@ -590,9 +480,156 @@ async def simulate( ) rocket_summary = RocketSummary(rocket_data=_rocket_data) - return 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: {e}", + detail=f"Failed to simulate rocket: {exc_str}", ) from e + else: + return rocket_summary + finally: + logger.info( + f"Call to controllers.rocket.simulate completed for Rocket {rocket_id}" + ) + + @staticmethod + def get_rocketpy_nose(nose: NoseCone) -> RocketPyNoseCone: + """ + Get a rocketpy nose cone object. + + Returns: + RocketPyNoseCone + """ + + rocketpy_nose = RocketPyNoseCone( + length=nose.length, + kind=nose.kind, + base_radius=nose.base_radius, + rocket_radius=nose.rocket_radius, + ) + rocketpy_nose.position = nose.position + return rocketpy_nose + + @staticmethod + def get_rocketpy_finset( + trapezoidal_fins: TrapezoidalFins, + ) -> RocketPyTrapezoidalFins: + """ + Get a rocketpy finset object. + + Returns: + RocketPyTrapezoidalFins + """ + rocketpy_finset = RocketPyTrapezoidalFins( + n=trapezoidal_fins.n, + root_chord=trapezoidal_fins.root_chord, + tip_chord=trapezoidal_fins.tip_chord, + span=trapezoidal_fins.span, + cant_angle=trapezoidal_fins.cant_angle, + rocket_radius=trapezoidal_fins.radius, + airfoil=trapezoidal_fins.airfoil, + ) + rocketpy_finset.position = trapezoidal_fins.position + return rocketpy_finset + + @staticmethod + def get_rocketpy_tail(tail: Tail) -> RocketPyTail: + """ + Get a rocketpy tail object. + + Returns: + RocketPyTail + """ + rocketpy_tail = RocketPyTail( + top_radius=tail.top_radius, + bottom_radius=tail.bottom_radius, + length=tail.length, + rocket_radius=tail.radius, + ) + rocketpy_tail.position = tail.position + return rocketpy_tail + + @staticmethod + def get_rocketpy_parachute( + parachute: Parachute, p: int + ) -> RocketPyParachute: + """ + Get a rocketpy parachute object. + + Returns: + RocketPyParachute + """ + rocketpy_parachute = RocketPyParachute( + name=parachute[p].name[0], + cd_s=parachute[p].cd_s[0], + trigger=eval(parachute[p].triggers[0]), + sampling_rate=parachute[p].sampling_rate[0], + lag=parachute[p].lag[0], + noise=parachute[p].noise[0], + ) + return rocketpy_parachute + + @staticmethod + def check_parachute_trigger(expression: str) -> bool: + """ + Check if the trigger expression is valid. + + Args: + expression: str + + Returns: + bool: True if the expression is valid, False otherwise. + """ + + # Parsing the expression into an AST + try: + parsed_expression = ast.parse(expression, mode="eval") + except SyntaxError: + print("Invalid syntax.") + return False + + # Constant case (supported after beta v1) + if isinstance(parsed_expression.body, ast.Constant): + return True + # Name case (supported after beta v1) + if ( + isinstance(parsed_expression.body, ast.Name) + and parsed_expression.body.id == "apogee" + ): + global apogee + apogee = "apogee" + return True + + # Validating the expression structure + if not isinstance(parsed_expression.body, ast.Lambda): + print("Invalid expression structure (not a Lambda).") + return False + + lambda_node = parsed_expression.body + if len(lambda_node.args.args) != 3: + print("Invalid expression structure (invalid arity).") + return False + + if not isinstance(lambda_node.body, ast.Compare): + try: + for operand in lambda_node.body.values: + if not isinstance(operand, ast.Compare): + print("Invalid expression structure (not a Compare).") + return False + except AttributeError: + print("Invalid expression structure (not a Compare).") + return False + + # Restricting access to functions or attributes + for node in ast.walk(lambda_node): + if isinstance(node, ast.Call): + print("Calling functions is not allowed in the expression.") + return False + if isinstance(node, ast.Attribute): + print("Accessing attributes is not allowed in the expression.") + return False + return True diff --git a/lib/data/engines/Cesaroni_M1400.eng b/lib/data/engines/CESARONI_M1400.env similarity index 100% rename from lib/data/engines/Cesaroni_M1400.eng rename to lib/data/engines/CESARONI_M1400.env diff --git a/lib/data/engines/Cesaroni_7450M2505-P.csv b/lib/data/engines/Cesaroni_7450M2505-P.csv deleted file mode 100644 index 12aa26d..0000000 --- a/lib/data/engines/Cesaroni_7450M2505-P.csv +++ /dev/null @@ -1,13 +0,0 @@ -0,0 -0.1200,2600.0000 -0.2100,2482.0000 -0.6000,2715.0000 -0.9000,2876.0000 -1.2000,2938.0000 -1.5000,2889.0000 -1.8000,2785.0000 -2.1000,2573.0000 -2.4000,2349.0000 -2.7000,2182.0000 -2.9900,85.0000 -3.0000,0.0000 diff --git a/lib/models/aerosurfaces.py b/lib/models/aerosurfaces.py index 73284ea..0dff121 100644 --- a/lib/models/aerosurfaces.py +++ b/lib/models/aerosurfaces.py @@ -28,8 +28,7 @@ class Fins(BaseModel, frozen=True): class TrapezoidalFins(Fins, frozen=True): - def __init__(self): - super().__init__() + pass class Tail(BaseModel, frozen=True): diff --git a/lib/models/environment.py b/lib/models/environment.py index cd2b9ab..4074fde 100644 --- a/lib/models/environment.py +++ b/lib/models/environment.py @@ -14,3 +14,7 @@ class Env(BaseModel, frozen=True): date: Optional[datetime.datetime] = ( datetime.datetime.today() + datetime.timedelta(days=1) ) + + @property + def env_id(self) -> str: + return str(hash(self)) diff --git a/lib/models/flight.py b/lib/models/flight.py index 61343cc..ab945ec 100644 --- a/lib/models/flight.py +++ b/lib/models/flight.py @@ -9,3 +9,7 @@ class Flight(BaseModel, frozen=True): inclination: int = 85 heading: int = 0 rail_length: float = 5.2 + + @property + def flight_id(self) -> str: + return str(hash(self)) diff --git a/lib/models/motor.py b/lib/models/motor.py index 635ee51..1598b49 100644 --- a/lib/models/motor.py +++ b/lib/models/motor.py @@ -11,25 +11,25 @@ class MotorKinds(str, Enum): - hybrid: str = "Hybrid" - solid: str = "Solid" - liquid: str = "Liquid" + HYBRID: str = "HYBRID" + SOLID: str = "SOLID" + LIQUID: str = "LIQUID" class MotorEngines(str, Enum): - cesaroni: str = "Cesaroni_M1670" - custom: str = "Custom" + CESARONI_M1670: str = "CESARONI_M1670" + CUSTOM: str = "CUSTOM" class TankKinds(str, Enum): - level: str = "Level" - mass: str = "Mass" - mass_flow: str = "MassFlow" - ullage: str = "Ullage" + LEVEL: str = "LEVEL" + MASS: str = "MASS" + MASS_FLOW: str = "MASSFlOW" + ULLAGE: str = "ULLAGE" class TankFluids(BaseModel, frozen=True): - name: str = "FluidName" + name: str = "FLUIDNAME" density: float = 100.0 @@ -39,7 +39,7 @@ class MotorTank(BaseModel, frozen=True): ((0, 5), 1), ((5, 10), 2), ] - tank_kind: TankKinds = TankKinds.mass_flow + tank_kind: TankKinds = TankKinds.MASS_FLOW gas: TankFluids = TankFluids() liquid: TankFluids = TankFluids() name: str = "Tank" @@ -71,17 +71,17 @@ def __init__(self, **kwargs): } match self.tank_kind: - case TankKinds.level: + case TankKinds.LEVEL: tank = LevelBasedTank( **tank_core, liquid_height=self.liquid_height ) - case TankKinds.mass: + case TankKinds.MASS: tank = MassBasedTank( **tank_core, liquid_mass=self.liquid_mass, gas_mass=self.gas_mass, ) - case TankKinds.mass_flow: + case TankKinds.MASS_FLOW: tank = MassFlowRateBasedTank( **tank_core, gas_mass_flow_rate_in=self.gas_mass_flow_rate_in, @@ -105,7 +105,7 @@ class Motor(BaseModel, frozen=True): # TODO: thrust_source must be the id of a previously uploaded .eng file and a list of "default" files must be provided in the api docs # Required parameters - thrust_source: MotorEngines = MotorEngines.cesaroni + thrust_source: MotorEngines = MotorEngines.CESARONI_M1670 burn_time: float = 3.9 nozzle_radius: float = 0.033 dry_mass: float = 1.815 @@ -128,10 +128,18 @@ class Motor(BaseModel, frozen=True): "nozzle_to_combustion_chamber" ) - def __init__(self, motor_kind=MotorKinds.solid, **kwargs): + def __init__(self, motor_kind=MotorKinds.SOLID, **kwargs): super().__init__(**kwargs) self._motor_kind = motor_kind + @property + def motor_kind(self) -> MotorKinds: + return self._motor_kind + + @property + def motor_id(self) -> str: + return str(hash(self)) + def __hash__(self): temp = vars(self) temp = str(temp) diff --git a/lib/models/rocket.py b/lib/models/rocket.py index aa8665d..b78e1b6 100644 --- a/lib/models/rocket.py +++ b/lib/models/rocket.py @@ -7,8 +7,8 @@ class RocketOptions(str, Enum): - CALISTO: str = "Calisto" - CUSTOM: str = "Custom" + CALISTO: str = "CALISTO" + CUSTOM: str = "CUSTOM" class Rocket(BaseModel, frozen=True): @@ -44,6 +44,14 @@ def __init__(self, rocket_option=RocketOptions.CALISTO, **kwargs): super().__init__(**kwargs) self._rocket_option = rocket_option + @property + def rocket_option(self) -> RocketOptions: + return self._rocket_option + + @property + def rocket_id(self) -> str: + return str(hash(self)) + def __hash__(self): temp = vars(self) temp = str(temp) diff --git a/lib/repositories/__init__.py b/lib/repositories/__init__.py index 87e5b46..c8add6a 100644 --- a/lib/repositories/__init__.py +++ b/lib/repositories/__init__.py @@ -1,6 +1 @@ # lib/controllers/__init__.py - - -def parse_error(e): - exc_str = f"{e}".replace("\n", " ").replace(" ", " ") - return exc_str diff --git a/lib/repositories/environment.py b/lib/repositories/environment.py index 1301498..a494d3b 100644 --- a/lib/repositories/environment.py +++ b/lib/repositories/environment.py @@ -1,11 +1,8 @@ from typing import Union -from lib import logging -from lib.repositories import parse_error +from lib import logger, parse_error from lib.models.environment import Env from lib.repositories.repo import Repository -logger = logging.getLogger(__name__) - class EnvRepository(Repository): """ @@ -17,16 +14,18 @@ class EnvRepository(Repository): """ - def __init__(self, environment: Env = None, env_id: str = None): + def __init__(self): super().__init__("environments") - self._env = environment - if env_id: - self._env_id = env_id - else: - self._env_id = str(hash(self._env)) + self._env = None + self._env_id = None + + def fetch_env(self, environment: Env): + self.env = environment + self.env_id = environment.env_id + return self @property - def env(self) -> "Env": + def env(self) -> Env: return self._env @env.setter @@ -34,13 +33,24 @@ def env(self, environment: "Env"): self._env = environment @property - def env_id(self) -> "str": + def env_id(self) -> str: return self._env_id @env_id.setter def env_id(self, env_id: "str"): self._env_id = env_id + async def insert_env(self, env_data: dict): + await self.collection.insert_one(env_data) + return self + + async def find_env(self, env_id: str): + return await self.collection.find_one({"env_id": env_id}) + + async def delete_env(self, env_id: str): + await self.collection.delete_one({"env_id": env_id}) + return self + async def create_env(self): """ Creates a non-existing models.Env in the database @@ -51,75 +61,56 @@ async def create_env(self): try: environment_to_dict = self.env.dict() environment_to_dict["env_id"] = self.env_id - await self.collection.insert_one(environment_to_dict) + await self.insert_env(environment_to_dict) except Exception as e: exc_str = parse_error(e) logger.error(f"repositories.environment.create_env: {exc_str}") - raise Exception(f"Error creating environment: {str(e)}") from e + raise Exception(f"Error creating environment: {exc_str}") from e else: return self finally: logger.info( - f"Call to repositories.environment.create_env completed; states: EnvID {self.env_id}" + f"Call to repositories.environment.create_env completed for Env {self.env_id}" ) - async def update_env(self): + async def get_env_by_id(self, env_id: str) -> Union[Env, None]: """ - Updates a models.Env in the database + Gets a models.Env from the database Returns: - self + self """ try: - environment_to_dict = self.env.dict() - environment_to_dict["env_id"] = str(hash(self.env)) - await self.collection.update_one( - {"env_id": self.env_id}, {"$set": environment_to_dict} - ) - self.env_id = environment_to_dict["env_id"] + 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 Exception as e: exc_str = parse_error(e) - logger.error(f"repositories.environment.update_env: {exc_str}") - raise Exception(f"Error updating environment: {str(e)}") from e + logger.error(f"repositories.environment.get_env: {exc_str}") + raise Exception(f"Error getting environment: {exc_str}") from e else: return self finally: logger.info( - f"Call to repositories.environment.update_env completed; states: Env {hash(self.env)}, EnvID {self.env_id}" + f"Call to repositories.environment.get_env completed for Env {env_id}" ) - async def get_env(self) -> "Union[Env, None]": - """ - Gets a models.Env from the database - - Returns: - models.Env - """ - try: - read_env = await self.collection.find_one({"env_id": self.env_id}) - except Exception as e: - logger.error(f"repositories.environment.get_env: {str(e)}") - raise Exception(f"Error getting environment: {str(e)}") from e - else: - return Env.parse_obj(read_env) if read_env else None - finally: - logger.info( - f"Call to repositories.environment.get_env completed; states: Env {hash(self.env)}, EnvID {self.env_id}" - ) - - async def delete_env(self): + async def delete_env_by_id(self, env_id: str): """ Deletes a models.Env from the database Returns: - None + self """ try: - await self.collection.delete_one({"env_id": self.env_id}) + await self.delete_env(env_id) except Exception as e: - logger.error(f"repositories.environment.delete_env: {str(e)}") - raise Exception(f"Error deleting environment: {str(e)}") from e + exc_str = parse_error(e) + logger.error(f"repositories.environment.delete_env: {exc_str}") + raise Exception(f"Error deleting environment: {exc_str}") from e + else: + return self finally: logger.info( - f"Call to repositories.environment.delete_env completed; states: Env {hash(self.env)}, EnvID {self.env_id}" + f"Call to repositories.environment.delete_env completed for Env {env_id}" ) diff --git a/lib/repositories/flight.py b/lib/repositories/flight.py index 3a14bc6..7aa9964 100644 --- a/lib/repositories/flight.py +++ b/lib/repositories/flight.py @@ -1,113 +1,125 @@ from typing import Union -from pymongo.results import InsertOneResult -from pymongo.results import DeleteResult +from lib import logger, parse_error from lib.models.flight import Flight from lib.repositories.repo import Repository class FlightRepository(Repository): """ - Flight repository + Enables database CRUD operations with models.Flight Init Attributes: - flight: Flight object - flight_id: Flight id + flight: models.Flight + flight_id: str - Enables CRUD operations on flight objects """ - def __init__(self, flight: Flight = None, flight_id: str = None): + def __init__(self): super().__init__("flights") + self._flight = None + self._flight_id = None + + def fetch_flight(self, flight: Flight): self.flight = flight - if flight_id: - self.flight_id = flight_id - else: - self.flight_id = self.flight.__hash__() + self.flight_id = flight.flight_id + return self - def __del__(self): - super().__del__() + @property + def flight(self) -> Flight: + return self._flight - async def create_flight( - self, motor_kind: str = "Solid", rocket_option: str = "Calisto" - ) -> "InsertOneResult": - """ - Creates a flight in the database + @flight.setter + def flight(self, flight: "Flight"): + self._flight = flight - Args: - rocketpy_flight: rocketpy flight object + @property + def flight_id(self) -> str: + return self._flight_id - Returns: - InsertOneResult: result of the insert operation - """ - if not await self.get_flight(): - try: - flight_to_dict = self.flight.dict() - flight_to_dict["flight_id"] = self.flight_id - flight_to_dict["rocket"]["rocket_option"] = rocket_option - flight_to_dict["rocket"]["motor"]["motor_kind"] = motor_kind - return await self.collection.insert_one(flight_to_dict) - except Exception as e: - raise Exception(f"Error creating flight: {str(e)}") from e - finally: - self.__del__() - else: - return InsertOneResult(acknowledged=True, inserted_id=None) + @flight_id.setter + def flight_id(self, flight_id: "str"): + self._flight_id = flight_id + + async def insert_flight(self, flight_data: dict): + await self.collection.insert_one(flight_data) + + async def find_flight(self, flight_id: str): + return await self.collection.find_one({"flight_id": flight_id}) - async def update_flight( - self, motor_kind: str = "Solid", rocket_option: str = "Calisto" - ) -> "Union[int, None]": + async def delete_flight(self, flight_id: str): + await self.collection.delete_one({"flight_id": flight_id}) + return self + + async def create_flight( + self, *, motor_kind: str = "SOLID", rocket_option: str = "CALISTO" + ): """ - Updates a flight in the database + Creates a non-existing models.Flight in the database + + Args: + rocket_option: models.rocket.RocketOptions + motor_kind: models.motor.MotorKinds Returns: - int: flight id + self """ try: flight_to_dict = self.flight.dict() - flight_to_dict["flight_id"] = self.flight.__hash__() + flight_to_dict["flight_id"] = self.flight_id flight_to_dict["rocket"]["rocket_option"] = rocket_option flight_to_dict["rocket"]["motor"]["motor_kind"] = motor_kind - - await self.collection.update_one( - {"flight_id": self.flight_id}, {"$set": flight_to_dict} - ) - - self.flight_id = flight_to_dict["flight_id"] - return self.flight_id + await self.insert_flight(flight_to_dict) except Exception as e: - raise Exception(f"Error updating flight: {str(e)}") from e + exc_str = parse_error(e) + logger.error(f"repositories.flight.create_flight: {exc_str}") + raise Exception(f"Error creating flight: {exc_str}") from e + else: + return self finally: - self.__del__() + logger.info( + f"Call to repositories.flight.create_flight completed for Flight {self.flight_id}" + ) - async def get_flight(self) -> "Union[Flight, None]": + async def get_flight_by_id(self, flight_id: str) -> Union[Flight, None]: """ - Gets a flight from the database + Gets a models.Flight from the database Returns: - models.Flight: Model flight object + self """ try: - flight = await self.collection.find_one( - {"flight_id": self.flight_id} + read_flight = await self.find_flight(flight_id) + parsed_flight = ( + Flight.parse_obj(read_flight) if read_flight else None ) - if flight is not None: - return Flight.parse_obj(flight) - return None + self.flight = parsed_flight except Exception as e: - raise Exception(f"Error getting flight: {str(e)}") from e + exc_str = parse_error(e) + logger.error(f"repositories.flight.get_flight: {exc_str}") + raise Exception(f"Error getting flight: {exc_str}") from e + else: + return self + finally: + logger.info( + f"Call to repositories.flight.get_flight completed for Flight {flight_id}" + ) - async def delete_flight(self) -> "DeleteResult": + async def delete_flight_by_id(self, flight_id: str): """ - Deletes a flight from the database + Deletes a models.Flight from the database Returns: - DeleteResult: result of the delete operation + self """ try: - return await self.collection.delete_one( - {"flight_id": self.flight_id} - ) + await self.delete_flight(flight_id) except Exception as e: - raise Exception(f"Error deleting flight: {str(e)}") from e + exc_str = parse_error(e) + logger.error(f"repositories.flight.delete_flight: {exc_str}") + raise Exception(f"Error deleting flight: {exc_str}") from e + else: + return self finally: - self.__del__() + logger.info( + f"Call to repositories.flight.delete_flight completed for Flight {flight_id}" + ) diff --git a/lib/repositories/motor.py b/lib/repositories/motor.py index 552b90a..fb9d34f 100644 --- a/lib/repositories/motor.py +++ b/lib/repositories/motor.py @@ -1,109 +1,120 @@ from typing import Union -from pymongo.results import InsertOneResult -from pymongo.results import DeleteResult +from lib import logger, parse_error from lib.models.motor import Motor from lib.repositories.repo import Repository class MotorRepository(Repository): """ - Motor repository + Enables database CRUD operations with models.Motor Init Attributes: - Motor: Motor object - motor_id: motor id + motor: models.Motor + motor_id: str - Enables CRUD operations on motor objects """ - def __init__(self, motor: Motor = None, motor_id: str = None) -> None: + def __init__(self): super().__init__("motors") + self._motor = None + self._motor_id = None + + def fetch_motor(self, motor: Motor): self.motor = motor - if motor_id: - self.motor_id = motor_id - else: - self.motor_id = self.motor.__hash__() + self.motor_id = motor.motor_id + return self - def __del__(self): - super().__del__() + @property + def motor(self) -> Motor: + return self._motor - async def create_motor( - self, motor_kind: str = "solid" - ) -> "InsertOneResult": - """ - Creates a motor in the database + @motor.setter + def motor(self, motor: "Motor"): + self._motor = motor - Args: - rocketpy_Motor: rocketpy motor object + @property + def motor_id(self) -> str: + return self._motor_id - Returns: - InsertOneResult: result of the insert operation - """ - if not await self.get_motor(): - try: - motor_to_dict = self.motor.dict() - motor_to_dict["motor_id"] = self.motor_id - motor_to_dict["motor_kind"] = motor_kind - return await self.collection.insert_one(motor_to_dict) - except Exception as e: - raise Exception(f"Error creating motor: {str(e)}") from e - finally: - self.__del__() - else: - return InsertOneResult(acknowledged=True, inserted_id=None) + @motor_id.setter + def motor_id(self, motor_id: "str"): + self._motor_id = motor_id + + async def insert_motor(self, motor_data: dict): + await self.collection.insert_one(motor_data) + return self + + async def find_motor(self, motor_id: str): + return await self.collection.find_one({"motor_id": motor_id}) - async def update_motor( - self, motor_kind: str = "solid" - ) -> "Union[int, None]": + async def delete_motor(self, motor_id: str): + await self.collection.delete_one({"motor_id": motor_id}) + return self + + async def create_motor(self, motor_kind: str = "SOLID"): """ - Updates a motor in the database + Creates a non-existing models.Motor in the database + + Args: + motor_kind: models.motor.MotorKinds Returns: - int: Motor id + self """ try: motor_to_dict = self.motor.dict() - motor_to_dict["motor_id"] = self.motor.__hash__() + motor_to_dict["motor_id"] = self.motor_id motor_to_dict["motor_kind"] = motor_kind - - await self.collection.update_one( - {"motor_id": self.motor_id}, {"$set": motor_to_dict} - ) - - self.motor_id = motor_to_dict["motor_id"] - return self.motor_id + await self.insert_motor(motor_to_dict) except Exception as e: - raise Exception(f"Error updating motor: {str(e)}") from e + exc_str = parse_error(e) + logger.error(f"repositories.motor.create_motor: {exc_str}") + raise Exception(f"Error creating motor: {exc_str}") from e + else: + return self finally: - self.__del__() + logger.info( + f"Call to repositories.motor.create_motor completed for Motor {self.motor_id}" + ) - async def get_motor(self) -> "Union[motor, None]": + async def get_motor_by_id(self, motor_id: str) -> Union[motor, None]: """ - Gets a motor from the database + Gets a models.Motor from the database Returns: - models.motor: Model motor object + self """ try: - motor = await self.collection.find_one({"motor_id": self.motor_id}) - if motor is not None: - return Motor.parse_obj(motor) - return None + read_motor = await self.find_motor(motor_id) + parsed_motor = Motor.parse_obj(read_motor) if read_motor else None + self.motor = parsed_motor except Exception as e: - raise Exception(f"Error getting motor: {str(e)}") from e + exc_str = parse_error(e) + logger.error(f"repositories.motor.get_motor: {exc_str}") + raise Exception(f"Error getting motor: {exc_str}") from e + else: + return self + finally: + logger.info( + f"Call to repositories.motor.get_motor completed for Motor {motor_id}" + ) - async def delete_motor(self) -> "DeleteResult": + async def delete_motor_by_id(self, motor_id: str): """ - Deletes a motor from the database + Deletes a models.Motor from the database Returns: - DeleteResult: result of the delete operation + self """ try: - return await self.collection.delete_one( - {"motor_id": self.motor_id} - ) + await self.delete_motor(motor_id) except Exception as e: - raise Exception(f"Error deleting motor: {str(e)}") from e + exc_str = parse_error(e) + logger.error(f"repositories.motor.delete_motor: {exc_str}") + raise Exception(f"Error deleting motor: {exc_str}") from e + else: + return self finally: - self.__del__() + logger.info( + f"Call to repositories.motor.delete_motor completed for Motor {motor_id}" + ) diff --git a/lib/repositories/repo.py b/lib/repositories/repo.py index cfebf67..12b63bb 100644 --- a/lib/repositories/repo.py +++ b/lib/repositories/repo.py @@ -1,6 +1,8 @@ +import asyncio from motor.motor_asyncio import AsyncIOMotorClient from pymongo.server_api import ServerApi -from lib.secrets import secrets_instance +from lib import logger +from lib.secrets import Secrets class Repository: @@ -8,19 +10,99 @@ class Repository: Base class for all repositories (singleton) """ - _self = None + _instances = {} + _lock = asyncio.Lock() def __new__(cls, *args, **kwargs): - if cls._self is None: - cls._self = super().__new__(cls) - return cls._self - - def __init__(self, collection: str): - self.connection_string = secrets_instance.get_secret( - "MONGODB_CONNECTION_STRING" - ) - self.client = AsyncIOMotorClient( - self.connection_string, server_api=ServerApi("1") - ) - self.db = self.client.rocketpy - self.collection = self.db[collection] + if cls not in cls._instances: + instance = super(Repository, cls).__new__(cls) + cls._instances[cls] = instance + return cls._instances[cls] + + def __init__(self, collection_name: str): + if not getattr(self, '_initialized', False): + self._collection_name = collection_name + self._initialized_event = asyncio.Event() + if not asyncio.get_event_loop().is_running(): + asyncio.run(self._async_init()) + else: + loop = asyncio.get_event_loop() + loop.create_task(self._async_init()).add_done_callback( + self._on_init_done + ) + + def _on_init_done(self, future): + try: + future.result() + except Exception as e: + logger.error("Initialization failed: %s", e, exc_info=True) + raise e from e + + async def _async_init(self): + async with self._lock: + self._initialize_connection() + self._initialized = True + self._initialized_event.set() + + async def __aenter__(self): + await self._initialized_event.wait() + return self + + async def __aexit__(self, exc_type, exc_value, traceback): + await self._initialized_event.wait() + async with self._lock: + self._cleanup_instance() + + def _initialize_connection(self): + try: + self._connection_string = Secrets.get_secret( + "MONGODB_CONNECTION_STRING" + ) + self._client = AsyncIOMotorClient( + self._connection_string, + server_api=ServerApi("1"), + maxIdleTimeMS=5000, + connectTimeoutMS=5000, + serverSelectionTimeoutMS=15000, + ) + self._collection = self._client.rocketpy[self._collection_name] + logger.info("MongoDB client initialized for %s", self.__class__) + except Exception as e: + logger.error( + f"Failed to initialize MongoDB client: {e}", exc_info=True + ) + raise ConnectionError( + "Could not establish a connection with MongoDB." + ) from e + + def _cleanup_instance(self): + if hasattr(self, '_client'): + self.client.close() + logger.info("Connection closed for %s", self.__class__) + self._instances.pop(self.__class__, None) + + @property + def connection_string(self): + return self._connection_string + + @connection_string.setter + def connection_string(self, value): + self._connection_string = value + + @property + def client(self): + if not getattr(self, '_initialized', False): + raise RuntimeError("Repository not initialized yet") + return self._client + + @client.setter + def client(self, value): + if not getattr(self, '_initialized', False): + raise RuntimeError("Repository not initialized yet") + self._client = value + + @property + def collection(self): + if not getattr(self, '_initialized', False): + raise RuntimeError("Repository not initialized yet") + return self._collection diff --git a/lib/repositories/rocket.py b/lib/repositories/rocket.py index 39536f5..d272369 100644 --- a/lib/repositories/rocket.py +++ b/lib/repositories/rocket.py @@ -1,111 +1,126 @@ from typing import Union -from pymongo.results import InsertOneResult -from pymongo.results import DeleteResult +from lib import logger, parse_error from lib.models.rocket import Rocket from lib.repositories.repo import Repository class RocketRepository(Repository): """ - Rocket repository + Enables database CRUD operations with models.Rocket Init Attributes: - rocket: Rocket object - rocket_id: Rocket id + rocket: models.Rocket + rocket_id: str - Enables CRUD operations on rocket objects """ - def __init__(self, rocket: Rocket = None, rocket_id: str = None): + def __init__(self): super().__init__("rockets") + self._rocket_id = None + self._rocket = None + + def fetch_rocket(self, rocket: Rocket): self.rocket = rocket - if rocket_id: - self.rocket_id = rocket_id - else: - self.rocket_id = self.rocket.__hash__() + self.rocket_id = rocket.rocket_id + return self - def __del__(self): - super().__del__() + @property + def rocket(self) -> Rocket: + return self._rocket - async def create_rocket( - self, rocket_option: str = "Calisto" - ) -> "InsertOneResult": - """ - Creates a rocket in the database + @rocket.setter + def rocket(self, rocket: "Rocket"): + self._rocket = rocket - Args: - rocketpy_rocket: rocketpy rocket object + @property + def rocket_id(self) -> str: + return self._rocket_id - Returns: - InsertOneResult: result of the insert operation - """ - if not await self.get_rocket(): - try: - rocket_to_dict = self.rocket.dict() - rocket_to_dict["rocket_id"] = self.rocket_id - rocket_to_dict["rocket_option"] = rocket_option - return await self.collection.insert_one(rocket_to_dict) - except Exception as e: - raise Exception(f"Error creating rocket: {str(e)}") from e - finally: - self.__del__() - else: - return InsertOneResult(acknowledged=True, inserted_id=None) + @rocket_id.setter + def rocket_id(self, rocket_id: "str"): + self._rocket_id = rocket_id + + async def insert_rocket(self, rocket_data: dict): + await self.collection.insert_one(rocket_data) + return self + + async def find_rocket(self, rocket_id: str): + return await self.collection.find_one({"rocket_id": rocket_id}) - async def update_rocket( - self, rocket_option: str = "Calisto" - ) -> "Union[int, None]": + async def delete_rocket(self, rocket_id: str): + await self.collection.delete_one({"rocket_id": rocket_id}) + return self + + async def create_rocket( + self, *, rocket_option: str = "CALISTO", motor_kind: str = "SOLID" + ): """ - Updates a rocket in the database + Creates a non-existing models.Rocket in the database + + Args: + rocket_option: models.rocket.RocketOptions + motor_kind: models.motor.MotorKinds Returns: - int: rocket id + self """ try: rocket_to_dict = self.rocket.dict() - rocket_to_dict["rocket_id"] = self.rocket.__hash__() + rocket_to_dict["rocket_id"] = self.rocket_id rocket_to_dict["rocket_option"] = rocket_option - - await self.collection.update_one( - {"rocket_id": self.rocket_id}, {"$set": rocket_to_dict} - ) - - self.rocket_id = rocket_to_dict["rocket_id"] - return self.rocket_id + rocket_to_dict["motor"]["motor_kind"] = motor_kind + await self.insert_rocket(rocket_to_dict) except Exception as e: - raise Exception(f"Error updating rocket: {str(e)}") from e + exc_str = parse_error(e) + logger.error(f"repositories.rocket.create_rocket: {exc_str}") + raise Exception(f"Error creating rocket: {exc_str}") from e + else: + return self finally: - self.__del__() + logger.info( + f"Call to repositories.rocket.create_rocket completed for Rocket {self.rocket_id}" + ) - async def get_rocket(self) -> "Union[Rocket, None]": + async def get_rocket_by_id(self, rocket_id: str) -> Union[Rocket, None]: """ - Gets a rocket from the database + Gets a models.Rocket from the database Returns: - models.Rocket: Model rocket object + models.Rocket """ try: - rocket = await self.collection.find_one( - {"rocket_id": self.rocket_id} + read_rocket = await self.find_rocket(rocket_id) + parsed_rocket = ( + Rocket.parse_obj(read_rocket) if read_rocket else None ) - if rocket is not None: - return Rocket.parse_obj(rocket) - return None + self.rocket = parsed_rocket except Exception as e: - raise Exception(f"Error getting rocket: {str(e)}") from e + exc_str = parse_error(e) + logger.error(f"repositories.rocket.get_rocket: {exc_str}") + raise Exception(f"Error getting rocket: {exc_str}") from e + else: + return self + finally: + logger.info( + f"Call to repositories.rocket.get_rocket completed for Rocket {rocket_id}" + ) - async def delete_rocket(self) -> "DeleteResult": + async def delete_rocket_by_id(self, rocket_id: str): """ - Deletes a rocket from the database + Deletes a models.Rocket from the database Returns: - DeleteResult: result of the delete operation + self """ try: - return await self.collection.delete_one( - {"rocket_id": self.rocket_id} - ) + await self.delete_rocket(rocket_id) except Exception as e: - raise Exception(f"Error deleting rocket: {str(e)}") from e + exc_str = parse_error(e) + logger.error(f"repositories.rocket.delete_rocket: {exc_str}") + raise Exception(f"Error deleting rocket: {exc_str}") from e + else: + return self finally: - self.__del__() + logger.info( + f"Call to repositories.rocket.delete_rocket completed for Rocket {rocket_id}" + ) diff --git a/lib/routes/environment.py b/lib/routes/environment.py index d3c8dec..6647073 100644 --- a/lib/routes/environment.py +++ b/lib/routes/environment.py @@ -26,7 +26,7 @@ @router.post("/") -async def create_env(env: Env) -> "EnvCreated": +async def create_env(env: Env) -> EnvCreated: """ Creates a new environment @@ -37,7 +37,7 @@ async def create_env(env: Env) -> "EnvCreated": @router.get("/{env_id}") -async def read_env(env_id: str) -> "Env": +async def read_env(env_id: str) -> Env: """ Reads an environment @@ -48,7 +48,7 @@ async def read_env(env_id: str) -> "Env": @router.put("/{env_id}") -async def update_env(env_id: str, env: Env) -> "EnvUpdated": +async def update_env(env_id: str, env: Env) -> EnvUpdated: """ Updates an environment @@ -58,22 +58,22 @@ async def update_env(env_id: str, env: Env) -> "EnvUpdated": env: models.Env JSON ``` """ - return await EnvController(env).update_env(env_id) + return await EnvController(env).update_env_by_id(env_id) @router.delete("/{env_id}") -async def delete_env(env_id: str) -> "EnvDeleted": +async def delete_env(env_id: str) -> EnvDeleted: """ Deletes an environment ## Args ``` env_id: Environment ID hash ``` """ - return await EnvController.delete_env(env_id) + return await EnvController.delete_env_by_id(env_id) @router.get("/rocketpy/{env_id}") -async def read_rocketpy_env(env_id: str) -> "EnvPickle": +async def read_rocketpy_env(env_id: str) -> EnvPickle: """ Loads rocketpy.environment as jsonpickle string @@ -84,11 +84,11 @@ async def read_rocketpy_env(env_id: str) -> "EnvPickle": @router.get("/{env_id}/simulate") -async def simulate_env(env_id: str) -> "EnvSummary": +async def simulate_env(env_id: str) -> EnvSummary: """ Loads rocketpy.environment simulation ## Args ``` env_id: str ``` """ - return await EnvController.simulate(env_id) + return await EnvController.simulate_env(env_id) diff --git a/lib/routes/flight.py b/lib/routes/flight.py index 8283085..863862b 100644 --- a/lib/routes/flight.py +++ b/lib/routes/flight.py @@ -31,7 +31,7 @@ @router.post("/") async def create_flight( flight: Flight, rocket_option: RocketOptions, motor_kind: MotorKinds -) -> "FlightCreated": +) -> FlightCreated: """ Creates a new flight @@ -39,34 +39,34 @@ async def create_flight( ``` Flight object as JSON ``` """ return await FlightController( - flight, rocket_option, motor_kind + flight, rocket_option=rocket_option, motor_kind=motor_kind ).create_flight() @router.get("/{flight_id}") -async def read_flight(flight_id: int) -> "Flight": +async def read_flight(flight_id: str) -> Flight: """ Reads a flight ## Args ``` flight_id: Flight ID hash ``` """ - return await FlightController.get_flight(flight_id) + return await FlightController.get_flight_by_id(flight_id) @router.get("/rocketpy/{flight_id}") -async def read_rocketpy_flight(flight_id: int) -> "FlightPickle": +async def read_rocketpy_flight(flight_id: str) -> FlightPickle: """ Reads a rocketpy flight object ## Args ``` flight_id: Flight ID hash. ``` """ - return await FlightController.get_rocketpy_flight(flight_id) + return await FlightController.get_rocketpy_flight_as_jsonpickle(flight_id) @router.put("/{flight_id}/env") -async def update_flight_env(flight_id: int, env: Env) -> "FlightUpdated": +async def update_flight_env(flight_id: str, env: Env) -> FlightUpdated: """ Updates flight environment @@ -76,16 +76,16 @@ async def update_flight_env(flight_id: int, env: Env) -> "FlightUpdated": env: env object as JSON ``` """ - return await FlightController.update_env(flight_id, env) + return await FlightController.update_env_by_flight_id(flight_id, env=env) @router.put("/{flight_id}/rocket") async def update_flight_rocket( - flight_id: int, + flight_id: str, rocket: Rocket, rocket_option: RocketOptions, motor_kind: MotorKinds, -) -> "FlightUpdated": +) -> FlightUpdated: """ Updates flight rocket. @@ -95,18 +95,21 @@ async def update_flight_rocket( rocket: Rocket object as JSON ``` """ - return await FlightController.update_rocket( - flight_id, rocket, rocket_option, motor_kind + return await FlightController.update_rocket_by_flight_id( + flight_id, + rocket=rocket, + rocket_option=rocket_option, + motor_kind=motor_kind, ) @router.put("/{flight_id}") async def update_flight( - flight_id: int, + flight_id: str, flight: Flight, rocket_option: RocketOptions, motor_kind: MotorKinds, -) -> "FlightUpdated": +) -> FlightUpdated: """ Updates Flight object @@ -117,27 +120,27 @@ async def update_flight( ``` """ return await FlightController( - flight, rocket_option, motor_kind - ).update_flight(flight_id) + flight, rocket_option=rocket_option, motor_kind=motor_kind + ).update_flight_by_id(flight_id) @router.delete("/{flight_id}") -async def delete_flight(flight_id: int) -> "FlightDeleted": +async def delete_flight(flight_id: str) -> FlightDeleted: """ Deletes a flight ## Args ``` flight_id: Flight ID hash ``` """ - return await FlightController.delete_flight(flight_id) + return await FlightController.delete_flight_by_id(flight_id) @router.get("/{flight_id}/simulate") -async def simulate_flight(flight_id: int) -> "FlightSummary": +async def simulate_flight(flight_id: str) -> FlightSummary: """ Simulates a flight ## Args ``` flight_id: Flight ID hash ``` """ - return await FlightController.simulate(flight_id) + return await FlightController.simulate_flight(flight_id) diff --git a/lib/routes/motor.py b/lib/routes/motor.py index 85ad263..3f74825 100644 --- a/lib/routes/motor.py +++ b/lib/routes/motor.py @@ -26,31 +26,33 @@ @router.post("/") -async def create_motor(motor: Motor, motor_kind: MotorKinds) -> "MotorCreated": +async def create_motor(motor: Motor, motor_kind: MotorKinds) -> MotorCreated: """ Creates a new motor ## Args ``` Motor object as a JSON ``` """ - return await MotorController(motor, motor_kind).create_motor() + return await MotorController( + motor=motor, motor_kind=motor_kind + ).create_motor() @router.get("/{motor_id}") -async def read_motor(motor_id: int) -> "Motor": +async def read_motor(motor_id: str) -> Motor: """ Reads a motor ## Args ``` motor_id: Motor ID hash ``` """ - return await MotorController.get_motor(motor_id) + return await MotorController.get_motor_by_id(motor_id) @router.put("/{motor_id}") async def update_motor( - motor_id: int, motor: Motor, motor_kind: MotorKinds -) -> "MotorUpdated": + motor_id: str, motor: Motor, motor_kind: MotorKinds +) -> MotorUpdated: """ Updates a motor @@ -60,37 +62,39 @@ async def update_motor( motor: Motor object as JSON ``` """ - return await MotorController(motor, motor_kind).update_motor(motor_id) + return await MotorController( + motor=motor, motor_kind=motor_kind + ).update_motor_by_id(motor_id) @router.delete("/{motor_id}") -async def delete_motor(motor_id: int) -> "MotorDeleted": +async def delete_motor(motor_id: str) -> MotorDeleted: """ Deletes a motor ## Args ``` motor_id: Motor ID hash ``` """ - return await MotorController.delete_motor(motor_id) + return await MotorController.delete_motor_by_id(motor_id) @router.get("/rocketpy/{motor_id}") -async def read_rocketpy_motor(motor_id: int) -> "MotorPickle": +async def read_rocketpy_motor(motor_id: str) -> MotorPickle: """ Reads a rocketpy motor ## Args ``` motor_id: Motor ID hash ``` """ - return await MotorController.get_rocketpy_motor(motor_id) + return await MotorController.get_rocketpy_motor_as_jsonpickle(motor_id) @router.get("/{motor_id}/simulate") -async def simulate_motor(motor_id: int) -> "MotorSummary": +async def simulate_motor(motor_id: str) -> MotorSummary: """ Simulates a motor ## Args ``` motor_id: Motor ID hash ``` """ - return await MotorController.simulate(motor_id) + return await MotorController.simulate_motor(motor_id) diff --git a/lib/routes/rocket.py b/lib/routes/rocket.py index 4681763..b8b9687 100644 --- a/lib/routes/rocket.py +++ b/lib/routes/rocket.py @@ -29,7 +29,7 @@ @router.post("/") async def create_rocket( rocket: Rocket, rocket_option: RocketOptions, motor_kind: MotorKinds -) -> "RocketCreated": +) -> RocketCreated: """ Creates a new rocket @@ -37,28 +37,28 @@ async def create_rocket( ``` Rocket object as a JSON ``` """ return await RocketController( - rocket, rocket_option, motor_kind + rocket=rocket, rocket_option=rocket_option, motor_kind=motor_kind ).create_rocket() @router.get("/{rocket_id}") -async def read_rocket(rocket_id: int) -> Rocket: +async def read_rocket(rocket_id: str) -> Rocket: """ Reads a rocket ## Args ``` rocket_id: Rocket ID hash ``` """ - return await RocketController.get_rocket(rocket_id) + return await RocketController.get_rocket_by_id(rocket_id) @router.put("/{rocket_id}") async def update_rocket( - rocket_id: int, + rocket_id: str, rocket: Rocket, rocket_option: RocketOptions, motor_kind: MotorKinds, -) -> "RocketUpdated": +) -> RocketUpdated: """ Updates a rocket @@ -69,38 +69,38 @@ async def update_rocket( ``` """ return await RocketController( - rocket, rocket_option, motor_kind - ).update_rocket(rocket_id) + rocket=rocket, rocket_option=rocket_option, motor_kind=motor_kind + ).update_rocket_by_id(rocket_id) @router.delete("/{rocket_id}") -async def delete_rocket(rocket_id: int) -> "RocketDeleted": +async def delete_rocket(rocket_id: str) -> RocketDeleted: """ Deletes a rocket ## Args ``` rocket_id: Rocket ID hash ``` """ - return await RocketController.delete_rocket(rocket_id) + return await RocketController.delete_rocket_by_id(rocket_id) @router.get("/rocketpy/{rocket_id}") -async def read_rocketpy_rocket(rocket_id: int) -> "RocketPickle": +async def read_rocketpy_rocket(rocket_id: str) -> RocketPickle: """ Reads a rocketpy rocket ## Args ``` rocket_id: Rocket ID hash ``` """ - return await RocketController.get_rocketpy_rocket(rocket_id) + return await RocketController.get_rocketpy_rocket_as_jsonpickle(rocket_id) @router.get("/{rocket_id}/simulate") -async def simulate_rocket(rocket_id: int) -> "RocketSummary": +async def simulate_rocket(rocket_id: str) -> RocketSummary: """ Simulates a rocket ## Args ``` rocket_id: Rocket ID hash ``` """ - return await RocketController.simulate(rocket_id) + return await RocketController.simulate_rocket(rocket_id) diff --git a/lib/secrets.py b/lib/secrets.py index 422a491..42ddf94 100644 --- a/lib/secrets.py +++ b/lib/secrets.py @@ -1,25 +1,21 @@ import os from dotenv import dotenv_values -from pydantic import BaseModel -class Secrets(BaseModel): +class Secrets: """ Secrets class to load secrets from .env file """ - secrets: dict = dotenv_values(".env") + secrets = dotenv_values(".env") @staticmethod def get_os_secret(key): return os.environ.get(key) - def get_secret(self, key): - dotenv_secret = self.secrets.get(key) + @classmethod + def get_secret(cls, key): + dotenv_secret = cls.secrets.get(key) if not dotenv_secret: - return self.get_os_secret(key) + return cls.get_os_secret(key) return dotenv_secret - - -# global instance -secrets_instance = Secrets() diff --git a/pyproject.toml b/pyproject.toml index 1ba712f..b78b26d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,10 @@ +[build-system] +requires = ["setuptools", "setuptools_scm"] +build-backend = "setuptools.build_meta" + +[tool.setuptools.dynamic] +dependencies = {file = ["requirements.txt"]} + [project] name = "Infinity-API" version = "1.1.0" @@ -12,15 +19,12 @@ maintainers = [ {name = "Luiz Mota", email = "luiz.mota1999@usp.br"} ] readme = "README.md" -keywords = ["rocketpy", "rocket flight", "simulation", "API"] +keywords = ["rocketpy", "API", "simulation", "rocket", "flight"] classifiers = [ "Development Status :: Alpha", "Programming Language :: Python" ] -[tool.setuptools.dynamic] -dependencies = { file = ["requirements.txt"] } - [project.urls] Homepage = "http://api.rocketpy.org/" Documentation = "http://api.rocketpy.org/docs" @@ -47,5 +51,10 @@ disable = """ logging-fstring-interpolation, broad-exception-raised, import-error, - protected-access + protected-access, + unnecessary-dunder-call, + wrong-import-position, + consider-using-f-string, + too-many-function-args, + no-member """